beaver 1.0.0 → 1.1.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 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 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?
@@ -58,22 +58,30 @@ Run ''beaver'' from the command line, passing in your beaver file and some logs:
58
58
  '/var/www/rails-app/log/production.log.1']
59
59
 
60
60
  # Parse the logs, but only include requests from yesterday
61
- Beaver.parse logs, :on => Date.today-1 do
61
+ Beaver.stream logs, :on => Date.today-1 do
62
62
  hit :failed_login, :method => :post, :path => '/login', :status => 401
63
63
  ...
64
64
  end
65
65
 
66
- == Use beaver like grep for Rails logs
66
+ == Beavers love Unix
67
67
 
68
- It's pretty difficult to grep through a multi-line log format and output each matching multi-line event. But Beaver is up to it:
68
+ 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.
69
69
 
70
- beaver --path="/widgets.*" --method=post,put /var/www/rails-app/log/production.log
70
+ beaver --path="/widgets" --method=post,put /var/www/rails-app/log/production.log
71
71
 
72
72
  Or format the output to a single line:
73
73
 
74
- beaver --path="/widgets.*" --method=post,put /var/www/rails-app/log/production.log --print "%{ip} hit %{path} using %{method}"
74
+ beaver --controller=widgets /var/www/rails-app/log/production.log --print "%{ip} hit %{action} using %{method}"
75
75
 
76
- The command-line matchers are nearly identical to the DSL's. See more details with 'beaver --help'.
76
+ Also accepts log content from pipes and stdin (might be a bit faster):
77
+
78
+ cat /var/www/rails-app/log/production.log* | beaver --action=edit
79
+
80
+ beaver --action=edit < /var/www/rails-app/log/production.log.1
81
+
82
+ beaver --action=edit --stdin
83
+
84
+ See all options with 'beaver --help'.
77
85
 
78
86
  == Example use with Logwatch
79
87
  This assumes 1) you're rotating your Rails logs daily and 2) you're running logwatch daily.
data/bin/beaver CHANGED
@@ -4,59 +4,47 @@ require 'optparse'
4
4
  require 'rubygems'
5
5
  require 'beaver'
6
6
 
7
- # Parse a string from the command-line into a Date object
8
- def parse_date(date)
9
- case date
10
- when /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/ then Date.parse(date)
11
- when /^-\d+$/ then Date.today + date.to_i
12
- else nil
13
- end
14
- end
15
-
16
- options = {}
7
+ options, matchers = {}, {}
17
8
  o = OptionParser.new do |opts|
