beaver 1.2.0 → 1.3.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/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})"
|