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.
@@ -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. For a full list of methods available inside a "hit" block, or to members
13
- of the "hits" array in a "dam" block, see the Beaver::Request class.
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
- == Run a DSL file with beaver
19
- hit :failed_login, :method => :post, :path => '/login', :status => 401
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 :server_error, :status => (500..503)
34
+ hit :errors, :status => (500..503)
35
35
 
36
- dam :failed_login do
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 :server_errors do
47
- puts "Server errors:"
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 your own Beaver
54
+ == Use beaver as a library
58
55
  require 'rubygems'
59
56
  require 'beaver'
60
57
 
61
- # Logs from the last day or so
62
- logs = ['/var/www/rails-app/log/production.log',
63
- '/var/www/rails-app/log/production.log.1']
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
- == Beavers love Unix
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 (might be a bit faster):
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', 'foop'], ['bar', 'baz'], ['yay']]
132
- # Could also be written ['foo, foop', 'bar, baz', 'yay']
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 "foop") OR ("bar" AND "baz") OR "yay"
146
- beaver --tagged foo,foop --taged bar,baz --tagged yay production.log
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...] [/rails/production.log...]'
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 controller name or regex') { |c| matchers[:controller] = Regexp.new(c, Regexp::IGNORECASE) }
13
- opts.on('--action CONTROLLER', 'Rails action name or regex') { |a| matchers[:action] = Regexp.new(a, Regexp::IGNORECASE) }
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('--regex REGEX', 'A regex string to be matched against the entire request') { |r| matchers[:match] = Regexp.new(r, Regexp::IGNORECASE) }
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', 'Show version') { puts "beaver #{Beaver::VERSION}"; exit }
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! { |b| File.open(b, &:read) }
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
- hit :cli, matchers do
48
- puts options[:print] ? eval('"'+options[:print].gsub(/%\{/, '#{')+'"') : to_s << $/
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
@@ -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'
@@ -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
- # Beaver.stream do
5
- # hit :error, :status => (400..505) do
6
- # puts "#{status} on #{path} at #{time} from #{ip} with #{params_str}"
7
- # end
8
- # end
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 creating a new Beaver
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
- # beaver = Beaver.new do
37
- # hit :help, :path => '/help' do
38
- # puts "#{ip} needed help"
39
- # end
40
- # end
34
+ # beaver = Beaver.new do
35
+ # hit :help, :path => '/help' do
36
+ # puts "#{ip} needed help"
37
+ # end
38
+ # end
41
39
  #
42
- # # Method 1 - logs will be parsed and filtered line-by-line, then discarded. Performance should be constant regardless of the number of logs.
43
- # beaver.stream
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
- # # Method 2 - all of the logs will be parsed at once and stored in "beaver.requests". Then each request will be filtered.
46
- # # This does not scale as well, but is necessary *if you want to hang onto the parsed requests*.
47
- # beaver.parse.filter
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
- STDERR.puts "WARNING Overwriting existing hit '#{dam_name}'" if @dams.has_key? dam_name
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
- STDERR.puts "WARNING Overwriting existing dam '#{name}'" if @sums.has_key? name
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.matches? request
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
- STDERR.puts "WARNING You have defined a dam for '#{dam_name}', but there is no hit defined for '#{dam_name}'"
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 ||= Request.for(line).new
172
- if request.bad?
173
- request = nil
174
- next
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
- STDIN.each_line &parse_it
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
 
@@ -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
- # Matchers:
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 => String for exact match, or Regex
15
+ # :path Rails HTTP String for exact match, or Regex
8
16
  #
9
- # :method => A Symbol of :get, :post, :put or :delete, or any array of any (reads the magic _method field if present)
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 => A String like 'FooController' or a Regex like /foo/i
19
+ # :controller Rails String like 'FooController' or a Regex like /foo/i
12
20
  #
13
- # :action => A String like 'index' or a Regex like /(index)|(show)/
21
+ # :action Rails String like 'index' or a Regex like /(index)|(show)/
14
22
  #
15
- # :status => A Fixnum like 404 or a Range like (500..503)
23
+ # :status Rails HTTP Fixnum like 404 or a Range like (500..503)
16
24
  #
17
- # :ip => String for exact match, or Regex
25
+ # :ip Rails HTTP String for exact match, or Regex
18
26
  #
19
- # :format => A symbol or array of symbols of response formats like :html, :json
27
+ # :format Rails Symbol or array of symbols of response formats like :html, :json
20
28
  #
21
- # :longer_than => Fixnum n. Matches any request which took longer than n milliseconds to complete.
29
+ # :longer_than Rails Fixnum n. Matches any request which took longer than n milliseconds to complete.
22
30
  #
23
- # :shorter_than => Fixnum n. Matches any request which took less than n milliseconds to complete.
31
+ # :shorter_than Rails Fixnum n. Matches any request which took less than n milliseconds to complete.
24
32
  #
25
- # :before => Date or Time for which the request must have ocurred before
33
+ # :before Rails HTTP Date or Time for which the request must have ocurred before
26
34
  #
27
- # :after => Date or Time for which the request must have ocurred after
35
+ # :after Rails HTTP Date or Time for which the request must have ocurred after
28
36
  #
29
- # :on => Date - the request must have ocurred on this date
37
+ # :on Rails HTTP Date - the request must have ocurred on this date
30
38
  #
31
- # :params_str => A regular expressing matching the Parameters string
39
+ # :params_str Rails HTTP Regular expressing matching the Parameters string
32
40
  #
33
- # :params => A Hash of Symbol=>String/Regexp pairs: {:username => 'bob', :email => /@gmail\.com$/}. All must match.
41
+ # :params Rails Hash of Symbol=>String/Regexp pairs: {:username => 'bob', :email => /@gmail\.com$/}. All must match.
34
42
  #
35
- # :tagged => A comma-separated String or Array of Rails Tagged Logger tags. If you specify multiple tags, a request must have *all* of them.
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
- # :match => A "catch-all" Regex that will be matched against the entire request string
45
+ # :size HTTP Fixnum matching the response size in bytes
38
46
  #
39
- # The last argument may be a block, which will be called everytime this Dam is hit.
40
- # The block will be run in the context of the Request object. This can be used for
41
- # further checks or for reporting purposes.
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 matches?(request)
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 @match_before.nil? or @match_before > request.time
79
- return false unless @match_after.nil? or @match_after < request.time
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
- @match_before = if matchers[:before].is_a? Time
189
- matchers[:before]
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
- Utils::NormalizedTime.new(matchers[:before].year, matchers[:before].month, matchers[:before].day)
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
- @match_after = if matchers[:after].is_a? Time
197
- matchers[:after]
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
- Utils::NormalizedTime.new(matchers[:after].year, matchers[:after].month, matchers[:after].day)
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})"