module RackStatsD VERSION = "0.2.1" # 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' HEADERS = {"Content-Type" => "text/plain"}.freeze # 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 if @callback.respond_to?(:call) return @callback.call else return [200, HEADERS, [@callback.to_s]] end 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 @host = options.key?(:host) ? options[:host] : `hostname -s`.chomp @sha = options[:revision] || '' end def call(env) status, headers, body = @app.call(env) headers['X-Node'] = @host if @host 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 REQUEST_METHOD = 'REQUEST_METHOD'.freeze VALID_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'].freeze # 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. # :window - The Integer number of seconds before the # horizon resets. # :stats - Optional StatsD client. # :hostname - Optional String hostname. Set to nil # to exclude. # :stats_prefix - Optional String prefix for StatsD keys. # Default: "rack" 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] 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(".") 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 def record_request(status, env) now = Time.now diff = (now - @start) @active_time += diff @requests += 1 $0 = procline if @stats @stats.timing("#{@stats_prefix}.response_time", diff * 1000) if VALID_METHODS.include?(env[REQUEST_METHOD]) stat = "#{@stats_prefix}.response_time.#{env[REQUEST_METHOD].downcase}" @stats.timing(stat, diff * 1000) end if suffix = status_suffix(status) @stats.increment "#{@stats_prefix}.status_code.#{status_suffix(status)}" end if @track_gc && GC.time > 0 @stats.timing "#{@stats_prefix}.gc.time", GC.time / 1000 @stats.count "#{@stats_prefix}.gc.collections", GC.collections 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 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 when 400 then :bad_request when 401 then :unauthorized when 402 then :payment_required when 403 then :forbidden when 404 then :missing when 410 then :gone when 422 then :invalid when 500 then :error when 502 then :bad_gateway when 503 then :node_down when 504 then :gateway_timeout 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) if @body.respond_to?(:each) @body.each(&block) else block.call(@body) end 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) body = Body.new(body) { record_request(status, env) } [status, headers, body] end end end