├── .consolerc ├── .github └── dependabot.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── examples ├── Gemfile ├── Gemfile.lock ├── config.ru ├── paralell_test.rb └── unicorn.rb ├── lib └── rack │ ├── server_status.rb │ └── server_status │ └── version.rb ├── rack-server_status.gemspec └── spec ├── server_status_spec.rb └── spec_helper.rb /.consolerc: -------------------------------------------------------------------------------- 1 | # This file is automatically loaded when `bundle console` is run. You can add 2 | # fixtures and/or initialization code here to make experimenting with your gem 3 | # easier. 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "20:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | tmp 11 | vendor 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3 4 | - 2.4 5 | - 2.5 6 | - 2.6 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in rack-server_status.gemspec 4 | gemspec 5 | 6 | group :development, :test do 7 | gem 'pry' 8 | end 9 | group :test do 10 | gem 'rack' 11 | gem 'timecop' 12 | end 13 | 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 SpringMT 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rack::ServerStatus [![Build Status](https://travis-ci.org/SpringMT/rack-server_status.svg?branch=master)](https://travis-ci.org/SpringMT/rack-server_status) 2 | 3 | This is a Ruby version of [kazeburo/Plack-Middleware-ServerStatus-Lite](https://github.com/kazeburo/Plack-Middleware-ServerStatus-Lite). 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'rack-server_status' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install rack-server_status 20 | 21 | ## Usage 22 | ### Getting started 23 | Tell your app to use the Rack::ServerStatus middleware. 24 | 25 | #### For Rails 3+ apps 26 | 27 | ``` 28 | # In config/application.rb 29 | config.middleware.use Rack::ServerStatus, scoreboard_path: './tmp' 30 | ``` 31 | 32 | #### Rackup files 33 | 34 | ``` 35 | # In config.ru 36 | use Rack::ServerStatus, scoreboard_path: './tmp' 37 | ``` 38 | 39 | ### Get Status 40 | 41 | ``` 42 | % curl http://server:port/server-status 43 | Uptime: 1432227723 (12 seconds) 44 | BusyWorkers: 1 45 | IdleWorkers: 3 46 | -- 47 | pid status remote_addr host method uri protocol ss 48 | 55091 _ - 0 49 | 55092 _ - 1 50 | 55093 A 127.0.0.1 localhost:3000 GET /server-status HTTP/1.1 0 51 | 55094 _ - 0 52 | 53 | # JSON format 54 | % curl http://server:port/server-status?json 55 | {"Uptime":1432388968,"BusyWorkers":1,"IdleWorkers":3,"stats":[{"remote_addr":null,"host":"-","method":null,"uri":null,"protocol":null,"pid":87240,"status":"_","ss":2},{"remote_addr":"127.0.0.1","host":"localhost:3000","method":"GET","uri":"/server-status?json","protocol":"HTTP/1.1","pid":87241,"status":"A","ss":0},{"remote_addr":null,"host":"-","method":null,"uri":null,"protocol":null,"pid":87242,"status":"_","ss":3},{"remote_addr":null,"host":"-","method":null,"uri":null,"protocol":null,"pid":87243,"status":"_","ss":3}]} 56 | ``` 57 | 58 | ## Configuration 59 | 60 | | name | detail | example | default | 61 | |------|--------|---------|---------| 62 | | path | location that displays server status | `path: '/server-status'` | `/server-status` | 63 | | allow | host based access control of a page of server status. | `allow: ['127.0.0.1']` | `[]` | 64 | | scoreboard | scoreboard directory | `scoreboard_path: './tmp'` | nil | 65 | | skip_ps_command | | `skip_ps_command: true` | false | 66 | 67 | 68 | ## Contributing 69 | 70 | 1. Fork it ( https://github.com/[my-github-username]/rack-server_status/fork ) 71 | 2. Create your feature branch (`git checkout -b my-new-feature`) 72 | 3. Commit your changes (`git commit -am 'Add some feature'`) 73 | 4. Push to the branch (`git push origin my-new-feature`) 74 | 5. Create a new Pull Request 75 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | 8 | -------------------------------------------------------------------------------- /examples/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'unicorn' 3 | gem 'worker_scoreboard', git: 'https://github.com/SpringMT/worker_scoreboard.git' 4 | gem 'parallel' 5 | -------------------------------------------------------------------------------- /examples/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/SpringMT/worker_scoreboard.git 3 | revision: 9f6ab52e0e132339ea019a9be22d9c586d9036fd 4 | specs: 5 | worker_scoreboard (0.0.5) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | kgio (2.11.2) 11 | parallel (1.12.1) 12 | raindrops (0.19.0) 13 | unicorn (5.4.0) 14 | kgio (~> 2.6) 15 | raindrops (~> 0.7) 16 | 17 | PLATFORMS 18 | ruby 19 | 20 | DEPENDENCIES 21 | parallel 22 | unicorn 23 | worker_scoreboard! 24 | 25 | BUNDLED WITH 26 | 1.16.1 27 | -------------------------------------------------------------------------------- /examples/config.ru: -------------------------------------------------------------------------------- 1 | $:.unshift File.join(File.dirname(__FILE__), '..', 'lib/') 2 | require 'rack/server_status' 3 | 4 | use Rack::ServerStatus, scoreboard_path: './tmp' 5 | 6 | class HelloWorldApp 7 | def call(env) 8 | #sleep 10 9 | [ 200, { 'Content-Type' => 'text/plain' }, ['Hello World!'] ] 10 | end 11 | end 12 | 13 | map "/foo" do 14 | run HelloWorldApp.new 15 | end 16 | 17 | -------------------------------------------------------------------------------- /examples/paralell_test.rb: -------------------------------------------------------------------------------- 1 | require 'parallel' 2 | require 'net/http' 3 | $proc_num = 5 4 | $execute_num = 10 5 | 6 | Parallel.map([1,2,3,4,5,6], :in_processes => $proc_num) do |letter| 7 | $execute_num.times do 8 | http = Net::HTTP.new('localhost', 3000) 9 | req = Net::HTTP::Get.new('/foo') 10 | http.request(req) 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /examples/unicorn.rb: -------------------------------------------------------------------------------- 1 | worker_processes 4 2 | -------------------------------------------------------------------------------- /lib/rack/server_status.rb: -------------------------------------------------------------------------------- 1 | require 'rack/server_status/version' 2 | require 'json' 3 | require 'worker_scoreboard' 4 | 5 | module Rack 6 | class ServerStatus 7 | def initialize(app, options = {}) 8 | @app = app 9 | @uptime = Time.now.to_i 10 | @skip_ps_command = options[:skip_ps_command] || false 11 | @path = options[:path] || '/server-status' 12 | @allow = options[:allow] || [] 13 | scoreboard_path = options[:scoreboard_path] 14 | unless scoreboard_path.nil? 15 | @scoreboard = WorkerScoreboard.new(scoreboard_path) 16 | end 17 | end 18 | 19 | def call(env) 20 | set_state!('A', env) 21 | 22 | if env['PATH_INFO'] == @path 23 | handle_server_status(env) 24 | else 25 | @app.call(env) 26 | end 27 | ensure 28 | set_state!('_') 29 | end 30 | 31 | private 32 | 33 | def set_state!(status = '_', env) 34 | return if @scoreboard.nil? 35 | prev = {} 36 | unless env.nil? 37 | prev = { 38 | remote_addr: env['REMOTE_ADDR'], 39 | host: env['HTTP_HOST'] || '-', 40 | method: env['REQUEST_METHOD'], 41 | uri: env['REQUEST_URI'], 42 | protocol: env['SERVER_PROTOCOL'], 43 | time: Time.now.to_i 44 | } 45 | end 46 | prev[:pid] = Process.pid 47 | prev[:ppid] = Process.ppid 48 | prev[:uptime] = @uptime 49 | prev[:status] = status 50 | 51 | @scoreboard.update(prev.to_json) 52 | end 53 | 54 | def allowed?(address) 55 | return true if @allow.empty? 56 | @allow.include?(address) 57 | end 58 | 59 | def handle_server_status(env) 60 | unless allowed?(env['REMOTE_ADDR']) 61 | return [403, {'Content-Type' => 'text/plain'}, [ 'Forbidden' ]] 62 | end 63 | 64 | upsince = Time.now.to_i - @uptime 65 | duration = "#{upsince} seconds" 66 | body = "Uptime: #{@uptime} (#{duration})\n" 67 | status = {Uptime: @uptime} 68 | 69 | unless @scoreboard.nil? 70 | stats = @scoreboard.read_all 71 | parent_pid = Process.ppid 72 | all_workers = [] 73 | idle = 0 74 | busy = 0 75 | if @skip_ps_command 76 | all_workers = stats.keys 77 | elsif RUBY_PLATFORM !~ /mswin(?!ce)|mingw|cygwin|bccwin/ 78 | ps = `LC_ALL=C command ps -e -o ppid,pid` 79 | ps.each_line do |line| 80 | line.lstrip! 81 | next if line =~ /^\D/ 82 | ppid, pid = line.chomp.split(/\s+/, 2) 83 | all_workers << pid.to_i if ppid.to_i == parent_pid 84 | end 85 | else 86 | all_workers = stats.keys 87 | end 88 | process_status_str = '' 89 | process_status_list = [] 90 | 91 | all_workers.each do |pid| 92 | json =stats[pid] || '{}' 93 | pstatus = begin; JSON.parse(json, symbolize_names: true); rescue; end 94 | pstatus ||= {} 95 | if !pstatus[:status].nil? && pstatus[:status] == 'A' 96 | busy += 1 97 | else 98 | idle += 1 99 | end 100 | unless pstatus[:time].nil? 101 | pstatus[:ss] = Time.now.to_i - pstatus[:time].to_i 102 | end 103 | pstatus[:pid] ||= pid 104 | pstatus.delete :time 105 | pstatus.delete :ppid 106 | pstatus.delete :uptime 107 | process_status_str << sprintf("%s\n", [:pid, :status, :remote_addr, :host, :method, :uri, :protocol, :ss].map {|item| pstatus[item] || '' }.join(' ')) 108 | process_status_list << pstatus 109 | end 110 | body << <<"EOF" 111 | BusyWorkers: #{busy} 112 | IdleWorkers: #{idle} 113 | -- 114 | pid status remote_addr host method uri protocol ss 115 | #{process_status_str} 116 | EOF 117 | body.chomp! 118 | status[:BusyWorkers] = busy 119 | status[:IdleWorkers] = idle 120 | status[:stats] = process_status_list 121 | else 122 | body << "WARN: Scoreboard has been disabled\n" 123 | status[:WARN] = 'Scoreboard has been disabled' 124 | end 125 | if (env['QUERY_STRING'] || '') =~ /\bjson\b/ 126 | return [200, {'Content-Type' => 'application/json; charset=utf-8'}, [status.to_json]] 127 | end 128 | return [200, {'Content-Type' => 'text/plain'}, [body]] 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/rack/server_status/version.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class ServerStatus 3 | VERSION = "0.0.4" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /rack-server_status.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rack/server_status/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "rack-server_status" 8 | spec.version = Rack::ServerStatus::VERSION 9 | spec.authors = ["SpringMT"] 10 | spec.email = ["today.is.sky.blue.sky@gmail.com"] 11 | 12 | #spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com' to prevent pushes to rubygems.org, or delete to allow pushes to any server." 13 | spec.required_rubygems_version = ">= 2.0" 14 | 15 | spec.summary = %q{Show server status} 16 | spec.description = %q{Show server status} 17 | spec.homepage = "https://github.com/SpringMT/rack-server_status" 18 | spec.license = "MIT" 19 | 20 | spec.files = `git ls-files -z`.split("\x0") 21 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 22 | spec.test_files = spec.files.grep(%r{^(test|spec|features|examples)/}) 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_dependency "worker_scoreboard" 26 | spec.add_development_dependency "bundler", "~> 1.7" 27 | spec.add_development_dependency "rake", "~> 13.0" 28 | spec.add_development_dependency "rspec" 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/server_status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require File.dirname(__FILE__) + '/spec_helper' 4 | 5 | describe Rack::ServerStatus do 6 | app = lambda { |env| 7 | [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] 8 | } 9 | 10 | context 'confirm to Rack::Lint' do 11 | context 'Not affected WorkerScoreboard' do 12 | subject do 13 | Rack::Lint.new(Rack::ServerStatus.new(app)) 14 | end 15 | it do 16 | response = Rack::MockRequest.new(subject).get('/') 17 | expect(response.body).to eq 'Hello, World!' 18 | end 19 | end 20 | context 'Affected WorkerScoreboard' do 21 | subject do 22 | Rack::Lint.new(Rack::ServerStatus.new(app, scoreboard_path: Dir.tmpdir)) 23 | end 24 | it do 25 | response = Rack::MockRequest.new(subject).get('/') 26 | expect(response.body).to eq 'Hello, World!' 27 | end 28 | end 29 | end 30 | 31 | context 'return valid server-status' do 32 | subject do 33 | Rack::Lint.new(Rack::ServerStatus.new(app, scoreboard_path: Dir.tmpdir)) 34 | end 35 | it do 36 | response = Rack::MockRequest.new(subject).get('/server-status') 37 | expect(response.successful?).to be_truthy 38 | expect(response.headers['Content-Type']).to eq 'text/plain' 39 | end 40 | end 41 | 42 | context 'return json valid server-status' do 43 | subject do 44 | Rack::Lint.new(Rack::ServerStatus.new(app, scoreboard_path: Dir.tmpdir)) 45 | end 46 | it do 47 | response = Rack::MockRequest.new(subject).get('/server-status?json') 48 | expect(response.successful?).to be_truthy 49 | expect(response.headers['Content-Type']).to eq 'application/json; charset=utf-8' 50 | end 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup(:default, :test) 3 | Bundler.require(:default, :test) 4 | 5 | $TESTING=true 6 | $:.unshift File.join(File.dirname(__FILE__), '..', 'lib/rack/') 7 | require 'rack/server_status' 8 | --------------------------------------------------------------------------------