18
- opts.banner = 'Usage: beaver [options] [dsl.rb...] /rails/production.log...'
19
- opts.on('--path PATH', 'URL path, string or regex') { |path| options[:path] = Regexp.new(path, Regexp::IGNORECASE) }
20
- opts.on('--method METHOD', 'HTTP request method(s), e.g. get or post,put') { |methods| options[:method] = methods.split(',').map { |m| m.downcase.to_sym } }
21
- opts.on('--status STATUS', 'HTTP status(es), e.g. 200 or 500..503') { |s| options[:status] = s =~ /(\.+)/ ? Range.new(*s.split($1).map(&:to_i)) : s.to_i }
22
- opts.on('--ip IP', 'IP address, string or regex') { |ip| options[:ip] = Regexp.new(ip) }
23
- opts.on('--params PARAMS', 'Request parameters string (a Ruby Hash), string or regex') { |params| options[:params_str] = Regexp.new(params, Regexp::IGNORECASE) }
24
- opts.on('--format FORMAT', 'Response format(s), e.g. html or json,xml') { |f| options[:format] = f.split(',').map(&:to_sym) }
25
- opts.on('--longer-than MS', 'Minimum response time in ms') { |ms| options[:longer_than] = ms.to_i }
26
- opts.on('--shorter-than MS', 'Maximum response time in ms') { |ms| options[:shorter_than] = ms.to_i }
27
- opts.on('--on DATE', 'Only include log entries from the given date (yyyy-mm-dd or -n days)') { |d| options[:on] = parse_date(d) }
28
- opts.on('--after DATE', 'Only include log entries from after the given date (yyyy-mm-dd or -n days)') { |d| options[:after] = parse_date(d) }
29
- opts.on('--before DATE', 'Only include log entries from before the given date (yyyy-mm-dd or -n days)') { |d| options[:before] = parse_date(d) }
30
- opts.on('--today', 'Alias to --on=-0') { options[:on] = Date.today }
31
- opts.on('--yesterday', 'Alias to --on=-1') { options[:on] = Date.today-1 }
32
- opts.on('--regex REGEX', 'A regex string to be matched against the entire request') { |r| options[:match] = Regexp.new(r, Regexp::IGNORECASE) }
33
- opts.on('--print FORMAT', 'Formatted request string, e.g. "%{ip} went to %{path} passing %{params[:email]}"') { |hit| options[:hit] = hit }
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('--params PARAMS', 'Request parameters string (a Ruby Hash), string or regex') { |params| matchers[:params_str] = Regexp.new(params, Regexp::IGNORECASE) }
17
+ opts.on('--format FORMAT', 'Response format(s), e.g. html or json,xml') { |f| matchers[:format] = f.split(',').map(&:to_sym) }
18
+ opts.on('--longer-than MS', 'Minimum response time in ms') { |ms| matchers[:longer_than] = ms.to_i }
19
+ opts.on('--shorter-than MS', 'Maximum response time in ms') { |ms| matchers[:shorter_than] = ms.to_i }
20
+ 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) }
21
+ 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) }
22
+ 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) }
23
+ opts.on('--today', 'Alias to --on=-0') { matchers[:on] = Date.today }
24
+ opts.on('--yesterday', 'Alias to --on=-1') { matchers[:on] = Date.today-1 }
25
+ opts.on('--regex REGEX', 'A regex string to be matched against the entire request') { |r| matchers[:match] = Regexp.new(r, Regexp::IGNORECASE) }
26
+ opts.on('--print FORMAT', 'Formatted request string, e.g. "%{ip} went to %{path} passing %{params[:email]}"') { |hit| options[:print] = hit }
27
+ opts.on('--stdin', 'Read log content from stdin (for typing/pasting)') { options[:tty] = true }
34
28
  opts.on('-v', '--version', 'Show version') { puts "beaver #{Beaver::VERSION}"; exit }
35
29
  end
36
30
  o.parse!
37
31
 
38
- # Separate the beaver files and log files
32
+ # Separate the beaver files and log files, then build the Beaver arguments
39
33
  beaver_files, log_files = ARGV.uniq.partition { |a| a =~ /\.rb$/ }
34
+ args = beaver_files.any? ? log_files << matchers : log_files
40
35
 
41
- # There have to be log files
42
- if log_files.empty?
43
- puts o.banner
44
- exit 1
45
- end
46
-
47
- # Run logs through the DSL file(s)
48
- if beaver_files.any?
49
- STDERR.puts "WARNING --hit is ignored when running Beaver DSL files" if options[:hit]
50
-
51
- beaver_files.map! { |b| File.open(b, &:read) }
52
- Beaver.parse *[log_files, options].flatten do
36
+ # Run Beaver
37
+ Beaver.stream *args do
38
+ stdin!
39
+ tty! if options[:tty]
40
+ # Filter the logs through the DSL files
41
+ if beaver_files.any?
42
+ beaver_files.map! { |b| File.open(b, &:read) }
53
43
  beaver_files.each { |b| eval b }
