about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-05-23 09:49:28 +0000
committerzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-05-23 09:49:28 +0000
commit7022bfab09a2e036bf92f7892f1b4495d23f3426 (patch)
treea43cfe4d80c657762f714157638acbd1e3ebedd8
parente6e7c3a058e38db93330c68ebea9564c753075b6 (diff)
downloadunicorn-7022bfab09a2e036bf92f7892f1b4495d23f3426.tar.gz
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@209 19e92222-5c0b-0410-8929-a290d50e31e9
-rw-r--r--examples/simpletest.rb6
-rw-r--r--lib/mongrel.rb6
-rw-r--r--lib/mongrel/handlers.rb124
-rw-r--r--lib/mongrel/stats.rb8
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 << "<a href=\"#{base}/#{child}\">Up to parent..</a><br/>"
             else
-              out << "<a href=\"#{base}/#{child}\">#{child}</a><br/>"
+              out << "<a href=\"#{base}/#{child}/\">#{child}</a><br/>"
             end
           end
           out << "</body></html>"
@@ -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 = "<table border=\"1\"><tr><th colspan=\"#{rows[0].length}\">#{title}</th></tr>"
+      rows.each do |cols|
+        results << "<tr>"
+        cols.each {|col| results << "<td>#{col}</td>" }
+        results << "</tr>"
+      end
+      results + "</table>"
+    end
+
+    def describe_listener
+      results = ""
+        results << "<h1>Listener #{listener.host}:#{listener.port}</h1>"
+        results << table("settings", [
+                         ["host",listener.host],
+                         ["port",listener.port],
+                         ["timeout",listener.timeout],
+                         ["workers max",listener.num_processors],
+        ])
+
+        if @stats
+          results << "<h2>Statistics</h2><p>N means the number of samples, pay attention to MEAN, SD, MIN and MAX."
+          results << "<pre>#{@stats.dump}</pre>"
+        end
+
+        results << "<h2>Registered Handlers</h2>"
+        uris = listener.classifier.handler_map
+        results << table("handlers", uris.map {|uri,handlers|
+          [uri,
+            "<pre>" +
+            handlers.map {|h| h.class.to_s }.join("\n") +
+            "</pre>"
+          ]
+        })
+
+      results
+    end
+
+    def process(request, response)
+      response.start do |head,out|
+        out.write <<-END
+        <html><body><title>Mongrel Server Status</title>
+        #{describe_listener}
+        </body></html>
+        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