From 7022bfab09a2e036bf92f7892f1b4495d23f3426 Mon Sep 17 00:00:00 2001 From: zedshaw Date: Tue, 23 May 2006 09:49:28 +0000 Subject: A simple status and statistics handler for people. git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@209 19e92222-5c0b-0410-8929-a290d50e31e9 --- examples/simpletest.rb | 6 +++ lib/mongrel.rb | 6 +++ lib/mongrel/handlers.rb | 124 ++++++++++++++++++++++++++++++++++++++++++++---- lib/mongrel/stats.rb | 8 +++- 4 files changed, 133 insertions(+), 11 deletions(-) diff --git a/examples/simpletest.rb b/examples/simpletest.rb index 8ac2e98..48ff1dc 100644 --- a/examples/simpletest.rb +++ b/examples/simpletest.rb @@ -28,13 +28,19 @@ if ARGV.length != 3 exit(1) end +stats = Mongrel::StatisticsFilter.new(:sample_rate => 1) + config = Mongrel::Configurator.new :host => ARGV[0], :port => ARGV[1] do listener do uri "/", :handler => SimpleHandler.new uri "/", :handler => Mongrel::DeflateFilter.new + uri "/", :handler => stats uri "/dumb", :handler => DumbHandler.new uri "/dumb", :handler => Mongrel::DeflateFilter.new + uri "/dumb", :handler => stats uri "/files", :handler => Mongrel::DirHandler.new(ARGV[2]) + uri "/files", :handler => stats + uri "/status", :handler => Mongrel::StatusHandler.new(:stats_filter => stats) end trap("INT") { stop } diff --git a/lib/mongrel.rb b/lib/mongrel.rb index 9165f0a..e013e5c 100644 --- a/lib/mongrel.rb +++ b/lib/mongrel.rb @@ -45,6 +45,8 @@ end module Mongrel class URIClassifier + attr_reader :handler_map + # Returns the URIs that have been registered with this classifier so far. # The URIs returned should not be modified as this will cause a memory leak. # You can use this to inspect the contents of the URIClassifier. @@ -457,6 +459,8 @@ module Mongrel attr_reader :classifier attr_reader :host attr_reader :port + attr_reader :timeout + attr_reader :num_processors # Creates a working server on host:port (strange things happen if port isn't a Number). # Use HttpServer::run to start the server and HttpServer.acceptor.join to @@ -650,6 +654,8 @@ module Mongrel @classifier.register(uri, [handler]) end end + + handler.listener = self end # Removes any handlers registered at the given URI. See Mongrel::URIClassifier#unregister diff --git a/lib/mongrel/handlers.rb b/lib/mongrel/handlers.rb index 0e34a15..b70568c 100644 --- a/lib/mongrel/handlers.rb +++ b/lib/mongrel/handlers.rb @@ -1,3 +1,5 @@ +require 'mongrel/stats' + # Mongrel Web Server - A Mostly Ruby Webserver and Library # # Copyright (C) 2005 Zed A. Shaw zedshaw AT zedshaw dot com @@ -29,6 +31,7 @@ module Mongrel # class HttpHandler attr_reader :header_only + attr_accessor :listener def process(request, response) end @@ -42,6 +45,7 @@ module Mongrel module HttpHandlerPlugin attr_reader :options attr_reader :header_only + attr_accessor :listener def initialize(options={}) @options = options @@ -64,7 +68,7 @@ module Mongrel def initialize(msg) @response = Const::ERROR_404_RESPONSE + msg end - + # Just kicks back the standard 404 response with your special message. def process(request, response) response.socket.write(@response) @@ -165,7 +169,7 @@ module Mongrel if child == ".." out << "Up to parent..
" else - out << "#{child}
" + out << "#{child}
" end end out << "" @@ -177,7 +181,7 @@ module Mongrel end end - + # Sends the contents of a file back to the user. Not terribly efficient since it's # opening and closing the file for each read. def send_file(req_path, request, response, header_only=false) @@ -195,13 +199,13 @@ module Mongrel # test to see if this is a conditional request, and test if # the response would be identical to the last response same_response = case - when unmodified_since && !last_response_time = Time.httpdate(unmodified_since) rescue nil : false - when unmodified_since && last_response_time > Time.now : false - when unmodified_since && mtime > last_response_time : false - when none_match && none_match == '*' : false - when none_match && !none_match.strip.split(/\s*,\s*/).include?(etag) : false - else unmodified_since || none_match # validation successful if we get this far and at least one of the header exists - end + when unmodified_since && !last_response_time = Time.httpdate(unmodified_since) rescue nil : false + when unmodified_since && last_response_time > Time.now : false + when unmodified_since && mtime > last_response_time : false + when none_match && none_match == '*' : false + when none_match && !none_match.strip.split(/\s*,\s*/).include?(etag) : false + else unmodified_since || none_match # validation successful if we get this far and at least one of the header exists + end header = response.header header[Const::ETAG] = etag @@ -290,4 +294,104 @@ module Mongrel end end end + + + # Implements a few basic statistics for a particular URI. Register it anywhere + # you want in the request chain and it'll quickly gather some numbers for you + # to analyze. It is pretty fast, but don't put it out in production. + # + # You should pass the filter to StatusHandler as StatusHandler.new(:stats_filter => stats). + # This lets you then hit the status URI you want and get these stats from a browser. + # + # StatisticsFilter takes an option of :sample_rate. This is a number that's passed to + # rand and if that number gets hit then a sample is taken. This helps reduce the load + # and keeps the statistics valid (since sampling is a part of how they work). + # + # The exception to :sample_rate is that inter-request time is sampled on every request. + # If this wasn't done then it wouldn't be accurate as a measure of time between requests. + class StatisticsFilter < HttpHandler + attr_reader :stats + + def initialize(ops={}) + @sample_rate = ops[:sample_rate] || 300 + + @processors = Stats.new("processors") + @reqsize = Stats.new("request Kb") + @headcount = Stats.new("req param count") + @respsize = Stats.new("response Kb") + @interreq = Stats.new("inter-request time") + end + + + def process(request, response) + if rand(@sample_rate)+1 == @sample_rate + @processors.sample(listener.workers.list.length) + @headcount.sample(request.params.length) + @reqsize.sample(request.body.length / 1024.0) + @respsize.sample((response.body.length + response.header.out.length) / 1024.0) + end + @interreq.tick + end + + def dump + "#{@processors.to_s}\n#{@reqsize.to_s}\n#{@headcount.to_s}\n#{@respsize.to_s}\n#{@interreq.to_s}" + end + end + + + # The :stats_filter is basically any configured stats filter that you've added to this same + # URI. This lets the status handler print out statistics on how Mongrel is doing. + class StatusHandler < HttpHandler + def initialize(ops={}) + @stats = ops[:stats_filter] + end + + def table(title, rows) + results = "" + rows.each do |cols| + results << "" + cols.each {|col| results << "" } + results << "" + end + results + "
#{title}
#{col}
" + end + + def describe_listener + results = "" + results << "

