beaver 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +26 -28
- data/bin/beaver +54 -23
- data/lib/beaver.rb +2 -0
- data/lib/beaver/beaver.rb +34 -30
- data/lib/beaver/dam.rb +117 -74
- data/lib/beaver/parsers/http.rb +109 -0
- data/lib/beaver/parsers/rails.rb +66 -89
- data/lib/beaver/request.rb +30 -121
- data/lib/beaver/utils.rb +59 -23
- data/lib/beaver/version.rb +4 -0
- metadata +5 -3
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
== Beaver, chewing through logs to make something useful
|
2
2
|
|
3
|
-
Beaver is a light DSL and command line utility for parsing Rails production logs back into usable data. It answers questions like:
|
3
|
+
Beaver is a light DSL and command line utility for parsing HTTP and Rails production logs back into usable data. It answers questions like:
|
4
4
|
|
5
5
|
* How many failed logins have there been today? Were they for the same user? From the same IP?
|
6
6
|
* How many 500, 404, etc. errors yesterday? On what pages?
|
@@ -9,14 +9,14 @@ Beaver is a light DSL and command line utility for parsing Rails production logs
|
|
9
9
|
* Rails 3.2 tagged logging is cool, but what's a good way to review them?
|
10
10
|
|
11
11
|
Read the full documentation at {jordanhollinger.com/docs/beaver/}[http://jordanhollinger.com/docs/beaver/].
|
12
|
-
For a full list of matchers available to "hit", see the Beaver::Dam class.
|
13
|
-
of the "hits" array in a "dam" block, see the Beaver::Request
|
12
|
+
For a full list of matchers available to "hit", see the Beaver::Dam class.
|
13
|
+
For a full list of methods available inside a "hit" block, or to members of the "hits" array in a "dam" block, see the Beaver::Request, Beaver::Parsers::Rails, and Beaver::Parsers::HTTP classes.
|
14
14
|
|
15
15
|
== Installation
|
16
16
|
[sudo] gem install beaver
|
17
17
|
|
18
|
-
==
|
19
|
-
hit :
|
18
|
+
== Use beaver with a DSL file
|
19
|
+
hit :failed_logins, :method => :post, :path => '/login', :status => 401
|
20
20
|
|
21
21
|
hit :new_widgets, :path => '/widgets', :method => :post, :status => 302 do
|
22
22
|
puts "A Widget named #{params[:widget][:name]} was created!"
|
@@ -31,9 +31,9 @@ of the "hits" array in a "dam" block, see the Beaver::Request class.
|
|
31
31
|
puts "user 1 was tagged at #{path} - other tags were: #{tags.join(', ')}"
|
32
32
|
end
|
33
33
|
|
34
|
-
hit :
|
34
|
+
hit :errors, :status => (500..503)
|
35
35
|
|
36
|
-
dam :
|
36
|
+
dam :failed_logins do
|
37
37
|
puts "Failed logins:"
|
38
38
|
hits.group_by { |h| h.params[:username] } do |user, fails|
|
39
39
|
puts " #{user}"
|
@@ -43,32 +43,26 @@ of the "hits" array in a "dam" block, see the Beaver::Request class.
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
dam :
|
47
|
-
puts
|
48
|
-
hits.group_by(&:status) do |status, errors|
|
49
|
-
puts " There were #{errors.size} #{status} errors"
|
50
|
-
end
|
46
|
+
dam :errors do
|
47
|
+
puts tablize(' | ') { |hit| [hit.status, hit.path, hit.ip, hit.time] }
|
51
48
|
end
|
52
49
|
|
53
50
|
Run ''beaver'' from the command line, passing in your beaver file and some logs:
|
54
51
|
|
55
52
|
beaver my_beaver_file.rb /var/www/rails-app/log/production.log*
|
56
53
|
|
57
|
-
== Use
|
54
|
+
== Use beaver as a library
|
58
55
|
require 'rubygems'
|
59
56
|
require 'beaver'
|
60
57
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# Parse the logs, but only include requests from yesterday
|
66
|
-
Beaver.stream logs, :on => Date.today-1 do
|
67
|
-
hit :failed_login, :method => :post, :path => '/login', :status => 401
|
68
|
-
...
|
58
|
+
beaver = Beaver.new('/path/to/httpd/access_logs*')
|
59
|
+
beaver.hit :failed_logins, :method => :post, :path => '/login', :status => 401
|
60
|
+
beaver.dam :failed_logins do
|
61
|
+
puts "#{hits.size} failed logins!"
|
69
62
|
end
|
63
|
+
beaver.stream
|
70
64
|
|
71
|
-
==
|
65
|
+
== Use beaver as part of the *nix toolchain
|
72
66
|
|
73
67
|
It's difficult to grep through a multi-line log format like Rails' and output each matching multi-line event (though I hear Google is working on a 'Context-Free Grep', which may help solve that). Until then, for Rails anyway, beaver is happy to step in.
|
74
68
|
|
@@ -78,7 +72,7 @@ Or format the output to a single line:
|
|
78
72
|
|
79
73
|
beaver --controller=widgets /var/www/rails-app/log/production.log --print "%{ip} hit %{action} using %{method}"
|
80
74
|
|
81
|
-
Also accepts log content from pipes and stdin
|
75
|
+
Also accepts log content from pipes and stdin. Use it to filter log files:
|
82
76
|
|
83
77
|
cat /var/www/rails-app/log/production.log* | beaver --action=edit
|
84
78
|
|
@@ -86,6 +80,10 @@ Also accepts log content from pipes and stdin (might be a bit faster):
|
|
86
80
|
|
87
81
|
beaver --action=edit --stdin
|
88
82
|
|
83
|
+
Or for dead-simple real-time event monitoring:
|
84
|
+
|
85
|
+
tail -f /var/log/nginx/access_log | beaver www.rb
|
86
|
+
|
89
87
|
See all options with 'beaver --help'.
|
90
88
|
|
91
89
|
== Example use with Logwatch
|
@@ -111,7 +109,7 @@ HTTP status codes. For example, your failed logins are probably returning
|
|
111
109
|
|
112
110
|
A detailed description of each status code and when to use it can be found at {www.w3.org}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html].
|
113
111
|
|
114
|
-
== Complex tag querying
|
112
|
+
== Complex Rails tag querying
|
115
113
|
Beaver supports complex Rails Tagged Logger tag quering, in both DSL and command-line modes.
|
116
114
|
|
117
115
|
=== DSL
|
@@ -128,8 +126,8 @@ Beaver supports complex Rails Tagged Logger tag quering, in both DSL and command
|
|
128
126
|
hit :tagged_with_any, :tagged => [['foo'], ['bar']]
|
129
127
|
|
130
128
|
# Matches any request tagged with ("foo" AND "bar") OR ("bar" AND "baz") OR "yay"
|
131
|
-
hit :tagged_and_or_and_or, :tagged => [['foo', '
|
132
|
-
# Could also be written ['foo,
|
129
|
+
hit :tagged_and_or_and_or, :tagged => [['foo', 'bar'], ['bar', 'baz'], ['yay']]
|
130
|
+
# Could also be written ['foo, bar', 'bar, baz', 'yay']
|
133
131
|
|
134
132
|
=== Command-line
|
135
133
|
|
@@ -142,8 +140,8 @@ Beaver supports complex Rails Tagged Logger tag quering, in both DSL and command
|
|
142
140
|
# Matches any request tagged with "foo" OR "bar"
|
143
141
|
beaver --tagged foo --tagged bar production.log
|
144
142
|
|
145
|
-
# Matches any request tagged with ("foo" AND "
|
146
|
-
beaver --tagged foo,
|
143
|
+
# Matches any request tagged with ("foo" AND "bar") OR ("bar" AND "baz") OR "yay"
|
144
|
+
beaver --tagged foo,bar --tagged bar,baz --tagged yay production.log
|
147
145
|
|
148
146
|
== License
|
149
147
|
Copyright 2011 Jordan Hollinger
|
data/bin/beaver
CHANGED
@@ -6,27 +6,34 @@ require 'beaver'
|
|
6
6
|
|
7
7
|
options, matchers = {}, {}
|
8
8
|
o = OptionParser.new do |opts|
|
9
|
-
opts.banner = 'Usage: beaver [options] [dsl.rb...] [/
|
10
|
-
opts.on('--path PATH', 'URL path, string or regex') { |path| matchers[:path] = Regexp.new(path, Regexp::IGNORECASE) }
|
11
|
-
opts.on('--method METHOD', 'HTTP request method(s), e.g. get or post,put') { |methods| matchers[:method] = methods.split(',').map { |m| m.downcase.to_sym } }
|
12
|
-
opts.on('--controller CONTROLLER', 'Rails
|
13
|
-
opts.on('--action
|
14
|
-
opts.on('--status STATUS', 'HTTP status(es), e.g. 200 or 500..503') { |s| matchers[:status] = s =~ /(\.+)/ ? Range.new(*s.split($1).map(&:to_i)) : s.to_i }
|
15
|
-
opts.on('--ip IP', 'IP address, string or regex') { |ip| matchers[:ip] = Regexp.new(ip) }
|
16
|
-
opts.on('--tagged TAGS', 'Comma-separated Rails Tagged Logger tags') { |tags| (matchers[:tagged] ||= []) << tags }
|
17
|
-
opts.on('--params PARAMS', 'Request parameters string (a Ruby Hash), string or regex') { |params| matchers[:params_str] = Regexp.new(params, Regexp::IGNORECASE) }
|
18
|
-
opts.on('--format FORMAT', 'Response format(s), e.g. html or json,xml') { |f| matchers[:format] = f.split(',').map(&:to_sym) }
|
19
|
-
opts.on('--longer-than MS', 'Minimum response time in ms') { |ms| matchers[:longer_than] = ms.to_i }
|
20
|
-
opts.on('--shorter-than MS', 'Maximum response time in ms') { |ms| matchers[:shorter_than] = ms.to_i }
|
21
|
-
opts.on('--on DATE', 'Only include log entries from the given date (yyyy-mm-dd or -n days)') { |d| matchers[:on] = Beaver::Utils.parse_date(d) }
|
22
|
-
opts.on('--after DATE', 'Only include log entries from after the given date (yyyy-mm-dd or -n days)') { |d| matchers[:after] = Beaver::Utils.parse_date(d) }
|
23
|
-
opts.on('--before DATE', 'Only include log entries from before the given date (yyyy-mm-dd or -n days)') { |d| matchers[:before] = Beaver::Utils.parse_date(d) }
|
24
|
-
opts.on('--today', 'Alias to --on=-0') { matchers[:on] = Date.today }
|
25
|
-
opts.on('--yesterday', 'Alias to --on=-1') { matchers[:on] = Date.today-1 }
|
26
|
-
opts.on('--
|
9
|
+
opts.banner = 'Usage: beaver [options] [dsl.rb...] [/path/to/log...]'
|
10
|
+
opts.on('--path PATH', 'Rails HTTP URL path, string or regex') { |path| matchers[:path] = Regexp.new(path, Regexp::IGNORECASE) }
|
11
|
+
opts.on('--method METHOD', 'Rails HTTP HTTP request method(s), e.g. get or post,put') { |methods| matchers[:method] = methods.split(',').map { |m| m.downcase.to_sym } }
|
12
|
+
opts.on('--controller CONTROLLER', 'Rails Controller name or regex') { |c| matchers[:controller] = Regexp.new(c, Regexp::IGNORECASE) }
|
13
|
+
opts.on('--action ACTION', 'Rails Action name or regex') { |a| matchers[:action] = Regexp.new(a, Regexp::IGNORECASE) }
|
14
|
+
opts.on('--status STATUS', 'Rails HTTP HTTP status(es), e.g. 200 or 500..503') { |s| matchers[:status] = s =~ /(\.+)/ ? Range.new(*s.split($1).map(&:to_i)) : s.to_i }
|
15
|
+
opts.on('--ip IP', 'Rails HTTP IP address, string or regex') { |ip| matchers[:ip] = Regexp.new(ip) }
|
16
|
+
opts.on('--tagged TAGS', 'Rails Comma-separated Rails Tagged Logger tags') { |tags| (matchers[:tagged] ||= []) << tags }
|
17
|
+
opts.on('--params PARAMS', 'Rails HTTP Request parameters string (a Ruby Hash), string or regex') { |params| matchers[:params_str] = Regexp.new(params, Regexp::IGNORECASE) }
|
18
|
+
opts.on('--format FORMAT', 'Rails Response format(s), e.g. html or json,xml') { |f| matchers[:format] = f.split(',').map(&:to_sym) }
|
19
|
+
opts.on('--longer-than MS', 'Rails Minimum response time in ms') { |ms| matchers[:longer_than] = ms.to_i }
|
20
|
+
opts.on('--shorter-than MS', 'Rails Maximum response time in ms') { |ms| matchers[:shorter_than] = ms.to_i }
|
21
|
+
opts.on('--on DATE', 'Rails HTTP Only include log entries from the given date (yyyy-mm-dd or -n days)') { |d| matchers[:on] = Beaver::Utils.parse_date(d) }
|
22
|
+
opts.on('--after DATE', 'Rails HTTP Only include log entries from after the given date (yyyy-mm-dd or -n days)') { |d| matchers[:after] = Beaver::Utils.parse_date(d) }
|
23
|
+
opts.on('--before DATE', 'Rails HTTP Only include log entries from before the given date (yyyy-mm-dd or -n days)') { |d| matchers[:before] = Beaver::Utils.parse_date(d) }
|
24
|
+
opts.on('--today', 'Rails HTTP Alias to --on=-0') { matchers[:on] = Date.today }
|
25
|
+
opts.on('--yesterday', 'Rails HTTP Alias to --on=-1') { matchers[:on] = Date.today-1 }
|
26
|
+
opts.on('--size BYTES', ' HTTP Responses of n bytes') { |bytes| matchers[:size] = bytes.to_i }
|
27
|
+
opts.on('--smaller-than BYTES', ' HTTP Responses smaller than n bytes') { |bytes| matchers[:smaller_than] = bytes.to_i }
|
28
|
+
opts.on('--larger-than BYTES', ' HTTP Responses larger than n bytes') { |bytes| matchers[:larger_than] = bytes.to_i }
|
29
|
+
opts.on('--referer URL', ' HTTP Referer URL, string or regex') { |url| matchers[:referer] = Regexp.new(url, Regexp::IGNORECASE) }
|
30
|
+
opts.on('--referrer URL', ' HTTP Referer URL, string or regex') { |url| matchers[:referrer] = Regexp.new(url, Regexp::IGNORECASE) }
|
31
|
+
opts.on('--user-agent STRING', ' HTTP User Agent, string or regex') { |ua| matchers[:user_agent] = Regexp.new(ua, Regexp::IGNORECASE) }
|
32
|
+
opts.on('--regex REGEX', 'Rails HTTP A regex string to be matched against the entire request') { |r| matchers[:match] = Regexp.new(r, Regexp::IGNORECASE) }
|
27
33
|
opts.on('--print FORMAT', 'Formatted request string, e.g. "%{ip} went to %{path} passing %{params[:email]}"') { |hit| options[:print] = hit }
|
34
|
+
opts.on('--tablize', 'Print output from --print in a table') { options[:tablize] = true }
|
28
35
|
opts.on('--stdin', 'Read log content from stdin (for typing/pasting)') { options[:tty] = true }
|
29
|
-
opts.on('-v', '--version', '
|
36
|
+
opts.on('-v', '--version', 'Print version and exit') { puts "beaver #{Beaver::VERSION}"; exit }
|
30
37
|
end
|
31
38
|
o.parse!
|
32
39
|
|
@@ -40,12 +47,36 @@ Beaver.stream *args do
|
|
40
47
|
tty! if options[:tty]
|
41
48
|
# Filter the logs through the DSL files
|
42
49
|
if beaver_files.any?
|
43
|
-
beaver_files.map
|
44
|
-
beaver_files.each { |b| eval b }
|
50
|
+
beaver_files.map { |b| File.open(b, &:read) }.each &method(:eval)
|
45
51
|
# Filter the logs through the CLI options
|
46
52
|
else
|
47
|
-
|
48
|
-
|
53
|
+
# Format each entry
|
54
|
+
if options[:print]
|
55
|
+
options[:print].gsub!(/%\{/, '#{')
|
56
|
+
arg_pattern = /#\{[^\}]+\}/
|
57
|
+
|
58
|
+
# Make each column the same length
|
59
|
+
if options[:tablize]
|
60
|
+
hit :cli, matchers
|
61
|
+
dam :cli do
|
62
|
+
text = options[:print].split(arg_pattern) << ''
|
63
|
+
tablize(false) do |hit|
|
64
|
+
options[:print].gsub(arg_pattern).map { |match| hit.instance_eval(%Q|"#{match}"|) rescue '?' }
|
65
|
+
end.each do |cols|
|
66
|
+
puts text.map { |t| "#{t}#{cols.shift}" }.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
# Just format and print
|
70
|
+
else
|
71
|
+
hit :cli, matchers do
|
72
|
+
puts options[:print].gsub(arg_pattern) { |match| self.instance_eval(%Q|"#{match}"|) rescue '?' }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
# Print the entirety of each entry
|
76
|
+
else
|
77
|
+
hit :cli, matchers do
|
78
|
+
puts to_s
|
79
|
+
end
|
49
80
|
end
|
50
81
|
end
|
51
82
|
end
|
data/lib/beaver.rb
CHANGED
@@ -4,9 +4,11 @@ rescue LoadError
|
|
4
4
|
$stderr.puts "Zlib not available; compressed log files will be skipped."
|
5
5
|
end
|
6
6
|
|
7
|
+
require 'beaver/version'
|
7
8
|
require 'beaver/utils'
|
8
9
|
require 'beaver/beaver'
|
9
10
|
require 'beaver/dam'
|
10
11
|
require 'beaver/request'
|
11
12
|
|
12
13
|
require 'beaver/parsers/rails'
|
14
|
+
require 'beaver/parsers/http'
|
data/lib/beaver/beaver.rb
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
# Not specifically a performance analyzer (like https://github.com/wvanbergen/request-log-analyzer/wiki)
|
2
2
|
# Rather, a DSL for finding out how people are using your Rails app (which could include performance).
|
3
|
+
# Can also be used to parse/analyze HTTP access logs (Apache, Nginx, etc.)
|
3
4
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
5
|
+
# Beaver.stream('/path/to/log/files') do
|
6
|
+
# hit :error, :status => (400..505) do
|
7
|
+
# puts "#{status} on #{path} at #{time} from #{ip} with #{params_str}"
|
8
|
+
# end
|
9
|
+
# end
|
9
10
|
#
|
10
11
|
module Beaver
|
11
|
-
MAJOR_VERSION, MINOR_VERSION, TINY_VERSION, PRE_VERSION = 1, 2, 0, nil
|
12
|
-
VERSION = [MAJOR_VERSION, MINOR_VERSION, TINY_VERSION, PRE_VERSION].compact.join '.'
|
13
|
-
|
14
12
|
# Creates a new Beaver and immediately filters the log files. This should scale well
|
15
13
|
# for even very large logs, at least when compared to Beaver#parse.
|
16
14
|
def self.stream(*args, &blk)
|
@@ -25,7 +23,7 @@ module Beaver
|
|
25
23
|
Beaver.new(*args, &blk).parse.filter
|
26
24
|
end
|
27
25
|
|
28
|
-
# Alias to
|
26
|
+
# Alias to Beaver::Beaver.new
|
29
27
|
def self.new(*args, &blk)
|
30
28
|
Beaver.new(*args, &blk)
|
31
29
|
end
|
@@ -33,18 +31,18 @@ module Beaver
|
|
33
31
|
# The Beaver class, which keeps track of the files you're parsing, the Beaver::Dam objects you've defined,
|
34
32
|
# and parses and filters the matching Beaver::Request objects.
|
35
33
|
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
34
|
+
# beaver = Beaver.new do
|
35
|
+
# hit :help, :path => '/help' do
|
36
|
+
# puts "#{ip} needed help"
|
37
|
+
# end
|
38
|
+
# end
|
41
39
|
#
|
42
|
-
#
|
43
|
-
#
|
40
|
+
# # Method 1 - logs will be parsed and filtered line-by-line, then discarded. Performance should be constant regardless of the number of logs.
|
41
|
+
# beaver.stream
|
44
42
|
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
43
|
+
# # Method 2 - all of the logs will be parsed at once and stored in "beaver.requests". Then each request will be filtered.
|
44
|
+
# # This does not scale as well, but is necessary *if you want to hang onto the parsed requests*.
|
45
|
+
# beaver.parse.filter
|
48
46
|
#
|
49
47
|
class Beaver
|
50
48
|
# The log files to parse
|
@@ -72,15 +70,17 @@ module Beaver
|
|
72
70
|
# Creates a new Dam and appends it to this Beaver. name should be a unique symbol.
|
73
71
|
# See Beaver::Dam for available options.
|
74
72
|
def hit(dam_name, matchers={}, &callback)
|
75
|
-
|
73
|
+
$stderr.puts "WARNING Overwriting existing hit '#{dam_name}'" if @dams.has_key? dam_name
|
76
74
|
matchers = @global_matchers.merge matchers
|
77
75
|
@dams[dam_name] = Dam.new(dam_name, matchers, &callback)
|
78
76
|
end
|
79
77
|
|
80
78
|
# Define a sumarry for a Dam
|
81
|
-
def dam(name, &callback)
|
82
|
-
|
79
|
+
def dam(name, hit_options={}, &callback)
|
80
|
+
$stderr.puts "WARNING Overwriting existing dam '#{name}'" if @sums.has_key? name
|
83
81
|
@sums[name] = callback
|
82
|
+
# Optionally create a new hit
|
83
|
+
hit(name, hit_options) if @dams[name].nil?
|
84
84
|
end
|
85
85
|
|
86
86
|
# Parses the logs and immediately filters them through the dams. Requests are not retained,
|
@@ -140,7 +140,7 @@ module Beaver
|
|
140
140
|
# Run the callback on each dam matching request. Optionally pass a block, which will be passed back matching dams.
|
141
141
|
def hit_dams(request, &blk)
|
142
142
|
for dam in @dams.values
|
143
|
-
if dam
|
143
|
+
if dam === request
|
144
144
|
catch :skip do
|
145
145
|
request.instance_eval(&dam.callback) if dam.callback
|
146
146
|
blk.call(dam) if block_given?
|
@@ -158,7 +158,7 @@ module Beaver
|
|
158
158
|
blk.call(@dams[dam_name]) if block_given?
|
159
159
|
end
|
160
160
|
else
|
161
|
-
|
161
|
+
$stderr.puts "WARNING You have defined a dam for '#{dam_name}', but there is no hit defined for '#{dam_name}'"
|
162
162
|
end
|
163
163
|
end
|
164
164
|
end
|
@@ -168,12 +168,12 @@ module Beaver
|
|
168
168
|
request = nil
|
169
169
|
# Parses a line into part of a request
|
170
170
|
parse_it = lambda { |line|
|
171
|
-
request
|
172
|
-
|
173
|
-
|
174
|
-
|
171
|
+
if request
|
172
|
+
request << line
|
173
|
+
else
|
174
|
+
request = Request.for(line)
|
175
|
+
next if request.nil?
|
175
176
|
end
|
176
|
-
request << line
|
177
177
|
if request.completed?
|
178
178
|
blk.call(request)
|
179
179
|
request = nil
|
@@ -184,7 +184,11 @@ module Beaver
|
|
184
184
|
if @tty
|
185
185
|
STDIN.read.each_line &parse_it # Read entire stream, then parse it - looks much better to the user
|
186
186
|
elsif !STDIN.tty?
|
187
|
-
|
187
|
+
begin
|
188
|
+
STDIN.each_line &parse_it
|
189
|
+
rescue Interrupt
|
190
|
+
$stderr.puts 'Closing input stream; parsing input...'
|
191
|
+
end
|
188
192
|
end if @stdin
|
189
193
|
request = nil
|
190
194
|
|
data/lib/beaver/dam.rb
CHANGED
@@ -2,43 +2,59 @@ module Beaver
|
|
2
2
|
# A Dam "traps" certain Requests, using one or more matching options. A request must meet *all* of the
|
3
3
|
# matching options specified.
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# The last argument may be a block, which will be called everytime this Dam is hit.
|
6
|
+
# The block will be run in the context of the Request object. This can be used for
|
7
|
+
# further checks or for reporting purposes.
|
8
|
+
#
|
9
|
+
# hit :reads, :method => :get do
|
10
|
+
# puts "#{ip} read from #{path} at #{time}"
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Available matchers:
|
6
14
|
#
|
7
|
-
# :path
|
15
|
+
# :path Rails HTTP String for exact match, or Regex
|
8
16
|
#
|
9
|
-
# :method
|
17
|
+
# :method Rails HTTP Symbol of :get, :post, :put or :delete, or any array of any (reads the magic _method field if present)
|
10
18
|
#
|
11
|
-
# :controller
|
19
|
+
# :controller Rails String like 'FooController' or a Regex like /foo/i
|
12
20
|
#
|
13
|
-
# :action
|
21
|
+
# :action Rails String like 'index' or a Regex like /(index)|(show)/
|
14
22
|
#
|
15
|
-
# :status
|
23
|
+
# :status Rails HTTP Fixnum like 404 or a Range like (500..503)
|
16
24
|
#
|
17
|
-
# :ip
|
25
|
+
# :ip Rails HTTP String for exact match, or Regex
|
18
26
|
#
|
19
|
-
# :format
|
27
|
+
# :format Rails Symbol or array of symbols of response formats like :html, :json
|
20
28
|
#
|
21
|
-
# :longer_than
|
29
|
+
# :longer_than Rails Fixnum n. Matches any request which took longer than n milliseconds to complete.
|
22
30
|
#
|
23
|
-
# :shorter_than
|
31
|
+
# :shorter_than Rails Fixnum n. Matches any request which took less than n milliseconds to complete.
|
24
32
|
#
|
25
|
-
# :before
|
33
|
+
# :before Rails HTTP Date or Time for which the request must have ocurred before
|
26
34
|
#
|
27
|
-
# :after
|
35
|
+
# :after Rails HTTP Date or Time for which the request must have ocurred after
|
28
36
|
#
|
29
|
-
# :on
|
37
|
+
# :on Rails HTTP Date - the request must have ocurred on this date
|
30
38
|
#
|
31
|
-
# :params_str
|
39
|
+
# :params_str Rails HTTP Regular expressing matching the Parameters string
|
32
40
|
#
|
33
|
-
# :params
|
41
|
+
# :params Rails Hash of Symbol=>String/Regexp pairs: {:username => 'bob', :email => /@gmail\.com$/}. All must match.
|
34
42
|
#
|
35
|
-
# :tagged
|
43
|
+
# :tagged Rails Comma-separated String or Array of Rails Tagged Logger tags. If you specify multiple tags, a request must have *all* of them.
|
36
44
|
#
|
37
|
-
# :
|
45
|
+
# :size HTTP Fixnum matching the response size in bytes
|
38
46
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
47
|
+
# :smaller_than HTTP Fixnum matching the maximum response size in bytes
|
48
|
+
#
|
49
|
+
# :larger_than HTTP Fixnum matching the minimum response size in bytes
|
50
|
+
#
|
51
|
+
# :referer HTTP String for exact match, or Regex
|
52
|
+
#
|
53
|
+
# :referrer HTTP Alias to :referer
|
54
|
+
#
|
55
|
+
# :user_agent HTTP String for exact match, or Regex
|
56
|
+
#
|
57
|
+
# :match Rails HTTP A "catch-all" Regex that will be matched against the entire request string
|
42
58
|
class Dam
|
43
59
|
# The symbol name of this Beaver::Dam
|
44
60
|
attr_reader :name
|
@@ -56,15 +72,30 @@ module Beaver
|
|
56
72
|
build matchers
|
57
73
|
end
|
58
74
|
|
75
|
+
# Transforms arrays of values into rows with equally padded columns.
|
76
|
+
# Useful for generating table-like formatting of hits.
|
77
|
+
# If delim is falsey, the columns will not be joined, but returned as arrays.
|
78
|
+
#
|
79
|
+
# dam :errors do
|
80
|
+
# puts tablize { |hit| [hit.ip, hit.path, hit.status] }
|
81
|
+
# end
|
82
|
+
def tablize(delim=' ', &block)
|
83
|
+
rows = Utils.tablize(hits.map &block)
|
84
|
+
rows.map! { |cols| cols.join(delim) } if delim
|
85
|
+
rows
|
86
|
+
end
|
87
|
+
|
59
88
|
# Returns an array of IP address that hit this Dam.
|
60
89
|
def ips
|
61
90
|
@ips ||= @hits.map(&:ip).uniq
|
62
91
|
end
|
63
92
|
|
64
93
|
# Returns true if the given Request hits this Dam, false if not.
|
65
|
-
def
|
94
|
+
def ===(request)
|
66
95
|
return false if request.final?
|
67
96
|
return false unless @match_path.nil? or @match_path === request.path
|
97
|
+
return false unless @match_referer.nil? or @match_referer === request.referer
|
98
|
+
return false unless @match_user_agent.nil? or @match_user_agent === request.user_agent
|
68
99
|
return false unless @match_longer.nil? or @match_longer < request.ms
|
69
100
|
return false unless @match_shorter.nil? or @match_shorter > request.ms
|
70
101
|
return false unless @match_method_s.nil? or @match_method_s == request.method
|
@@ -75,153 +106,165 @@ module Beaver
|
|
75
106
|
return false unless @match_ip.nil? or @match_ip === request.ip
|
76
107
|
return false unless @match_format_s.nil? or @match_format_s == request.format
|
77
108
|
return false unless @match_format_a.nil? or @match_format_a.include? request.format
|
78
|
-
return false unless @
|
79
|
-
return false unless @
|
109
|
+
return false unless @match_before_time.nil? or @match_before_time > request.time
|
110
|
+
return false unless @match_before_date.nil? or @match_before_date > request.date
|
111
|
+
return false unless @match_after_time.nil? or @match_after_time < request.time
|
112
|
+
return false unless @match_after_date.nil? or @match_after_date < request.date
|
80
113
|
return false unless @match_on.nil? or @match_on == request.date
|
81
114
|
return false unless @match_params_str.nil? or @match_params_str =~ request.params_str
|
115
|
+
return false unless @match_size.nil? or @match_size == request.size
|
116
|
+
return false unless @match_size_lt.nil? or request.size < @match_size_lt
|
117
|
+
return false unless @match_size_gt.nil? or request.size > @match_size_gt
|
82
118
|
return false unless @match_r.nil? or @match_r =~ request.to_s
|
83
119
|
if @deep_tag_match
|
84
|
-
return false unless @match_tags.nil? or (@match_tags.any? and request.tags_str and deep_matching_tags(@match_tags, request.tags))
|
120
|
+
return false unless @match_tags.nil? or (@match_tags.any? and request.tags_str and Utils.deep_matching_tags(@match_tags, request.tags))
|
85
121
|
else
|
86
122
|
return false unless @match_tags.nil? or (@match_tags.any? and request.tags_str and (@match_tags - request.tags).empty?)
|
87
123
|
end
|
88
|
-
return false unless @match_params.nil? or matching_hashes?(@match_params, request.params)
|
124
|
+
return false unless @match_params.nil? or Utils.matching_hashes?(@match_params, request.params)
|
89
125
|
return true
|
90
126
|
end
|
91
127
|
|
92
|
-
private
|
93
|
-
|
94
|
-
# Matches tags recursively
|
95
|
-
def deep_matching_tags(matchers, tags)
|
96
|
-
all_tags_matched = nil
|
97
|
-
any_arrays_matched = false
|
98
|
-
for m in matchers
|
99
|
-
if m.is_a? Array
|
100
|
-
matched = deep_matching_tags m, tags
|
101
|
-
any_arrays_matched = true if matched
|
102
|
-
else
|
103
|
-
matched = tags.include? m
|
104
|
-
all_tags_matched = (matched && all_tags_matched != false) ? true : false
|
105
|
-
end
|
106
|
-
end
|
107
|
-
return (all_tags_matched or any_arrays_matched)
|
108
|
-
end
|
109
|
-
|
110
|
-
# Recursively compares to Hashes. If all of Hash A is in Hash B, they match.
|
111
|
-
def matching_hashes?(a,b)
|
112
|
-
intersecting_keys = a.keys & b.keys
|
113
|
-
if intersecting_keys.any?
|
114
|
-
a_values = a.values_at(*intersecting_keys)
|
115
|
-
b_values = b.values_at(*intersecting_keys)
|
116
|
-
indicies = (0..b_values.size-1)
|
117
|
-
indicies.all? do |i|
|
118
|
-
if a_values[i].is_a? String
|
119
|
-
a_values[i] == b_values[i]
|
120
|
-
elsif a_values[i].is_a?(Regexp) and b_values[i].is_a?(String)
|
121
|
-
a_values[i] =~ b_values[i]
|
122
|
-
elsif a_values[i].is_a?(Hash) and b_values[i].is_a?(Hash)
|
123
|
-
matching_hashes? a_values[i], b_values[i]
|
124
|
-
else
|
125
|
-
false
|
126
|
-
end
|
127
|
-
end
|
128
|
-
else
|
129
|
-
false
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
public
|
134
|
-
|
135
128
|
# Parses and checks the validity of the matching options passed to the Dam.
|
129
|
+
# XXX Yikes this is long and ugly...
|
136
130
|
def build(matchers)
|
131
|
+
# Match path
|
137
132
|
if matchers[:path].respond_to? :===
|
138
133
|
@match_path = matchers[:path]
|
139
134
|
else
|
140
135
|
raise ArgumentError, "Path must respond to the '===' method; try a String or a Regexp (it's a #{matchers[:path].class.name})"
|
141
136
|
end if matchers[:path]
|
142
137
|
|
138
|
+
# Match HTTP referer
|
139
|
+
referer = matchers[:referer] || matchers[:referrer]
|
140
|
+
if referer.respond_to? :===
|
141
|
+
@match_referer = referer
|
142
|
+
else
|
143
|
+
raise ArgumentError, "Referrer must respond to the '===' method; try a String or a Regexp (it's a #{referer.class.name})"
|
144
|
+
end if referer
|
145
|
+
|
146
|
+
# Match request method
|
143
147
|
case
|
144
148
|
when matchers[:method].is_a?(Symbol) then @match_method_s = matchers[:method].to_s.downcase.to_sym
|
145
149
|
when matchers[:method].is_a?(Array) then @match_method_a = matchers[:method].map { |m| m.to_s.downcase.to_sym }
|
146
150
|
else raise ArgumentError, "Method must be a Symbol or an Array (it's a #{matchers[:method].class.name})"
|
147
151
|
end if matchers[:method]
|
148
152
|
|
153
|
+
# Match Rails controller
|
149
154
|
if matchers[:controller].respond_to? :===
|
150
155
|
@match_controller = matchers[:controller]
|
151
156
|
else
|
152
157
|
raise ArgumentError, "Controller must respond to the '===' method; try a String or a Regexp (it's a #{matchers[:controller].class.name})"
|
153
158
|
end if matchers[:controller]
|
154
159
|
|
160
|
+
# Match Rails controller action
|
155
161
|
if matchers[:action].respond_to? :=== or matchers[:action].is_a? Symbol
|
156
162
|
@match_action = matchers[:action]
|
157
163
|
else
|
158
164
|
raise ArgumentError, "Action must respond to the '===' method or be a Symbol; try a String, Symbol or a Regexp (it's a #{matchers[:action].class.name})"
|
159
165
|
end if matchers[:action]
|
160
166
|
|
167
|
+
# Match response status
|
161
168
|
case matchers[:status].class.name
|
162
169
|
when Fixnum.name, Range.name then @match_status = matchers[:status]
|
163
170
|
else raise ArgumentError, "Status must be a Fixnum or a Range (it's a #{matchers[:status].class.name})"
|
164
171
|
end if matchers[:status]
|
165
172
|
|
173
|
+
# Match request IP
|
166
174
|
if matchers[:ip].respond_to? :===
|
167
175
|
@match_ip = matchers[:ip]
|
168
176
|
else
|
169
177
|
raise ArgumentError, "IP must respond to the '===' method; try a String or a Regexp (it's a #{matchers[:ip].class.name})"
|
170
178
|
end if matchers[:ip]
|
171
179
|
|
180
|
+
# Match Rails' response format
|
172
181
|
case
|
173
182
|
when matchers[:format].is_a?(Symbol) then @match_format_s = matchers[:format].to_s.downcase.to_sym
|
174
183
|
when matchers[:format].is_a?(Array) then @match_format_a = matchers[:format].map { |f| f.to_s.downcase.to_sym }
|
175
184
|
else raise ArgumentError, "Format must be a Symbol or an Array (it's a #{matchers[:format].class.name})"
|
176
185
|
end if matchers[:format]
|
177
186
|
|
187
|
+
# Match Rails' response time (at least)
|
178
188
|
case matchers[:longer_than].class.name
|
179
189
|
when Fixnum.name then @match_longer = matchers[:longer_than]
|
180
190
|
else raise ArgumentError, "longer_than must be a Fixnum (it's a #{matchers[:longer_than].class.name})"
|
181
191
|
end if matchers[:longer_than]
|
182
192
|
|
193
|
+
# Match Rails' response time (at most)
|
183
194
|
case matchers[:shorter_than].class.name
|
184
195
|
when Fixnum.name then @match_shorter = matchers[:shorter_than]
|
185
196
|
else raise ArgumentError, "shorter_than must be a Fixnum (it's a #{matchers[:shorter_than].class.name})"
|
186
197
|
end if matchers[:shorter_than]
|
187
198
|
|
188
|
-
|
189
|
-
|
199
|
+
# Match HTTP response size
|
200
|
+
case matchers[:size].class.name
|
201
|
+
when Fixnum.name then @match_size = matchers[:size]
|
202
|
+
else raise ArgumentError, "size must be a Fixnum (it's a #{matchers[:size].class.name})"
|
203
|
+
end if matchers[:size]
|
204
|
+
|
205
|
+
# Match HTTP response size (at most)
|
206
|
+
case matchers[:smaller_than].class.name
|
207
|
+
when Fixnum.name then @match_size_lt = matchers[:smaller_than]
|
208
|
+
else raise ArgumentError, "size must be a Fixnum (it's a #{matchers[:smaller_than].class.name})"
|
209
|
+
end if matchers[:smaller_than]
|
210
|
+
|
211
|
+
# Match HTTP response size (at least)
|
212
|
+
case matchers[:larger_than].class.name
|
213
|
+
when Fixnum.name then @match_size_gt = matchers[:larger_than]
|
214
|
+
else raise ArgumentError, "size must be a Fixnum (it's a #{matchers[:larger_than].class.name})"
|
215
|
+
end if matchers[:larger_than]
|
216
|
+
|
217
|
+
# Match before a request date
|
218
|
+
if matchers[:before].is_a? Time
|
219
|
+
@match_before_time = matchers[:before]
|
190
220
|
elsif matchers[:before].is_a? Date
|
191
|
-
|
221
|
+
@match_before_date = matchers[:before]
|
192
222
|
else
|
193
223
|
raise ArgumentError, "before must be a Date or Time (it's a #{matchers[:before].class.name})"
|
194
224
|
end if matchers[:before]
|
195
225
|
|
196
|
-
|
197
|
-
|
226
|
+
# Match after a request date or datetime
|
227
|
+
if matchers[:after].is_a? Time
|
228
|
+
@match_after_time = matchers[:after]
|
198
229
|
elsif matchers[:after].is_a? Date
|
199
|
-
|
230
|
+
@match_after_date = matchers[:after]
|
200
231
|
else
|
201
232
|
raise ArgumentError, "after must be a Date or Time (it's a #{matchers[:after].class.name})"
|
202
233
|
end if matchers[:after]
|
203
234
|
|
235
|
+
# Match a request date
|
204
236
|
if matchers[:on].is_a? Date
|
205
237
|
@match_on = matchers[:on]
|
206
238
|
else
|
207
239
|
raise ArgumentError, "on must be a Date (it's a #{matchers[:on].class.name})"
|
208
240
|
end if matchers[:on]
|
209
241
|
|
242
|
+
# Match request URL parameters string
|
210
243
|
case matchers[:params_str].class.name
|
211
244
|
when Regexp.name then @match_params_str = matchers[:params_str]
|
212
245
|
else raise ArgumentError, "Params String must be a Regexp (it's a #{matchers[:params_str].class.name})"
|
213
246
|
end if matchers[:params_str]
|
214
247
|
|
248
|
+
# Match request URL parameters Hash
|
215
249
|
case matchers[:params].class.name
|
216
250
|
when Hash.name then @match_params = matchers[:params]
|
217
251
|
else raise ArgumentError, "Params must be a String or a Regexp (it's a #{matchers[:params].class.name})"
|
218
252
|
end if matchers[:params]
|
219
253
|
|
254
|
+
# Match Rails request tags
|
220
255
|
if matchers[:tagged]
|
221
256
|
@match_tags = parse_tag_matchers(matchers[:tagged])
|
222
257
|
@deep_tag_match = @match_tags.any? { |t| t.is_a? Array }
|
223
258
|
end
|
224
259
|
|
260
|
+
# Match HTTP user agent string
|
261
|
+
if matchers[:user_agent].respond_to? :===
|
262
|
+
@match_user_agent = matchers[:user_agent]
|
263
|
+
else
|
264
|
+
raise ArgumentError, "User Agent must respond to the '===' method; try a String or a Regexp (it's a #{matchers[:user_agent].class.name})"
|
265
|
+
end if matchers[:user_agent]
|
266
|
+
|
267
|
+
# Match the entire log entry string
|
225
268
|
case matchers[:match].class.name
|
226
269
|
when Regexp.name then @match_r = matchers[:match]
|
227
270
|
else raise ArgumentError, "Match must be a Regexp (it's a #{matchers[:match].class.name})"
|