54
- end
55
- # Run logs through the CLI options
56
- else
57
- Beaver.parse *log_files do
58
- hit :cli, options do
59
- puts options[:hit] ? eval('"'+options[:hit].gsub(/%\{/, '#{')+'"') : to_s << $/
44
+ # Filter the logs through the CLI options
45
+ else
46
+ hit :cli, matchers do
47
+ puts options[:print] ? eval('"'+options[:print].gsub(/%\{/, '#{')+'"') : to_s << $/
60
48
  end
61
49
  end
62
50
  end
@@ -1,40 +1,72 @@
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
+ #
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
9
+ #
3
10
  module Beaver
4
- MAJOR_VERSION, MINOR_VERSION, TINY_VERSION, PRE_VERSION = 1, 0, 0, nil
11
+ MAJOR_VERSION, MINOR_VERSION, TINY_VERSION, PRE_VERSION = 1, 1, 0, nil
5
12
  VERSION = [MAJOR_VERSION, MINOR_VERSION, TINY_VERSION, PRE_VERSION].compact.join '.'
6
13
 
7
- # Alias to creating a new Beaver, parsing the files, and filtering them
14
+ # Creates a new Beaver and immediately filters the log files. This should scale well
15
+ # for even very large logs, at least when compared to Beaver#parse.
16
+ def self.stream(*args, &blk)
17
+ raise ArgumentError, 'You must pass a block to Beaver#stream' unless block_given?
18
+ Beaver.new(*args, &blk).stream
19
+ end
20
+
21
+ # Identical to Beaver#stream, except that the requests are retained, so you may
22
+ # examine them afterwards. For large logs, this may be noticibly inefficient.
8
23
  def self.parse(*args, &blk)
9
24
  raise ArgumentError, 'You must pass a block to Beaver#parse' unless block_given?
10
- beaver = Beaver.new(*args)
11
- beaver.parse
12
- beaver.filter(&blk)
13
- beaver
25
+ Beaver.new(*args, &blk).parse.filter
14
26
  end
15
27
 
16
28
  # Alias to creating a new Beaver
17
- def self.new(*args)
18
- Beaver.new(*args)
29
+ def self.new(*args, &blk)
30
+ Beaver.new(*args, &blk)
19
31
  end
20
32
 
21
33
  # The Beaver class, which keeps track of the files you're parsing, the Beaver::Dam objects you've defined,
22
- # and parses and stores the matching Beaver::Request objects.
34
+ # and parses and filters the matching Beaver::Request objects.
35
+ #
36
+ # beaver = Beaver.new do
37
+ # hit :help, :path => '/help' do
38
+ # puts "#{ip} needed help"
39
+ # end
40
+ # end
41
+ #
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
44
+ #
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
48
+ #
23
49
  class Beaver
24
- # The files to parse
25
- attr_reader :files
50
+ # The log files to parse
51
+ attr_reader :logs
52
+ # Parse stdin (ignores tty)
53
+ attr_accessor :stdin
54
+ # Enables parsing from tty *if* @stdin in also true
55
+ attr_accessor :tty
26
56
  # The Beaver::Dam objects you're defined
27
57
  attr_reader :dams
28
- # The Beaver::Request objects matched in the given files
58
+ # The Beaver::Request objects matched in the given log files (only availble with Beaver#parse)
29
59
  attr_reader :requests
30
60
 
31
61
  # Pass in globs or file paths. The final argument may be an options Hash.
32
62
  # These options will be applied as matchers to all hits. See Beaver::Dam for available options.
33
- def initialize(*args)
63
+ def initialize(*args, &blk)
34
64
  @global_matchers = args.last.is_a?(Hash) ? args.pop : {}
35
- @files = args.map { |a| Dir.glob(a) }
36
- @files.flatten!
65
+ @logs = args.map { |a| Dir.glob(a) }
66
+ @logs.flatten!
67
+ @stdin, @tty = false, false
37
68
  @requests, @dams, @sums = [], {}, {}
69
+ instance_eval(&blk) if block_given?
38
70
  end
39
71
 
40
72
  # Creates a new Dam and appends it to this Beaver. name should be a unique symbol.
@@ -51,49 +83,118 @@ module Beaver
51
83
  @sums[name] = callback
52
84
  end
53
85
 
54
- # Parse the logs and filter them through the dams
55
- # "parse" must be run before this, or there will be no requests
56
- def filter(&blk)
57
- instance_eval(&blk) if block_given?
58
- @requests.each do |req|
59
- @dams.each_value do |dam|
60
- if dam.matches? req
61
- catch :skip do
62
- req.instance_eval(&dam.callback) if dam.callback
63
- dam.hits << req
64
- end
86
+ # Parses the logs and immediately filters them through the dams. Requests are not retained,
87
+ # so this should scale well to very large sets of logs.
88
+ def stream
89
+ # Match the request against each dam, and run the dam's callback
90
+ _parse do |request|
91
+ hit_dams(request) do |dam|
92
+ dam.hits << request if @sums[dam.name]
93
+ end
94
+ end
95
+ # Run the callback for each summary
96
+ summarize_dams do |dam|
97
+ dam.hits.clear # Clean up
98
+ end
99
+ self
100
+ end
101
+
102
+ # Filter @requests through the dams. (Beaver#parse must be run before this, or there will be no @requests.)
103
+ # Requests will be kept around after the run.
104
+ def filter
105
+ for request in @requests
106
+ hit_dams(request) do |dam|
107
+ dam.hits << request
108
+ end
109
+ end
110
+ summarize_dams
111
+ self
112
+ end
113
+
114
+ # Parse the logs into @requests. This does *not* run them through the dams. To do that call Beaver#filter afterwards.
115
+ def parse
116
+ @requests.clear
117
+ _parse { |request| @requests << request }
118
+ self
119
+ end
120
+
121
+ # Tells this Beaver to look in STDIN for log content to parse. NOTE This ignores tty input unless you also call Beaver#tty!.
122
+ #
123
+ # *Must* be called before "stream" or "parse" to have any effect. Returns "self," so it is chainable. Can also be used in the DSL.
124
+ def stdin!
125
+ @stdin = true
126
+ self
127
+ end
128
+
129
+ # Tells this Beaver to look in STDIN for tty input.
130
+ #
131
+ # *Must* be called before "stream" or "parse" to have any effect. Returns "self," so it is chainable. Can also be used in the DSL.
132
+ def tty!
133
+ stdin!
134
+ @tty = true
135
+ self
136
+ end
137
+
138
+ private
139
+
140
+ # Run the callback on each dam matching request. Optionally pass a block, which will be passed back matching dams.
141
+ def hit_dams(request, &blk)
142
+ for dam in @dams.values
143
+ if dam.matches? request
144
+ catch :skip do
145
+ request.instance_eval(&dam.callback) if dam.callback
146
+ blk.call(dam) if block_given?
65
147
  end
66
148
  end
67
149
  end
68
- @sums.each do |dam_name, callback|
150
+ end
151
+
152
+ # Run the summary callback for each dam that had matching requests. Optionally pass a block, which will be passed back each dam.
153
+ def summarize_dams(&blk)
154
+ for dam_name, callback in @sums
69
155
  if @dams.has_key? dam_name
70
- @dams[dam_name].instance_eval(&callback) if @dams[dam_name].hits.any?
156
+ if @dams[dam_name].hits.any?
157
+ @dams[dam_name].instance_eval(&callback)
158
+ blk.call(@dams[dam_name]) if block_given?
159
+ end
71
160
  else
72
161
  STDERR.puts "WARNING You have defined a dam for '#{dam_name}', but there is no hit defined for '#{dam_name}'"
73
162
  end
74
163
  end
75
164
  end
76
165
 
77
- # Parse the logs into @requests
78
- def parse
79
- @files.each do |file|
166
+ # Parses @logs into requests, and passes each request to &blk.
167
+ def _parse(&blk)
168
+ request = nil
169
+ # Parses a line into part of a request
170
+ parse_it = lambda { |line|
171
+ request ||= Request.for(line).new
172
+ if request.bad?
173
+ request = nil
174
+ next
175
+ end
176
+ request << line
177
+ if request.completed?
178
+ blk.call(request)
179
+ request = nil
180
+ end
181
+ }
182
+
183
+ # Parse stdin
184
+ if @tty
185
+ STDIN.read.each_line &parse_it # Read entire stream, then parse it - looks much better to the user
186
+ elsif !STDIN.tty?
187
+ STDIN.each_line &parse_it
188
+ end if @stdin
189
+ request = nil
190
+
191
+ # Parse log files
192
+ for file in @logs
80
193
  zipped = file =~ /\.gz\Z/i
81
194
  next if zipped and not defined? Zlib
82
195
  File.open(file, 'r:UTF-8') do |f|
83
196
  handle = (zipped ? Zlib::GzipReader.new(f) : f)
84
- request = nil
85
- handle.each_line do |line|
86
- request = Request.for(line).new if request.nil?
87
- if request.bad?
88
- request = nil
89
- next
90
- end
91
- request << line
92
- if request.completed?
93
- @requests << request
94
- request = nil
95
- end
96
- end
197
+ handle.each_line &parse_it
97
198
  end
98
199
  end
99
200
  end
@@ -7,6 +7,10 @@ module Beaver
7
7
  # :path => String for exact match, or Regex
8
8
  #
9
9
  # :method => A Symbol of :get, :post, :put or :delete, or any array of any (reads the magic _method field if present)
10
+ #
11
+ # :controller => A String like 'FooController' or a Regex like /foo/i
12
+ #
13
+ # :action => A String like 'index' or a Regex like /(index)|(show)/
10
14
  #
11
15
  # :status => A Fixnum like 404 or a Range like (500..503)
12
16
  #
@@ -58,20 +62,20 @@ module Beaver
58
62
  # Returns true if the given Request hits this Dam, false if not.
59
63
  def matches?(request)
60
64
  return false if request.final?
61
- return false unless @match_path_s.nil? or @match_path_s == request.path
62
- return false unless @match_path_r.nil? or @match_path_r =~ request.path
65
+ return false unless @match_path.nil? or @match_path === request.path
63
66
  return false unless @match_longer.nil? or @match_longer < request.ms
64
67
  return false unless @match_shorter.nil? or @match_shorter > request.ms
65
68
  return false unless @match_method_s.nil? or @match_method_s == request.method
66
69
  return false unless @match_method_a.nil? or @match_method_a.include? request.method
67
70
  return false unless @match_status.nil? or @match_status === request.status
68
- return false unless @match_ip_s.nil? or @match_ip_s == request.ip
69
- return false unless @match_ip_r.nil? or @match_ip_r =~ request.ip
71
+ return false unless @match_controller.nil? or @match_controller === request.controller
72
+ return false unless @match_action.nil? or @match_action === request.action.to_s or @match_action == request.action
73
+ return false unless @match_ip.nil? or @match_ip === request.ip
70
74
  return false unless @match_format_s.nil? or @match_format_s == request.format
71
75
  return false unless @match_format_a.nil? or @match_format_a.include? request.format
72
76
  return false unless @match_before.nil? or @match_before > request.time
73
77
  return false unless @match_after.nil? or @match_after < request.time
74
- return false unless @match_on.nil? or (@match_on.year == request.time.year and @match_on.month == request.time.month and @match_on.day == request.time.day)
78
+ return false unless @match_on.nil? or @match_on == request.date
75
79
  return false unless @match_params_str.nil? or @match_params_str =~ request.params_str
76
80
  return false unless @match_r.nil? or @match_r =~ request.to_s
77
81
  return false unless @match_params.nil? or matching_hashes?(@match_params, request.params)
@@ -105,32 +109,44 @@ module Beaver
105
109
 
106
110
  # Parses and checks the validity of the matching options passed to the Dam.
107
111
  def set_matchers(matchers)
108
- case matchers[:path].class.name
109
- when String.name then @match_path_s = matchers[:path]
110
- when Regexp.name then @match_path_r = matchers[:path]
111
- else raise ArgumentError, "Path must be a String or a Regexp (it's a #{matchers[:path].class.name})"
112
+ if matchers[:path].respond_to? :===
113
+ @match_path = matchers[:path]
114
+ else
115
+ raise ArgumentError, "Path must respond to the '===' method; try a String or a Regexp (it's a #{matchers[:path].class.name})"
112
116
  end if matchers[:path]
113
117
 
114
- case matchers[:method].class.name
115
- when Symbol.name then @match_method_s = matchers[:method].to_s.downcase.to_sym
116
- when Array.name then @match_method_a = matchers[:method].map { |m| m.to_s.downcase.to_sym }
118
+ case
119
+ when matchers[:method].is_a?(Symbol) then @match_method_s = matchers[:method].to_s.downcase.to_sym
120
+ when matchers[:method].is_a?(Array) then @match_method_a = matchers[:method].map { |m| m.to_s.downcase.to_sym }
117
121
  else raise ArgumentError, "Method must be a Symbol or an Array (it's a #{matchers[:method].class.name})"
118
122
  end if matchers[:method]
119
123
 
124
+ if matchers[:controller].respond_to? :===
125
+ @match_controller = matchers[:controller]
126
+ else
127
+ raise ArgumentError, "Controller must respond to the '===' method; try a String or a Regexp (it's a #{matchers[:controller].class.name})"
128
+ end if matchers[:controller]
129
+
130
+ if matchers[:action].respond_to? :=== or matchers[:action].is_a? Symbol
131
+ @match_action = matchers[:action]
132
+ else
133
+ 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})"
134
+ end if matchers[:action]
135
+
120
136
  case matchers[:status].class.name
