beaver 1.0.0 → 1.1.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 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