avoid reading errno repeatedly
[raindrops.git] / lib / raindrops / middleware.rb
blobea7f08adf71063164443c66b3347179d47e23dff
1 # -*- encoding: binary -*-
2 require 'raindrops'
4 # Raindrops::Middleware is Rack middleware that allows snapshotting
5 # current activity from an HTTP request.  For all operating systems,
6 # it returns at least the following fields:
8 # * calling - the number of application dispatchers on your machine
9 # * writing - the number of clients being written to on your machine
11 # Additional fields are available for \Linux users.
13 # It should be loaded at the top of Rack middleware stack before other
14 # middlewares for maximum accuracy.
16 # === Usage (Rainbows!/Unicorn preload_app=false)
18 # If you're using preload_app=false (the default) in your Rainbows!/Unicorn
19 # config file, you'll need to create the global Stats object before
20 # forking.
22 #    require 'raindrops'
23 #    $stats ||= Raindrops::Middleware::Stats.new
25 # In your Rack config.ru:
27 #    use Raindrops::Middleware, :stats => $stats
29 # === Usage (Rainbows!/Unicorn preload_app=true)
31 # If you're using preload_app=true in your Rainbows!/Unicorn
32 # config file, just add the middleware to your stack:
34 # In your Rack config.ru:
36 #    use Raindrops::Middleware
38 # === Linux-only extras!
40 # To get bound listener statistics under \Linux, you need to specify the
41 # listener names for your server.  You can even include listen sockets for
42 # *other* servers on the same machine.  This can be handy for monitoring
43 # your nginx proxy as well.
45 # In your Rack config.ru, just pass the :listeners argument as an array of
46 # strings (along with any other arguments).  You can specify any
47 # combination of TCP or Unix domain socket names:
49 #    use Raindrops::Middleware, :listeners => %w(0.0.0.0:80 /tmp/.sock)
51 # If you're running Unicorn 0.98.0 or later, you don't have to pass in
52 # the :listeners array, Raindrops will automatically detect the listeners
53 # used by Unicorn master process.  This does not detect listeners in
54 # different processes, of course.
56 # The response body includes the following stats for each listener
57 # (see also Raindrops::ListenStats):
59 # * active - total number of active clients on that listener
60 # * queued - total number of queued (pre-accept()) clients on that listener
62 # = Demo Server
64 # There is a server running this middleware (and Watcher) at
65 #  https://raindrops-demo.bogomips.org/_raindrops
67 # Also check out the Watcher demo at https://raindrops-demo.bogomips.org/
69 # The demo server is only limited to 30 users, so be sure not to abuse it
70 # by using the /tail/ endpoint too much.
72 class Raindrops::Middleware
73   attr_accessor :app, :stats, :path, :tcp, :unix # :nodoc:
75   # A Raindrops::Struct used to count the number of :calling and :writing
76   # clients.  This struct is intended to be shared across multiple processes
77   # and both counters are updated atomically.
78   #
79   # This is supported on all operating systems supported by Raindrops
80   Stats = Raindrops::Struct.new(:calling, :writing)
82   # :stopdoc:
83   require "raindrops/middleware/proxy"
84   # :startdoc:
86   # +app+ may be any Rack application, this middleware wraps it.
87   # +opts+ is a hash that understands the following members:
88   #
89   # * :stats - Raindrops::Middleware::Stats struct (default: Stats.new)
90   # * :path - HTTP endpoint used for reading the stats (default: "/_raindrops")
91   # * :listeners - array of host:port or socket paths (default: from Unicorn)
92   def initialize(app, opts = {})
93     @app = app
94     @stats = opts[:stats] || Stats.new
95     @path = opts[:path] || "/_raindrops"
96     tmp = opts[:listeners]
97     if tmp.nil? && defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
98       tmp = Unicorn.listener_names
99     end
100     @tcp = @unix = nil
102     if tmp
103       @tcp = tmp.grep(/\A.+:\d+\z/)
104       @unix = tmp.grep(%r{\A/})
105       @tcp = nil if @tcp.empty?
106       @unix = nil if @unix.empty?
107     end
108   end
110   # standard Rack endpoint
111   def call(env) # :nodoc:
112     env['PATH_INFO'] == @path and return stats_response
113     begin
114       @stats.incr_calling
116       status, headers, body = @app.call(env)
117       rv = [ status, headers, Proxy.new(body, @stats) ]
119       # the Rack server will start writing headers soon after this method
120       @stats.incr_writing
121       rv
122     ensure
123       @stats.decr_calling
124     end
125   end
127   def stats_response  # :nodoc:
128     body = "calling: #{@stats.calling}\n" \
129            "writing: #{@stats.writing}\n"
131     if defined?(Raindrops::Linux.tcp_listener_stats)
132       Raindrops::Linux.tcp_listener_stats(@tcp).each do |addr,stats|
133         body << "#{addr} active: #{stats.active}\n" \
134                 "#{addr} queued: #{stats.queued}\n"
135       end if @tcp
136       Raindrops::Linux.unix_listener_stats(@unix).each do |addr,stats|
137         body << "#{addr} active: #{stats.active}\n" \
138                 "#{addr} queued: #{stats.queued}\n"
139       end if @unix
140     end
142     headers = {
143       "Content-Type" => "text/plain",
144       "Content-Length" => body.size.to_s,
145     }
146     [ 200, headers, [ body ] ]
147   end