121
137
  when Fixnum.name, Range.name then @match_status = matchers[:status]
122
138
  else raise ArgumentError, "Status must be a Fixnum or a Range (it's a #{matchers[:status].class.name})"
123
139
  end if matchers[:status]
124
140
 
125
- case matchers[:ip].class.name
126
- when String.name then @match_ip_s = matchers[:ip]
127
- when Regexp.name then @match_ip_r = matchers[:ip]
128
- else raise ArgumentError, "IP must be a String or a Regexp (it's a #{matchers[:ip].class.name})"
141
+ if matchers[:ip].respond_to? :===
142
+ @match_ip = matchers[:ip]
143
+ else
144
+ raise ArgumentError, "IP must respond to the '===' method; try a String or a Regexp (it's a #{matchers[:ip].class.name})"
129
145
  end if matchers[:ip]
130
146
 
131
- case matchers[:format].class.name
132
- when Symbol.name then @match_format_s = matchers[:format].to_s.downcase.to_sym
133
- when Array.name then @match_format_a = matchers[:format].map { |f| f.to_s.downcase.to_sym }
147
+ case
148
+ when matchers[:format].is_a?(Symbol) then @match_format_s = matchers[:format].to_s.downcase.to_sym
149
+ when matchers[:format].is_a?(Array) then @match_format_a = matchers[:format].map { |f| f.to_s.downcase.to_sym }
134
150
  else raise ArgumentError, "Format must be a Symbol or an Array (it's a #{matchers[:format].class.name})"
