beaver 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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})"