beaver 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +15 -7
- data/bin/beaver +33 -45
- data/lib/beaver/beaver.rb +145 -44
- data/lib/beaver/dam.rb +54 -44
- data/lib/beaver/parsers/rails.rb +20 -0
- data/lib/beaver/request.rb +33 -2
- data/lib/beaver/utils.rb +9 -0
- metadata +3 -3
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
== Beaver, chewing through logs to make something useful
|
2
2
|
|
3
|
-
Beaver is a light DSL 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.
|
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
|
-
==
|
66
|
+
== Beavers love Unix
|
67
67
|
|
68
|
-
It's
|
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
|
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 --
|
74
|
+
beaver --controller=widgets /var/www/rails-app/log/production.log --print "%{ip} hit %{action} using %{method}"
|
75
75
|
|
76
|
-
|
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
|
-
|
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|
|
20
|
-
opts.on('--method METHOD', 'HTTP request method(s), e.g. get or post,put') { |methods|
|
21
|
-
opts.on('--
|
22
|
-
opts.on('--
|
23
|
-
opts.on('--
|
24
|
-
opts.on('--
|
25
|
-
opts.on('--
|
26
|
-
opts.on('--
|
27
|
-
opts.on('--
|
28
|
-
opts.on('--
|
29
|
-
opts.on('--
|
30
|
-
opts.on('--
|
31
|
-
opts.on('--
|
32
|
-
opts.on('--
|
33
|
-
opts.on('--
|
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
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
data/lib/beaver/beaver.rb
CHANGED
@@ -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,
|
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
|
-
#
|
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
|
-
|
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
|
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 :
|
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
|
-
@
|
36
|
-
@
|
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
|
-
#
|
55
|
-
#
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
if dam.
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
78
|
-
def
|
79
|
-
|
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
|
-
|
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
|
data/lib/beaver/dam.rb
CHANGED
@@ -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 @
|
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 @
|
69
|
-
return false unless @
|
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
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
115
|
-
when Symbol
|
116
|
-
when Array
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
132
|
-
when Symbol
|
133
|
-
when Array
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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]
|
data/lib/beaver/parsers/rails.rb
CHANGED
@@ -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)
|
data/lib/beaver/request.rb
CHANGED
@@ -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
|
-
:
|
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
|
-
:
|
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
|
data/lib/beaver/utils.rb
CHANGED
@@ -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
|
-
|
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-
|
17
|
+
date: 2011-12-08 00:00:00 -05:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|