135
151
  end if matchers[:format]
136
152
 
@@ -144,33 +160,27 @@ module Beaver
144
160
  else raise ArgumentError, "shorter_than must be a Fixnum (it's a #{matchers[:shorter_than].class.name})"
145
161
  end if matchers[:shorter_than]
146
162
 
147
- if matchers[:before]
148
- @match_before = if matchers[:before].is_a? Time
149
- matchers[:before]
150
- elsif matchers[:before].is_a? Date
151
- Utils::NormalizedTime.new(matchers[:before].year, matchers[:before].month, matchers[:before].day)
152
- else
153
- raise ArgumentError, "before must be a Date or Time (it's a #{matchers[:before].class.name})"
154
- end
155
- end
163
+ @match_before = if matchers[:before].is_a? Time
164
+ matchers[:before]
165
+ elsif matchers[:before].is_a? Date
166
+ Utils::NormalizedTime.new(matchers[:before].year, matchers[:before].month, matchers[:before].day)
167
+ else
168
+ raise ArgumentError, "before must be a Date or Time (it's a #{matchers[:before].class.name})"
169
+ end if matchers[:before]
156
170
 
157
- if matchers[:after]
158
- @match_after = if matchers[:after].is_a? Time
159
- matchers[:after]
160
- elsif matchers[:after].is_a? Date
161
- Utils::NormalizedTime.new(matchers[:after].year, matchers[:after].month, matchers[:after].day)
162
- else
163
- raise ArgumentError, "after must be a Date or Time (it's a #{matchers[:after].class.name})"
164
- end
165
- end
171
+ @match_after = if matchers[:after].is_a? Time
172
+ matchers[:after]
173
+ elsif matchers[:after].is_a? Date
174
+ Utils::NormalizedTime.new(matchers[:after].year, matchers[:after].month, matchers[:after].day)
175
+ else
176
+ raise ArgumentError, "after must be a Date or Time (it's a #{matchers[:after].class.name})"
177
+ end if matchers[:after]
166
178
 
