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.
- 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
|
|