raindrops.git  about / heads / tags
real-time stats for preforking Rack servers
blob 25b5a1ef8e7f350cee52c86e519e8cec6c5dc4c4 5088 bytes (raw)
$ git show HEAD:lib/raindrops/middleware.rb	# shows this blob on the CLI

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
 
# -*- encoding: binary -*-
# frozen_string_literal: false
require 'raindrops'
require 'thread'

# Raindrops::Middleware is Rack middleware that allows snapshotting
# current activity from an HTTP request.  For all operating systems,
# it returns at least the following fields:
#
# * calling - the number of application dispatchers on your machine
# * writing - the number of clients being written to on your machine
#
# Additional fields are available for \Linux users.
#
# It should be loaded at the top of Rack middleware stack before other
# middlewares for maximum accuracy.
#
# === Usage (Rainbows!/Unicorn preload_app=false)
#
# If you're using preload_app=false (the default) in your Rainbows!/Unicorn
# config file, you'll need to create the global Stats object before
# forking.
#
#    require 'raindrops'
#    $stats ||= Raindrops::Middleware::Stats.new
#
# In your Rack config.ru:
#
#    use Raindrops::Middleware, :stats => $stats
#
# === Usage (Rainbows!/Unicorn preload_app=true)
#
# If you're using preload_app=true in your Rainbows!/Unicorn
# config file, just add the middleware to your stack:
#
# In your Rack config.ru:
#
#    use Raindrops::Middleware
#
# === Linux-only extras!
#
# To get bound listener statistics under \Linux, you need to specify the
# listener names for your server.  You can even include listen sockets for
# *other* servers on the same machine.  This can be handy for monitoring
# your nginx proxy as well.
#
# In your Rack config.ru, just pass the :listeners argument as an array of
# strings (along with any other arguments).  You can specify any
# combination of TCP or Unix domain socket names:
#
#    use Raindrops::Middleware, :listeners => %w(0.0.0.0:80 /tmp/.sock)
#
# If you're running Unicorn 0.98.0 or later, you don't have to pass in
# the :listeners array, Raindrops will automatically detect the listeners
# used by Unicorn master process.  This does not detect listeners in
# different processes, of course.
#
# The response body includes the following stats for each listener
# (see also Raindrops::ListenStats):
#
# * active - total number of active clients on that listener
# * queued - total number of queued (pre-accept()) clients on that listener
#
# = Demo Server
#
# There is a server running this middleware (and Watcher) at
#  https://yhbt.net/raindrops-demo/_raindrops
#
# Also check out the Watcher demo at https://yhbt.net/raindrops-demo/
#
# The demo server is only limited to 30 users, so be sure not to abuse it
# by using the /tail/ endpoint too much.
#
class Raindrops::Middleware
  attr_accessor :app, :stats, :path, :tcp, :unix # :nodoc:

  # A Raindrops::Struct used to count the number of :calling and :writing
  # clients.  This struct is intended to be shared across multiple processes
  # and both counters are updated atomically.
  #
  # This is supported on all operating systems supported by Raindrops
  Stats = Raindrops::Struct.new(:calling, :writing)

  # :stopdoc:
  require "raindrops/middleware/proxy"
  # :startdoc:

  # +app+ may be any Rack application, this middleware wraps it.
  # +opts+ is a hash that understands the following members:
  #
  # * :stats - Raindrops::Middleware::Stats struct (default: Stats.new)
  # * :path - HTTP endpoint used for reading the stats (default: "/_raindrops")
  # * :listeners - array of host:port or socket paths (default: from Unicorn)
  def initialize(app, opts = {})
    @app = app
    @stats = opts[:stats] || Stats.new
    @path = opts[:path] || "/_raindrops"
    @mtx = Mutex.new
    tmp = opts[:listeners]
    if tmp.nil? && defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
      tmp = Unicorn.listener_names
    end
    @nl_sock = @tcp = @unix = nil

    if tmp
      @tcp = tmp.grep(/\A.+:\d+\z/)
      @unix = tmp.grep(%r{\A/})
      @tcp = nil if @tcp.empty?
      @unix = nil if @unix.empty?
    end
  end

  # standard Rack endpoint
  def call(env) # :nodoc:
    env['PATH_INFO'] == @path and return stats_response
    begin
      @stats.incr_calling

      status, headers, body = @app.call(env)
      rv = [ status, headers, Proxy.new(body, @stats) ]

      # the Rack server will start writing headers soon after this method
      @stats.incr_writing
      rv
    ensure
      @stats.decr_calling
    end
  end

  def stats_response  # :nodoc:
    body = "calling: #{@stats.calling}\n" \
           "writing: #{@stats.writing}\n"

    if defined?(Raindrops::Linux.tcp_listener_stats)
      @mtx.synchronize do
        @nl_sock ||= Raindrops::InetDiagSocket.new
        Raindrops::Linux.tcp_listener_stats(@tcp, @nl_sock).each do |addr,stats|
          body << "#{addr} active: #{stats.active}\n" \
                  "#{addr} queued: #{stats.queued}\n"
        end
      end if @tcp
      Raindrops::Linux.unix_listener_stats(@unix).each do |addr,stats|
        body << "#{addr} active: #{stats.active}\n" \
                "#{addr} queued: #{stats.queued}\n"
      end if @unix
    end

    headers = {
      "Content-Type" => "text/plain",
      "Content-Length" => body.size.to_s,
    }
    [ 200, headers, [ body ] ]
  end
end

git clone https://yhbt.net/raindrops.git