167
- if matchers[:on]
168
- if matchers[:on].is_a? Date
169
- @match_on = matchers[:on]
170
- else
171
- raise ArgumentError, "on must be a Date (it's a #{matchers[:on].class.name})"
172
- end
173
- end
179
+ if matchers[:on].is_a? Date
180
+ @match_on = matchers[:on]
181
+ else
182
+ raise ArgumentError, "on must be a Date (it's a #{matchers[:on].class.name})"
183
+ end if matchers[:on]
174
184
 
175
185
  case matchers[:params_str].class.name
176
186
  when Regexp.name then @match_params_str = matchers[:params_str]
@@ -7,6 +7,8 @@ module Beaver
7
7
 
8
8
  REGEX_METHOD = /^Started ([A-Z]+)/
9
9
  REGEX_METHOD_OVERRIDE = /"_method"=>"([A-Z]+)"/i
10
+ REGEX_CONTROLLER = /Processing by (\w+Controller)#/
11
+ REGEX_ACTION = /Processing by \w+Controller#(\w+) as/
10
12
  REGEX_COMPLETED = /^Completed (\d+)/
11
13
  REGEX_PATH = /^Started \w{3,4} "([^"]+)"/
12
14
  REGEX_PARAMS_STR = /^ Parameters: (\{.+\})$/
