raindrops 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +2 -1
- data/.gitignore +4 -0
- data/.wrongdoc.yml +4 -0
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +2 -196
- data/Gemfile +7 -0
- data/LICENSE +1 -1
- data/README +17 -47
- data/Rakefile +0 -104
- data/examples/linux-listener-stats.rb +123 -0
- data/examples/{config.ru → middleware.ru} +1 -1
- data/examples/watcher.ru +4 -0
- data/examples/watcher_demo.ru +13 -0
- data/examples/zbatery.conf.rb +13 -0
- data/ext/raindrops/extconf.rb +5 -0
- data/ext/raindrops/linux_inet_diag.c +449 -151
- data/ext/raindrops/linux_tcp_info.c +170 -0
- data/ext/raindrops/my_fileno.h +36 -0
- data/ext/raindrops/raindrops.c +232 -20
- data/lib/raindrops.rb +20 -7
- data/lib/raindrops/aggregate.rb +8 -0
- data/lib/raindrops/aggregate/last_data_recv.rb +86 -0
- data/lib/raindrops/aggregate/pmq.rb +239 -0
- data/lib/raindrops/last_data_recv.rb +100 -0
- data/lib/raindrops/linux.rb +26 -16
- data/lib/raindrops/middleware.rb +112 -41
- data/lib/raindrops/middleware/proxy.rb +34 -0
- data/lib/raindrops/struct.rb +15 -0
- data/lib/raindrops/watcher.rb +362 -0
- data/pkg.mk +171 -0
- data/raindrops.gemspec +10 -20
- data/test/ipv6_enabled.rb +10 -0
- data/test/rack_unicorn.rb +12 -0
- data/test/test_aggregate_pmq.rb +65 -0
- data/test/test_inet_diag_socket.rb +13 -0
- data/test/test_last_data_recv_unicorn.rb +69 -0
- data/test/test_linux.rb +55 -57
- data/test/test_linux_all_tcp_listen_stats.rb +66 -0
- data/test/test_linux_all_tcp_listen_stats_leak.rb +43 -0
- data/test/test_linux_ipv6.rb +158 -0
- data/test/test_linux_tcp_info.rb +61 -0
- data/test/test_middleware.rb +15 -2
- data/test/test_middleware_unicorn.rb +37 -0
- data/test/test_middleware_unicorn_ipv6.rb +37 -0
- data/test/test_raindrops.rb +65 -1
- data/test/test_raindrops_gc.rb +23 -1
- data/test/test_watcher.rb +85 -0
- metadata +69 -22
- data/examples/linux-tcp-listener-stats.rb +0 -44
data/lib/raindrops/linux.rb
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
# For reporting TCP ListenStats, users of older \Linux kernels need to ensure
|
4
|
+
# that the the "inet_diag" and "tcp_diag" kernel modules are loaded as they do
|
5
|
+
# not autoload correctly. The inet_diag facilities of \Raindrops is useful
|
6
|
+
# for periodic snapshot reporting of listen queue sizes.
|
7
|
+
#
|
8
|
+
# Instead of snapshotting, Raindrops::Aggregate::LastDataRecv may be used
|
9
|
+
# to aggregate statistics from +all+ accepted sockets as they arrive
|
10
|
+
# based on the +last_data_recv+ field in Raindrops::TCP_Info
|
11
|
+
module Raindrops::Linux
|
4
12
|
|
5
13
|
# The standard proc path for active UNIX domain sockets, feel free to call
|
6
14
|
# String#replace on this if your /proc is mounted in a non-standard location
|
@@ -11,7 +19,7 @@ module Linux
|
|
11
19
|
# Get ListenStats from an array of +paths+
|
12
20
|
#
|
13
21
|
# Socket state mapping from integer => symbol, based on socket_state
|
14
|
-
# enum from include/linux/net.h in the Linux kernel:
|
22
|
+
# enum from include/linux/net.h in the \Linux kernel:
|
15
23
|
# typedef enum {
|
16
24
|
# SS_FREE = 0, /* not allocated */
|
17
25
|
# SS_UNCONNECTED, /* unconnected to any socket */
|
@@ -26,20 +34,24 @@ module Linux
|
|
26
34
|
# counterpart due to the latter being able to use inet_diag via netlink.
|
27
35
|
# This parses /proc/net/unix as there is no other (known) way
|
28
36
|
# to expose Unix domain socket statistics over netlink.
|
29
|
-
def unix_listener_stats(paths)
|
30
|
-
rv = Hash.new { |h,k| h[k.freeze] = ListenStats.new(0, 0) }
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
37
|
+
def unix_listener_stats(paths = nil)
|
38
|
+
rv = Hash.new { |h,k| h[k.freeze] = Raindrops::ListenStats.new(0, 0) }
|
39
|
+
if nil == paths
|
40
|
+
paths = [ '[^\n]+' ]
|
41
|
+
else
|
42
|
+
paths = paths.map do |path|
|
43
|
+
path = path.dup
|
44
|
+
path.force_encoding(Encoding::BINARY) if defined?(Encoding)
|
45
|
+
rv[path]
|
46
|
+
Regexp.escape(path)
|
47
|
+
end
|
36
48
|
end
|
37
|
-
paths =
|
49
|
+
paths = /^\w+: \d+ \d+ 00000000 \d+ (\d+)\s+\d+ (#{paths.join('|')})$/n
|
38
50
|
|
39
51
|
# no point in pread since we can't stat for size on this file
|
40
52
|
File.read(*PROC_NET_UNIX_ARGS).scan(paths) do |s|
|
41
|
-
path = s
|
42
|
-
case s.
|
53
|
+
path = s[-1]
|
54
|
+
case s[0].to_i
|
43
55
|
when 2 then rv[path].queued += 1
|
44
56
|
when 3 then rv[path].active += 1
|
45
57
|
end
|
@@ -47,8 +59,6 @@ module Linux
|
|
47
59
|
|
48
60
|
rv
|
49
61
|
end
|
50
|
-
|
51
62
|
module_function :unix_listener_stats
|
52
63
|
|
53
|
-
end # Linux
|
54
|
-
end # Raindrops
|
64
|
+
end # Raindrops::Linux
|
data/lib/raindrops/middleware.rb
CHANGED
@@ -1,71 +1,144 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
2
|
require 'raindrops'
|
3
3
|
|
4
|
-
# Raindrops
|
5
|
-
#
|
6
|
-
|
7
|
-
|
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:
|
7
|
+
#
|
8
|
+
# * calling - the number of application dispatchers on your machine
|
9
|
+
# * writing - the number of clients being written to on your machine
|
10
|
+
#
|
11
|
+
# Additional fields are available for \Linux users.
|
12
|
+
#
|
13
|
+
# It should be loaded at the top of Rack middleware stack before other
|
14
|
+
# middlewares for maximum accuracy.
|
15
|
+
#
|
16
|
+
# === Usage (Rainbows!/Unicorn preload_app=false)
|
17
|
+
#
|
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.
|
21
|
+
#
|
22
|
+
# require 'raindrops'
|
23
|
+
# $stats ||= Raindrops::Middleware::Stats.new
|
24
|
+
#
|
25
|
+
# In your Rack config.ru:
|
26
|
+
#
|
27
|
+
# use Raindrops::Middleware, :stats => $stats
|
28
|
+
#
|
29
|
+
# === Usage (Rainbows!/Unicorn preload_app=true)
|
30
|
+
#
|
31
|
+
# If you're using preload_app=true in your Rainbows!/Unicorn
|
32
|
+
# config file, just add the middleware to your stack:
|
33
|
+
#
|
34
|
+
# In your Rack config.ru:
|
35
|
+
#
|
36
|
+
# use Raindrops::Middleware
|
37
|
+
#
|
38
|
+
# === Linux-only extras!
|
39
|
+
#
|
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.
|
44
|
+
#
|
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:
|
48
|
+
#
|
49
|
+
# use Raindrops::Middleware, :listeners => %w(0.0.0.0:80 /tmp/.sock)
|
50
|
+
#
|
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.
|
55
|
+
#
|
56
|
+
# The response body includes the following stats for each listener
|
57
|
+
# (see also Raindrops::ListenStats):
|
58
|
+
#
|
59
|
+
# * active - total number of active clients on that listener
|
60
|
+
# * queued - total number of queued (pre-accept()) clients on that listener
|
61
|
+
#
|
62
|
+
# = Demo Server
|
63
|
+
#
|
64
|
+
# There is a server running this middleware (and Watcher) at
|
65
|
+
# http://raindrops-demo.bogomips.org/_raindrops
|
66
|
+
#
|
67
|
+
# Also check out the Watcher demo at http://raindrops-demo.bogomips.org/
|
68
|
+
#
|
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.
|
71
|
+
#
|
72
|
+
class Raindrops::Middleware
|
73
|
+
attr_accessor :app, :stats, :path, :tcp, :unix # :nodoc:
|
74
|
+
|
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
|
+
class Stats < Raindrops::Struct.new(:calling, :writing)
|
81
|
+
end
|
8
82
|
|
9
83
|
# :stopdoc:
|
10
|
-
Stats = Raindrops::Struct.new(:calling, :writing)
|
11
84
|
PATH_INFO = "PATH_INFO"
|
85
|
+
require "raindrops/middleware/proxy"
|
12
86
|
# :startdoc:
|
13
87
|
|
88
|
+
# +app+ may be any Rack application, this middleware wraps it.
|
89
|
+
# +opts+ is a hash that understands the following members:
|
90
|
+
#
|
91
|
+
# * :stats - Raindrops::Middleware::Stats struct (default: Stats.new)
|
92
|
+
# * :path - HTTP endpoint used for reading the stats (default: "/_raindrops")
|
93
|
+
# * :listeners - array of host:port or socket paths (default: from Unicorn)
|
14
94
|
def initialize(app, opts = {})
|
15
|
-
|
95
|
+
@app = app
|
96
|
+
@stats = opts[:stats] || Stats.new
|
97
|
+
@path = opts[:path] || "/_raindrops"
|
16
98
|
tmp = opts[:listeners]
|
17
99
|
if tmp.nil? && defined?(Unicorn) && Unicorn.respond_to?(:listener_names)
|
18
100
|
tmp = Unicorn.listener_names
|
19
101
|
end
|
102
|
+
@tcp = @unix = nil
|
20
103
|
|
21
104
|
if tmp
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
105
|
+
@tcp = tmp.grep(/\A.+:\d+\z/)
|
106
|
+
@unix = tmp.grep(%r{\A/})
|
107
|
+
@tcp = nil if @tcp.empty?
|
108
|
+
@unix = nil if @unix.empty?
|
26
109
|
end
|
27
110
|
end
|
28
111
|
|
29
112
|
# standard Rack endpoint
|
30
|
-
def call(env)
|
31
|
-
env[PATH_INFO] == path
|
32
|
-
|
113
|
+
def call(env) # :nodoc:
|
114
|
+
env[PATH_INFO] == @path and return stats_response
|
115
|
+
begin
|
116
|
+
@stats.incr_calling
|
33
117
|
|
34
|
-
|
35
|
-
|
36
|
-
status, headers, self.app = app.call(env)
|
118
|
+
status, headers, body = @app.call(env)
|
119
|
+
rv = [ status, headers, Proxy.new(body, @stats) ]
|
37
120
|
|
38
|
-
|
39
|
-
|
40
|
-
|
121
|
+
# the Rack server will start writing headers soon after this method
|
122
|
+
@stats.incr_writing
|
123
|
+
rv
|
41
124
|
ensure
|
42
|
-
stats.decr_calling
|
43
|
-
|
44
|
-
|
45
|
-
# yield to the Rack server here for writing
|
46
|
-
def each(&block)
|
47
|
-
app.each(&block)
|
48
|
-
end
|
49
|
-
|
50
|
-
# the Rack server should call this after #each (usually ensure-d)
|
51
|
-
def close
|
52
|
-
stats.decr_writing
|
53
|
-
app.close if app.respond_to?(:close)
|
125
|
+
@stats.decr_calling
|
126
|
+
end
|
54
127
|
end
|
55
128
|
|
56
|
-
def stats_response
|
57
|
-
body = "calling: #{stats.calling}\n" \
|
58
|
-
"writing: #{stats.writing}\n"
|
129
|
+
def stats_response # :nodoc:
|
130
|
+
body = "calling: #{@stats.calling}\n" \
|
131
|
+
"writing: #{@stats.writing}\n"
|
59
132
|
|
60
|
-
if defined?(Linux)
|
61
|
-
Linux.tcp_listener_stats(tcp).each do |addr,stats|
|
133
|
+
if defined?(Raindrops::Linux)
|
134
|
+
Raindrops::Linux.tcp_listener_stats(@tcp).each do |addr,stats|
|
62
135
|
body << "#{addr} active: #{stats.active}\n" \
|
63
136
|
"#{addr} queued: #{stats.queued}\n"
|
64
|
-
end if tcp
|
65
|
-
Linux.unix_listener_stats(unix).each do |addr,stats|
|
137
|
+
end if @tcp
|
138
|
+
Raindrops::Linux.unix_listener_stats(@unix).each do |addr,stats|
|
66
139
|
body << "#{addr} active: #{stats.active}\n" \
|
67
140
|
"#{addr} queued: #{stats.queued}\n"
|
68
|
-
end if unix
|
141
|
+
end if @unix
|
69
142
|
end
|
70
143
|
|
71
144
|
headers = {
|
@@ -74,6 +147,4 @@ class Middleware < ::Struct.new(:app, :stats, :path, :tcp, :unix)
|
|
74
147
|
}
|
75
148
|
[ 200, headers, [ body ] ]
|
76
149
|
end
|
77
|
-
|
78
|
-
end
|
79
150
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :stopdoc:
|
3
|
+
# This class is by Raindrops::Middleware to proxy application response
|
4
|
+
# bodies. There should be no need to use it directly.
|
5
|
+
class Raindrops::Middleware::Proxy
|
6
|
+
def initialize(body, stats)
|
7
|
+
@body, @stats = body, stats
|
8
|
+
end
|
9
|
+
|
10
|
+
# yield to the Rack server here for writing
|
11
|
+
def each
|
12
|
+
@body.each { |x| yield x }
|
13
|
+
end
|
14
|
+
|
15
|
+
# the Rack server should call this after #each (usually ensure-d)
|
16
|
+
def close
|
17
|
+
@stats.decr_writing
|
18
|
+
@body.close if @body.respond_to?(:close)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Some Rack servers can optimize response processing if it responds
|
22
|
+
# to +to_path+ via the sendfile(2) system call, we proxy +to_path+
|
23
|
+
# to the underlying body if possible.
|
24
|
+
def to_path
|
25
|
+
@body.to_path
|
26
|
+
end
|
27
|
+
|
28
|
+
# Rack servers use +respond_to?+ to check for the presence of +close+
|
29
|
+
# and +to_path+ methods.
|
30
|
+
def respond_to?(m)
|
31
|
+
m = m.to_sym
|
32
|
+
:close == m || @body.respond_to?(m)
|
33
|
+
end
|
34
|
+
end
|
data/lib/raindrops/struct.rb
CHANGED
@@ -1,7 +1,22 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
2
|
|
3
|
+
# This is a wrapper around Raindrops objects much like the core Ruby
|
4
|
+
# \Struct can be seen as a wrapper around the core \Array class.
|
5
|
+
# It's usage is similar to the core \Struct class, except its fields
|
6
|
+
# may only be used to house unsigned long integers.
|
7
|
+
#
|
8
|
+
# class Foo < Raindrops::Struct.new(:readers, :writers)
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# foo = Foo.new 0, 0
|
12
|
+
#
|
13
|
+
# foo.incr_writers -> 1
|
14
|
+
# foo.incr_readers -> 1
|
15
|
+
#
|
3
16
|
class Raindrops::Struct
|
4
17
|
|
18
|
+
# returns a new class derived from Raindrops::Struct and supporting
|
19
|
+
# the given +members+ as fields, just like \Struct.new in core Ruby.
|
5
20
|
def self.new(*members)
|
6
21
|
members = members.map { |x| x.to_sym }.freeze
|
7
22
|
str = <<EOS
|
@@ -0,0 +1,362 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require "thread"
|
3
|
+
require "time"
|
4
|
+
require "socket"
|
5
|
+
require "rack"
|
6
|
+
require "aggregate"
|
7
|
+
|
8
|
+
# Raindrops::Watcher is a stand-alone Rack application for watching
|
9
|
+
# any number of TCP and UNIX listeners (all of them by default).
|
10
|
+
#
|
11
|
+
# It depends on the {Aggregate RubyGem}[http://rubygems.org/gems/aggregate]
|
12
|
+
#
|
13
|
+
# In your Rack config.ru:
|
14
|
+
#
|
15
|
+
# run Raindrops::Watcher(options = {})
|
16
|
+
#
|
17
|
+
# It takes the following options hash:
|
18
|
+
#
|
19
|
+
# - :listeners - an array of listener names, (e.g. %w(0.0.0.0:80 /tmp/sock))
|
20
|
+
# - :delay - interval between stats updates in seconds (default: 1)
|
21
|
+
#
|
22
|
+
# Raindrops::Watcher is compatible any thread-safe/thread-aware Rack
|
23
|
+
# middleware. It does not work well with multi-process web servers
|
24
|
+
# but can be used to monitor them. It consumes minimal resources
|
25
|
+
# with the default :delay.
|
26
|
+
#
|
27
|
+
# == HTTP endpoints
|
28
|
+
#
|
29
|
+
# === GET /
|
30
|
+
#
|
31
|
+
# Returns an HTML summary listing of all listen interfaces watched on
|
32
|
+
#
|
33
|
+
# === GET /active/$LISTENER.txt
|
34
|
+
#
|
35
|
+
# Returns a plain text summary + histogram with X-* HTTP headers for
|
36
|
+
# active connections.
|
37
|
+
#
|
38
|
+
# e.g.: curl http://example.com/active/0.0.0.0%3A80.txt
|
39
|
+
#
|
40
|
+
# === GET /active/$LISTENER.html
|
41
|
+
#
|
42
|
+
# Returns an HTML summary + histogram with X-* HTTP headers for
|
43
|
+
# active connections.
|
44
|
+
#
|
45
|
+
# e.g.: curl http://example.com/active/0.0.0.0%3A80.html
|
46
|
+
#
|
47
|
+
# === GET /queued/$LISTENER.txt
|
48
|
+
#
|
49
|
+
# Returns a plain text summary + histogram with X-* HTTP headers for
|
50
|
+
# queued connections.
|
51
|
+
#
|
52
|
+
# e.g.: curl http://example.com/queued/0.0.0.0%3A80.txt
|
53
|
+
#
|
54
|
+
# === GET /queued/$LISTENER.html
|
55
|
+
#
|
56
|
+
# Returns an HTML summary + histogram with X-* HTTP headers for
|
57
|
+
# queued connections.
|
58
|
+
#
|
59
|
+
# e.g.: curl http://example.com/queued/0.0.0.0%3A80.html
|
60
|
+
#
|
61
|
+
# === GET /tail/$LISTENER.txt?active_min=1&queued_min=1
|
62
|
+
#
|
63
|
+
# Streams chunked a response to the client.
|
64
|
+
# Interval is the preconfigured +:delay+ of the application (default 1 second)
|
65
|
+
#
|
66
|
+
# The response is plain text in the following format:
|
67
|
+
#
|
68
|
+
# ISO8601_TIMESTAMP LISTENER_NAME ACTIVE_COUNT QUEUED_COUNT LINEFEED
|
69
|
+
#
|
70
|
+
# Query parameters:
|
71
|
+
#
|
72
|
+
# - active_min - do not stream a line until this active count is reached
|
73
|
+
# - queued_min - do not stream a line until this queued count is reached
|
74
|
+
#
|
75
|
+
# == Response headers (mostly the same as Raindrops::LastDataRecv)
|
76
|
+
#
|
77
|
+
# - X-Count - number of requests received
|
78
|
+
# - X-Last-Reset - date since the last reset
|
79
|
+
#
|
80
|
+
# The following headers are only present if X-Count is greater than one.
|
81
|
+
#
|
82
|
+
# - X-Min - lowest last_data_recv time recorded (in milliseconds)
|
83
|
+
# - X-Max - highest last_data_recv time recorded (in milliseconds)
|
84
|
+
# - X-Mean - mean last_data_recv time recorded (rounded, in milliseconds)
|
85
|
+
# - X-Std-Dev - standard deviation of last_data_recv times
|
86
|
+
# - X-Outliers-Low - number of low outliers (hopefully many!)
|
87
|
+
# - X-Outliers-High - number of high outliers (hopefully zero!)
|
88
|
+
#
|
89
|
+
# = Demo Server
|
90
|
+
#
|
91
|
+
# There is a server running this app at http://raindrops-demo.bogomips.org/
|
92
|
+
# The Raindrops::Middleware demo is also accessible at
|
93
|
+
# http://raindrops-demo.bogomips.org/_raindrops
|
94
|
+
#
|
95
|
+
# The demo server is only limited to 30 users, so be sure not to abuse it
|
96
|
+
# by using the /tail/ endpoint too much.
|
97
|
+
class Raindrops::Watcher
|
98
|
+
# :stopdoc:
|
99
|
+
attr_reader :snapshot
|
100
|
+
include Rack::Utils
|
101
|
+
include Raindrops::Linux
|
102
|
+
|
103
|
+
def initialize(opts = {})
|
104
|
+
@tcp_listeners = @unix_listeners = nil
|
105
|
+
if l = opts[:listeners]
|
106
|
+
tcp, unix = [], []
|
107
|
+
Array(l).each { |addr| (addr =~ %r{\A/} ? unix : tcp) << addr }
|
108
|
+
unless tcp.empty? && unix.empty?
|
109
|
+
@tcp_listeners = tcp
|
110
|
+
@unix_listeners = unix
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
agg_class = opts[:agg_class] || Aggregate
|
115
|
+
start = Time.now.utc
|
116
|
+
@active = Hash.new { |h,k| h[k] = agg_class.new }
|
117
|
+
@queued = Hash.new { |h,k| h[k] = agg_class.new }
|
118
|
+
@resets = Hash.new { |h,k| h[k] = start }
|
119
|
+
@snapshot = [ start, {} ]
|
120
|
+
@delay = opts[:delay] || 1
|
121
|
+
@lock = Mutex.new
|
122
|
+
@start = Mutex.new
|
123
|
+
@cond = ConditionVariable.new
|
124
|
+
@thr = nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def hostname
|
128
|
+
Socket.gethostname
|
129
|
+
end
|
130
|
+
|
131
|
+
# rack endpoint
|
132
|
+
def call(env)
|
133
|
+
@start.synchronize { @thr ||= aggregator_thread(env["rack.logger"]) }
|
134
|
+
case env["REQUEST_METHOD"]
|
135
|
+
when "HEAD", "GET"
|
136
|
+
get env
|
137
|
+
when "POST"
|
138
|
+
post env
|
139
|
+
else
|
140
|
+
Rack::Response.new(["Method Not Allowed"], 405).finish
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def aggregator_thread(logger) # :nodoc:
|
145
|
+
@socket = sock = Raindrops::InetDiagSocket.new
|
146
|
+
thr = Thread.new do
|
147
|
+
begin
|
148
|
+
combined = tcp_listener_stats(@tcp_listeners, sock)
|
149
|
+
combined.merge!(unix_listener_stats(@unix_listeners))
|
150
|
+
@lock.synchronize do
|
151
|
+
combined.each do |addr,stats|
|
152
|
+
@active[addr] << stats.active
|
153
|
+
@queued[addr] << stats.queued
|
154
|
+
end
|
155
|
+
@snapshot = [ Time.now.utc, combined ]
|
156
|
+
@cond.broadcast
|
157
|
+
end
|
158
|
+
rescue => e
|
159
|
+
logger.error "#{e.class} #{e.inspect}"
|
160
|
+
end while sleep(@delay) && @socket
|
161
|
+
sock.close
|
162
|
+
end
|
163
|
+
wait_snapshot
|
164
|
+
thr
|
165
|
+
end
|
166
|
+
|
167
|
+
def active_stats(addr) # :nodoc:
|
168
|
+
@lock.synchronize do
|
169
|
+
tmp = @active[addr] or return
|
170
|
+
[ @resets[addr], tmp.dup ]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def queued_stats(addr) # :nodoc:
|
175
|
+
@lock.synchronize do
|
176
|
+
tmp = @queued[addr] or return
|
177
|
+
[ @resets[addr], tmp.dup ]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def wait_snapshot
|
182
|
+
@lock.synchronize do
|
183
|
+
@cond.wait @lock
|
184
|
+
@snapshot
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def agg_to_hash(reset_at, agg)
|
189
|
+
{
|
190
|
+
"X-Count" => agg.count.to_s,
|
191
|
+
"X-Min" => agg.min.to_s,
|
192
|
+
"X-Max" => agg.max.to_s,
|
193
|
+
"X-Mean" => agg.mean.to_s,
|
194
|
+
"X-Std-Dev" => agg.std_dev.to_s,
|
195
|
+
"X-Outliers-Low" => agg.outliers_low.to_s,
|
196
|
+
"X-Outliers-High" => agg.outliers_high.to_s,
|
197
|
+
"X-Last-Reset" => reset_at.httpdate,
|
198
|
+
}
|
199
|
+
end
|
200
|
+
|
201
|
+
def histogram_txt(agg)
|
202
|
+
reset_at, agg = *agg
|
203
|
+
headers = agg_to_hash(reset_at, agg)
|
204
|
+
body = agg.to_s
|
205
|
+
headers["Content-Type"] = "text/plain"
|
206
|
+
headers["Content-Length"] = bytesize(body).to_s
|
207
|
+
[ 200, headers, [ body ] ]
|
208
|
+
end
|
209
|
+
|
210
|
+
def histogram_html(agg, addr)
|
211
|
+
reset_at, agg = *agg
|
212
|
+
headers = agg_to_hash(reset_at, agg)
|
213
|
+
body = "<html>" \
|
214
|
+
"<head><title>#{hostname} - #{escape_html addr}</title></head>" \
|
215
|
+
"<body><table>" <<
|
216
|
+
headers.map { |k,v|
|
217
|
+
"<tr><td>#{k.gsub(/^X-/, '')}</td><td>#{v}</td></tr>"
|
218
|
+
}.join << "</table><pre>#{escape_html agg}</pre>" \
|
219
|
+
"<form action='/reset/#{escape addr}' method='post'>" \
|
220
|
+
"<input type='submit' name='x' value='reset' /></form>" \
|
221
|
+
"</body>"
|
222
|
+
headers["Content-Type"] = "text/html"
|
223
|
+
headers["Content-Length"] = bytesize(body).to_s
|
224
|
+
[ 200, headers, [ body ] ]
|
225
|
+
end
|
226
|
+
|
227
|
+
def get(env)
|
228
|
+
case env["PATH_INFO"]
|
229
|
+
when "/"
|
230
|
+
index
|
231
|
+
when %r{\A/active/(.+)\.txt\z}
|
232
|
+
histogram_txt(active_stats(unescape($1)))
|
233
|
+
when %r{\A/active/(.+)\.html\z}
|
234
|
+
addr = unescape $1
|
235
|
+
histogram_html(active_stats(addr), addr)
|
236
|
+
when %r{\A/queued/(.+)\.txt\z}
|
237
|
+
histogram_txt(queued_stats(unescape($1)))
|
238
|
+
when %r{\A/queued/(.+)\.html\z}
|
239
|
+
addr = unescape $1
|
240
|
+
histogram_html(queued_stats(addr), addr)
|
241
|
+
when %r{\A/tail/(.+)\.txt\z}
|
242
|
+
tail(unescape($1), env)
|
243
|
+
else
|
244
|
+
not_found
|
245
|
+
end
|
246
|
+
rescue Errno::EDOM
|
247
|
+
raise if defined?(retried)
|
248
|
+
retried = true
|
249
|
+
wait_snapshot
|
250
|
+
retry
|
251
|
+
end
|
252
|
+
|
253
|
+
def not_found
|
254
|
+
Rack::Response.new(["Not Found"], 404).finish
|
255
|
+
end
|
256
|
+
|
257
|
+
def post(env)
|
258
|
+
case env["PATH_INFO"]
|
259
|
+
when %r{\A/reset/(.+)\z}
|
260
|
+
reset!(env, unescape($1))
|
261
|
+
else
|
262
|
+
Rack::Response.new(["Not Found"], 404).finish
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def reset!(env, addr)
|
267
|
+
@lock.synchronize do
|
268
|
+
@active.include?(addr) or return not_found
|
269
|
+
@active.delete addr
|
270
|
+
@queued.delete addr
|
271
|
+
@resets[addr] = Time.now.utc
|
272
|
+
@cond.wait @lock
|
273
|
+
end
|
274
|
+
req = Rack::Request.new(env)
|
275
|
+
res = Rack::Response.new
|
276
|
+
url = req.referer || "#{req.host_with_port}/"
|
277
|
+
res.redirect(url)
|
278
|
+
res.content_type.replace "text/plain"
|
279
|
+
res.write "Redirecting to #{url}"
|
280
|
+
res.finish
|
281
|
+
end
|
282
|
+
|
283
|
+
def index
|
284
|
+
updated_at, all = snapshot
|
285
|
+
headers = {
|
286
|
+
"Content-Type" => "text/html",
|
287
|
+
"Last-Modified" => updated_at.httpdate,
|
288
|
+
}
|
289
|
+
body = "<html><head>" \
|
290
|
+
"<title>#{hostname} - all interfaces</title>" \
|
291
|
+
"</head><body><h3>Updated at #{updated_at.iso8601}</h3>" \
|
292
|
+
"<table><tr>" \
|
293
|
+
"<th>address</th><th>active</th><th>queued</th><th>reset</th>" \
|
294
|
+
"</tr>" <<
|
295
|
+
all.map do |addr,stats|
|
296
|
+
e_addr = escape addr
|
297
|
+
"<tr>" \
|
298
|
+
"<td><a href='/tail/#{e_addr}.txt'>#{escape_html addr}</a></td>" \
|
299
|
+
"<td><a href='/active/#{e_addr}.html'>#{stats.active}</a></td>" \
|
300
|
+
"<td><a href='/queued/#{e_addr}.html'>#{stats.queued}</a></td>" \
|
301
|
+
"<td><form action='/reset/#{e_addr}' method='post'>" \
|
302
|
+
"<input type='submit' name='x' value='x' /></form></td>" \
|
303
|
+
"</tr>" \
|
304
|
+
end.join << "</table></body></html>"
|
305
|
+
headers["Content-Length"] = bytesize(body).to_s
|
306
|
+
[ 200, headers, [ body ] ]
|
307
|
+
end
|
308
|
+
|
309
|
+
def tail(addr, env)
|
310
|
+
Tailer.new(self, addr, env).finish
|
311
|
+
end
|
312
|
+
# :startdoc:
|
313
|
+
|
314
|
+
# This is the response body returned for "/tail/$ADDRESS.txt". This
|
315
|
+
# must use a multi-threaded Rack server with streaming response support.
|
316
|
+
# It is an internal class and not expected to be used directly
|
317
|
+
class Tailer
|
318
|
+
def initialize(rdmon, addr, env) # :nodoc:
|
319
|
+
@rdmon = rdmon
|
320
|
+
@addr = addr
|
321
|
+
q = Rack::Utils.parse_query env["QUERY_STRING"]
|
322
|
+
@active_min = q["active_min"].to_i
|
323
|
+
@queued_min = q["queued_min"].to_i
|
324
|
+
len = Rack::Utils.bytesize(addr)
|
325
|
+
len = 35 if len > 35
|
326
|
+
@fmt = "%20s % #{len}s % 10u % 10u\n"
|
327
|
+
case env["HTTP_VERSION"]
|
328
|
+
when "HTTP/1.0", nil
|
329
|
+
@chunk = false
|
330
|
+
else
|
331
|
+
@chunk = true
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def finish
|
336
|
+
headers = { "Content-Type" => "text/plain" }
|
337
|
+
headers["Transfer-Encoding"] = "chunked" if @chunk
|
338
|
+
[ 200, headers, self ]
|
339
|
+
end
|
340
|
+
|
341
|
+
# called by the Rack server
|
342
|
+
def each # :nodoc:
|
343
|
+
begin
|
344
|
+
time, all = @rdmon.wait_snapshot
|
345
|
+
stats = all[@addr] or next
|
346
|
+
stats.queued >= @queued_min or next
|
347
|
+
stats.active >= @active_min or next
|
348
|
+
body = sprintf(@fmt, time.iso8601, @addr, stats.active, stats.queued)
|
349
|
+
body = "#{body.size.to_s(16)}\r\n#{body}\r\n" if @chunk
|
350
|
+
yield body
|
351
|
+
end while true
|
352
|
+
yield "0\r\n\r\n" if @chunk
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# shuts down the background thread
|
357
|
+
def shutdown
|
358
|
+
@socket = nil
|
359
|
+
@thr.join if @thr
|
360
|
+
@thr = nil
|
361
|
+
end
|
362
|
+
end
|