Listener #{listener.host}:#{listener.port}

" + results << table("settings", [ + ["host",listener.host], + ["port",listener.port], + ["timeout",listener.timeout], + ["workers max",listener.num_processors], + ]) + + if @stats + results << "

Statistics

N means the number of samples, pay attention to MEAN, SD, MIN and MAX." + results << "

#{@stats.dump}
" + end + + results << "

Registered Handlers

" + uris = listener.classifier.handler_map + results << table("handlers", uris.map {|uri,handlers| + [uri, + "
" + 
+            handlers.map {|h| h.class.to_s }.join("\n") + 
+            "
" + ] + }) + + results + end + + def process(request, response) + response.start do |head,out| + out.write <<-END + Mongrel Server Status + #{describe_listener} + + END + end + end + end end diff --git a/lib/mongrel/stats.rb b/lib/mongrel/stats.rb index 38ed173..9e8bf7c 100644 --- a/lib/mongrel/stats.rb +++ b/lib/mongrel/stats.rb @@ -57,9 +57,15 @@ class Stats # Dump this Stats object with an optional additional message. def dump(msg = "", out=STDERR) - out.puts "[#{@name}] #{msg} : SUM=#@sum, SUMSQ=#@sumsq, N=#@n, MEAN=#{mean}, SD=#{sd}, MIN=#@min, MAX=#@max" + out.puts "#{msg}: #{self.to_s}" end + # Returns a common display (used by dump) + def to_s + "[#{@name}]: SUM=%0.4f, SUMSQ=%0.4f, N=%0.4f, MEAN=%0.4f, SD=%0.4f, MIN=%0.4f, MAX=%0.4f" % [@sum, @sumsq, @n, mean, sd, @min, @max] + end + + # Calculates and returns the mean for the data passed so far. def mean @sum / @n -- cgit v1.2.3-24-ge0c7