@@ -44,6 +46,18 @@ module Beaver
44
46
  m ? m.captures.first.downcase.to_sym : :unknown
45
47
  end
46
48
 
49
+ # Parses the name of the Rails controller which handled the request
50
+ def parse_controller
51
+ c = REGEX_CONTROLLER.match(@lines) if c.nil?
52
+ c ? c.captures.first : BLANK_STR
53
+ end
54
+
55
+ # Parses the name of the Rails controller action which handled the request
56
+ def parse_action
57
+ a = REGEX_ACTION.match(@lines) if a.nil?
58
+ a ? a.captures.first.to_sym : :unknown
59
+ end
60
+
47
61
  # Parses and returns the response status
48
62
  def parse_status
49
63
  m = REGEX_COMPLETED.match(@lines)
@@ -80,6 +94,12 @@ module Beaver
80
94
  m ? m.captures.first.to_i : 0
81
95
  end
82
96
 
97
+ # Parses and returns the time at which the request was made
98
+ def parse_date
99
+ m = REGEX_TIME.match(@lines)
100
+ m ? Date.parse(m.captures.first) : nil
101
+ end
102
+
83
103
  # Parses and returns the time at which the request was made
84
104
  def parse_time
85
105
  m = REGEX_TIME.match(@lines)
@@ -1,3 +1,4 @@
1
+ require 'date'
1
2
  require 'time'
2
3
 
3
4
  module Beaver
@@ -49,6 +50,16 @@ module Beaver
49
50
  @method ||= parse_method
50
51
  end
51
52
 
53
+ # Returns the class name of the Rails controller that handled the request
54
+ def controller
55
+ @controller ||= parse_controller
56
+ end
57
+
58
+ # Returns the class name of the Rails controller action that handled the request
59
+ def action
60
+ @action ||= parse_action
61
+ end
62
+
52
63
  # Returns the response status
53
64
  def status
54
65
  @status ||= parse_status
@@ -79,6 +90,11 @@ module Beaver
79
90
  @ms ||= parse_ms
80
91
  end
81
92
 
93
+ # Returns the date on which the request was made
94
+ def date
95
+ @date ||= parse_date
96
+ end
97
+
82
98
  # Returns the time at which the request was made
83
99
  def time
84
100
  @time ||= parse_time
@@ -113,7 +129,17 @@ module Beaver
113
129
 
114
130
  # Parses and returns the request method
115
131
  def parse_method
116
- :method
132
+ :unknown
133
+ end
134
+
135
+ # Parses the name of the Rails controller which handled the request
136
+ def parse_controller
137
+ BLANK_STR
138
+ end
139
+
140
+ # Parses the name of the Rails controller action which handled the request
141
+ def parse_action
142
+ :unknown
117
143
  end
118
144
 
119
145
  # Parses and returns the response status
@@ -138,7 +164,7 @@ module Beaver
138
164
 
139
165
  # Parses and returns the respones format
140
166
  def parse_format
141
- :format
167
+ :unknown
142
168
  end
143
169
 
144
170
  # Parses and returns the number of milliseconds it took for the request to complete
@@ -146,6 +172,11 @@ module Beaver
146
172
  0
147
173
  end
148
174
 
175
+ # Parses and returns the date on which the request was made
176
+ def parse_date
177
+ nil
178
+ end
179
+
149
180
  # Parses and returns the time at which the request was made
150
181
  def parse_time
151
182
  nil
@@ -92,6 +92,15 @@ module Beaver
92
92
  YAML.load s
93
93
  end
94
94
 
95
+ # Parse a string (from a command-line arg) into a Date object
96
+ def self.parse_date(date)
97
+ case date
98
+ when /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/ then Date.parse(date)
99
+ when /^-\d+$/ then Date.today + date.to_i
100
+ else nil
101
+ end
102
+ end
103
+
95
104
  # Normalizes Time.new across Ruby 1.8 and 1.9.
96
105
  # Accepts the same arguments as Time.
97
106
  class NormalizedTime < ::Time
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 1
7
+ - 1
7
8
  - 0
8
- - 0
9
- version: 1.0.0
9
+ version: 1.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Jordan Hollinger
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-12-03 00:00:00 -05:00
17
+ date: 2011-12-08 00:00:00 -05:00
18
18
  default_executable:
19
19
  dependencies: []
20
20