2011-12-14 21:29:17 +04:00
|
|
|
module RackStatsD
|
2013-04-16 03:55:17 +04:00
|
|
|
VERSION = "0.2.1"
|
2011-12-03 03:29:33 +04:00
|
|
|
|
|
|
|
# Simple middleware to add a quick status URL for tools like Nagios.
|
|
|
|
class RequestStatus
|
|
|
|
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
|
|
|
GET = 'GET'.freeze
|
|
|
|
PATH_INFO = 'PATH_INFO'.freeze
|
|
|
|
STATUS_PATH = '/status'
|
2013-01-25 10:05:07 +04:00
|
|
|
HEADERS = {"Content-Type" => "text/plain"}.freeze
|
2011-12-03 03:29:33 +04:00
|
|
|
|
|
|
|
# Initializes the middleware.
|
|
|
|
#
|
|
|
|
# # Responds with "OK" on /status
|
|
|
|
# use RequestStatus, "OK"
|
|
|
|
#
|
|
|
|
# You can change what URL to look for:
|
|
|
|
#
|
|
|
|
# use RequestStatus, "OK", "/ping"
|
|
|
|
#
|
|
|
|
# You can also check internal systems and return something more informative.
|
|
|
|
#
|
|
|
|
# use RequestStatus, lambda {
|
|
|
|
# status = MyApp.status # A Hash of some live counters or something
|
|
|
|
# [200, {"Content-Type" => "application/json"}, status.to_json]
|
|
|
|
# }
|
|
|
|
#
|
|
|
|
# app - The next Rack app in the pipeline.
|
|
|
|
# callback_or_response - Either a Proc or a Rack response.
|
|
|
|
# status_path - Optional String path that returns the status.
|
|
|
|
# Default: "/status"
|
|
|
|
#
|
|
|
|
# Returns nothing.
|
|
|
|
def initialize(app, callback_or_response, status_path = nil)
|
|
|
|
@app = app
|
|
|
|
@status_path = (status_path || STATUS_PATH).freeze
|
|
|
|
@callback = callback_or_response
|
|
|
|
end
|
|
|
|
|
|
|
|
def call(env)
|
|
|
|
if env[REQUEST_METHOD] == GET
|
|
|
|
if env[PATH_INFO] == @status_path
|
2012-04-27 04:02:40 +04:00
|
|
|
if @callback.respond_to?(:call)
|
|
|
|
return @callback.call
|
|
|
|
else
|
2013-01-25 10:05:07 +04:00
|
|
|
return [200, HEADERS, [@callback.to_s]]
|
2012-04-27 04:02:40 +04:00
|
|
|
end
|
2011-12-03 03:29:33 +04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@app.call env
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Simple middleware that adds the current host name and current git SHA to
|
|
|
|
# the response headers. This can help diagnose problems by letting you
|
|
|
|
# know what code is running from what machine.
|
|
|
|
class RequestHostname
|
|
|
|
# Initializes the middlware.
|
|
|
|
#
|
|
|
|
# app - The next Rack app in the pipeline.
|
|
|
|
# options - Hash of options.
|
|
|
|
# :host - String hostname.
|
|
|
|
# :revision - String SHA that describes the version of code
|
|
|
|
# this process is running.
|
|
|
|
#
|
|
|
|
# Returns nothing.
|
|
|
|
def initialize(app, options = {})
|
|
|
|
@app = app
|
2013-01-25 10:01:47 +04:00
|
|
|
@host = options.key?(:host) ? options[:host] : `hostname -s`.chomp
|
2011-12-03 03:29:33 +04:00
|
|
|
@sha = options[:revision] || '<none>'
|
|
|
|
end
|
|
|
|
|
|
|
|
def call(env)
|
|
|
|
status, headers, body = @app.call(env)
|
2013-01-25 10:01:47 +04:00
|
|
|
headers['X-Node'] = @host if @host
|
2011-12-03 03:29:33 +04:00
|
|
|
headers['X-Revision'] = @sha
|
|
|
|
[status, headers, body]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Middleware that tracks the amount of time this process spends processing
|
|
|
|
# requests, as opposed to being idle waiting for a connection. Statistics
|
|
|
|
# are dumped to rack.errors every 5 minutes.
|
|
|
|
#
|
|
|
|
# NOTE This middleware is not thread safe. It should only be used when
|
|
|
|
# rack.multiprocess is true and rack.multithread is false.
|
|
|
|
class ProcessUtilization
|
2012-02-09 21:33:13 +04:00
|
|
|
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
|
|
|
VALID_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'].freeze
|
|
|
|
|
2011-12-03 03:29:33 +04:00
|
|
|
# Initializes the middleware.
|
|
|
|
#
|
|
|
|
# app - The next Rack app in the pipeline.
|
|
|
|
# domain - The String domain name the app runs in.
|
|
|
|
# revision - The String SHA that describes the current version of code.
|
|
|
|
# options - Hash of options.
|
2011-12-03 04:38:48 +04:00
|
|
|
# :window - The Integer number of seconds before the
|
|
|
|
# horizon resets.
|
|
|
|
# :stats - Optional StatsD client.
|
2011-12-07 21:08:53 +04:00
|
|
|
# :hostname - Optional String hostname. Set to nil
|
|
|
|
# to exclude.
|
2011-12-03 04:38:48 +04:00
|
|
|
# :stats_prefix - Optional String prefix for StatsD keys.
|
|
|
|
# Default: "rack"
|
2011-12-03 03:29:33 +04:00
|
|
|
def initialize(app, domain, revision, options = {})
|
|
|
|
@app = app
|
|
|
|
@domain = domain
|
|
|
|
@revision = revision
|
|
|
|
@window = options[:window] || 100
|
|
|
|
@horizon = nil
|
|
|
|
@active_time = nil
|
|
|
|
@requests = nil
|
|
|
|
@total_requests = 0
|
|
|
|
@worker_number = nil
|
|
|
|
@track_gc = GC.respond_to?(:time)
|
|
|
|
|
|
|
|
if @stats = options[:stats]
|
2011-12-07 21:08:53 +04:00
|
|
|
prefix = [options[:stats_prefix] || :rack]
|
|
|
|
if options.has_key?(:hostname)
|
|
|
|
prefix << options[:hostname] unless options[:hostname].nil?
|
|
|
|
else
|
|
|
|
prefix << `hostname -s`.chomp
|
|
|
|
end
|
|
|
|
@stats_prefix = prefix.join(".")
|
2011-12-03 03:29:33 +04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# the app's domain name - shown in proctitle
|
|
|
|
attr_accessor :domain
|
|
|
|
|
|
|
|
# the currently running git revision as a 7-sha
|
|
|
|
attr_accessor :revision
|
|
|
|
|
|
|
|
# time when we began sampling. this is reset every once in a while so
|
|
|
|
# averages don't skew over time.
|
|
|
|
attr_accessor :horizon
|
|
|
|
|
|
|
|
# total number of requests that have been processed by this worker since
|
|
|
|
# the horizon time.
|
|
|
|
attr_accessor :requests
|
|
|
|
|
|
|
|
# decimal number of seconds the worker has been active within a request
|
|
|
|
# since the horizon time.
|
|
|
|
attr_accessor :active_time
|
|
|
|
|
|
|
|
# total requests processed by this worker process since it started
|
|
|
|
attr_accessor :total_requests
|
|
|
|
|
|
|
|
# the unicorn worker number
|
|
|
|
attr_accessor :worker_number
|
|
|
|
|
|
|
|
# the amount of time since the horizon
|
|
|
|
def horizon_time
|
|
|
|
Time.now - horizon
|
|
|
|
end
|
|
|
|
|
|
|
|
# decimal number of seconds this process has been active since the horizon
|
|
|
|
# time. This is the inverse of the active time.
|
|
|
|
def idle_time
|
|
|
|
horizon_time - active_time
|
|
|
|
end
|
|
|
|
|
|
|
|
# percentage of time this process has been active since the horizon time.
|
|
|
|
def percentage_active
|
|
|
|
(active_time / horizon_time) * 100
|
|
|
|
end
|
|
|
|
|
|
|
|
# percentage of time this process has been idle since the horizon time.
|
|
|
|
def percentage_idle
|
|
|
|
(idle_time / horizon_time) * 100
|
|
|
|
end
|
|
|
|
|
|
|
|
# number of requests processed per second since the horizon
|
|
|
|
def requests_per_second
|
|
|
|
requests / horizon_time
|
|
|
|
end
|
|
|
|
|
|
|
|
# average response time since the horizon in milliseconds
|
|
|
|
def average_response_time
|
|
|
|
(active_time / requests.to_f) * 1000
|
|
|
|
end
|
|
|
|
|
|
|
|
# called exactly once before the first request is processed by a worker
|
|
|
|
def first_request
|
|
|
|
reset_horizon
|
|
|
|
record_worker_number
|
|
|
|
end
|
|
|
|
|
|
|
|
# resets the horizon and all dependent variables
|
|
|
|
def reset_horizon
|
|
|
|
@horizon = Time.now
|
|
|
|
@active_time = 0.0
|
|
|
|
@requests = 0
|
|
|
|
end
|
|
|
|
|
|
|
|
# extracts the worker number from the unicorn procline
|
|
|
|
def record_worker_number
|
|
|
|
if $0 =~ /^.* worker\[(\d+)\].*$/
|
|
|
|
@worker_number = $1.to_i
|
|
|
|
else
|
|
|
|
@worker_number = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# the generated procline
|
|
|
|
def procline
|
|
|
|
"unicorn %s[%s] worker[%02d]: %5d reqs, %4.1f req/s, %4dms avg, %5.1f%% util" % [
|
|
|
|
domain,
|
|
|
|
revision,
|
|
|
|
worker_number.to_i,
|
|
|
|
total_requests.to_i,
|
|
|
|
requests_per_second.to_f,
|
|
|
|
average_response_time.to_i,
|
|
|
|
percentage_active.to_f
|
|
|
|
]
|
|
|
|
end
|
|
|
|
|
|
|
|
# called immediately after a request to record statistics, update the
|
|
|
|
# procline, and dump information to the logfile
|
2012-02-09 21:33:13 +04:00
|
|
|
def record_request(status, env)
|
2011-12-03 03:29:33 +04:00
|
|
|
now = Time.now
|
|
|
|
diff = (now - @start)
|
|
|
|
@active_time += diff
|
|
|
|
@requests += 1
|
|
|
|
|
|
|
|
$0 = procline
|
|
|
|
|
|
|
|
if @stats
|
2011-12-03 04:38:48 +04:00
|
|
|
@stats.timing("#{@stats_prefix}.response_time", diff * 1000)
|
2012-02-09 21:33:13 +04:00
|
|
|
if VALID_METHODS.include?(env[REQUEST_METHOD])
|
|
|
|
stat = "#{@stats_prefix}.response_time.#{env[REQUEST_METHOD].downcase}"
|
|
|
|
@stats.timing(stat, diff * 1000)
|
|
|
|
end
|
|
|
|
|
2011-12-03 03:29:33 +04:00
|
|
|
if suffix = status_suffix(status)
|
2011-12-03 04:38:48 +04:00
|
|
|
@stats.increment "#{@stats_prefix}.status_code.#{status_suffix(status)}"
|
2011-12-03 03:29:33 +04:00
|
|
|
end
|
|
|
|
if @track_gc && GC.time > 0
|
2011-12-03 04:38:48 +04:00
|
|
|
@stats.timing "#{@stats_prefix}.gc.time", GC.time / 1000
|
|
|
|
@stats.count "#{@stats_prefix}.gc.collections", GC.collections
|
2011-12-03 03:29:33 +04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
reset_horizon if now - horizon > @window
|
|
|
|
rescue => boom
|
|
|
|
warn "ProcessUtilization#record_request failed: #{boom}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def status_suffix(status)
|
|
|
|
suffix = case status.to_i
|
2013-01-24 04:22:50 +04:00
|
|
|
when 200 then :ok
|
|
|
|
when 201 then :created
|
|
|
|
when 202 then :accepted
|
|
|
|
when 301 then :moved_permanently
|
|
|
|
when 302 then :found
|
|
|
|
when 303 then :see_other
|
|
|
|
when 304 then :not_modified
|
|
|
|
when 305 then :use_proxy
|
|
|
|
when 307 then :temporary_redirect
|
2011-12-07 23:27:10 +04:00
|
|
|
when 400 then :bad_request
|
|
|
|
when 401 then :unauthorized
|
2013-01-24 04:22:50 +04:00
|
|
|
when 402 then :payment_required
|
|
|
|
when 403 then :forbidden
|
2011-12-03 03:29:33 +04:00
|
|
|
when 404 then :missing
|
2013-01-24 04:22:50 +04:00
|
|
|
when 410 then :gone
|
2011-12-03 03:29:33 +04:00
|
|
|
when 422 then :invalid
|
|
|
|
when 500 then :error
|
2013-01-24 04:22:50 +04:00
|
|
|
when 502 then :bad_gateway
|
|
|
|
when 503 then :node_down
|
|
|
|
when 504 then :gateway_timeout
|
2011-12-03 03:29:33 +04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Body wrapper. Yields to the block when body is closed. This is used to
|
|
|
|
# signal when a response is fully finished processing.
|
|
|
|
class Body
|
|
|
|
def initialize(body, &block)
|
|
|
|
@body = body
|
|
|
|
@block = block
|
|
|
|
end
|
|
|
|
|
|
|
|
def each(&block)
|
2013-04-16 03:54:59 +04:00
|
|
|
if @body.respond_to?(:each)
|
|
|
|
@body.each(&block)
|
|
|
|
else
|
|
|
|
block.call(@body)
|
|
|
|
end
|
2011-12-03 03:29:33 +04:00
|
|
|
end
|
|
|
|
|
|
|
|
def close
|
|
|
|
@body.close if @body.respond_to?(:close)
|
|
|
|
@block.call
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Rack entry point.
|
|
|
|
def call(env)
|
|
|
|
@start = Time.now
|
|
|
|
GC.clear_stats if @track_gc
|
|
|
|
|
|
|
|
@total_requests += 1
|
|
|
|
first_request if @total_requests == 1
|
|
|
|
|
|
|
|
env['process.request_start'] = @start.to_f
|
|
|
|
env['process.total_requests'] = total_requests
|
|
|
|
|
|
|
|
# newrelic X-Request-Start
|
|
|
|
env.delete('HTTP_X_REQUEST_START')
|
|
|
|
|
|
|
|
status, headers, body = @app.call(env)
|
2012-02-09 21:33:13 +04:00
|
|
|
body = Body.new(body) { record_request(status, env) }
|
2011-12-03 03:29:33 +04:00
|
|
|
[status, headers, body]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|