raindrops 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|