beaver 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2011 Jordan Hollinger
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.rdoc ADDED
@@ -0,0 +1,105 @@
1
+ == Beaver, chewing through logs to make something useful
2
+
3
+ Beaver is a light DSL for parsing Rails production logs back into usable data. It answers questions like:
4
+
5
+ * How many failed logins have there been today? Were they for the same user? From the same IP?
6
+ * How many 500, 404, etc. errors yesterday? On what pages?
7
+ * How many Widgets were created yesterday, and with what data?
8
+ * Did anyone submit a form with the words "kill them all"? Yikes.
9
+
10
+ Beaver is *not* intended as a replacement for something like Google Analytics, as it is trying to answer some differents kinds of questions.
11
+ Rails logs contain some great information, but I've never found a good tool for using them programatically (except for speed analyzers).
12
+ Hopefully Beaver can fill that niche for you. Personally I find it quite useful with Logwatch.
13
+
14
+ Read the full documentation at http://jordanhollinger.com/docs/beaver/. The Beaver::Dam and Beaver::Request classes
15
+ are probably what you're after.
16
+
17
+ == Installation
18
+ Beaver is still somewhat experimental, but it has proved stable and robust enough for a non-beta release.
19
+
20
+ [sudo] gem install beaver
21
+
22
+ == Use the command-line Beaver
23
+ Write your beaver file:
24
+
25
+ hit :failed_login, :method => :post, :path => '/login', :status => 401
26
+
27
+ hit :new_widgets, :path => '/widgets', :method => :post, :status => 302 do
28
+ puts "A Widget named #{params[:widget][:name]} was created!"
29
+ end
30
+
31
+ hit :help, :path => %r|^/help| do
32
+ skip! if path == '/help/page_i_want_to_ignore'
33
+ puts "#{ip} looked for help at #{path}"
34
+ end
35
+
36
+ hit :server_error, :status => (500..503)
37
+
38
+ dam :failed_login do
39
+ puts "Failed logins:"
40
+ hits.group_by { |h| h.params[:username] } do |user, fails|
41
+ puts " #{user}"
42
+ fails.each do |hit|
43
+ puts " from #{hit.ip} at #{hit.time.to_s}"
44
+ end
45
+ end
46
+ end
47
+
48
+ dam :server_errors do
49
+ puts "Server errors:"
50
+ hits.group_by(&:status) do |status, errors|
51
+ puts " There were #{errors.size} #{status} errors"
52
+ end
53
+ end
54
+
55
+ Run ''beaver'' from the command line, passing in your beaver file and some logs:
56
+
57
+ beaver my_beaver_file.rb /var/www/rails-app/log/production.log*
58
+
59
+ == Example use with Logwatch
60
+ This assumes 1) you're rotating your Rails logs daily and 2) you're running logwatch daily.
61
+
62
+ Check your beaver DSL file into your Rails app, maybe under /script.
63
+
64
+ Add a logwatch config file for your new service at /etc/logwatch/conf/services/your_app.conf:
65
+
66
+ Title = "Your Rails App"
67
+ LogFile = NONE
68
+
69
+ In /etc/logwatch/scripts/services/your_app:
70
+
71
+ beaver /var/www/your_app/script/beaver.rb --yesterday /var/www/your_app/log/production.log{,.1}
72
+
73
+ == Use your own Beaver
74
+ Use Beaver like a library:
75
+
76
+ require 'rubygems'
77
+ require 'beaver'
78
+
79
+ # Logs from the last day or so
80
+ logs = ['/var/www/rails-app/log/production.log',
81
+ '/var/www/rails-app/log/production.log.1']
82
+
83
+ # Parse the logs, but only include requests from yesterday
84
+ Beaver.parse logs, :on => Date.today-1 do
85
+ # Same DSL as above
86
+ hit :failed_login, :method => :post, :path => '/login', :status => 401
87
+ ...
88
+ end
89
+
90
+ == Your Rails app should return appropriate HTTP statuses
91
+ Rails does a lot of great things for us, but one thing largely up to us are
92
+ HTTP status codes. For example, your failed logins are probably returning
93
+ 200 when they should arguably be returning 400 or 401.
94
+
95
+ render :action => :login, :status => 401
96
+
97
+ See, it's easy. And very useful to Beaver.
98
+
99
+ == TODO
100
+ * Add support for Apache/Nginx/Rack::CommonLogger
101
+
102
+ == License
103
+ Copyright 2011 Jordan Hollinger
104
+
105
+ Licensed under the Apache License
data/bin/beaver ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'rubygems'
5
+ require 'beaver'
6
+
7
+ #
8
+ # This application was installed by RubyGems as part of the 'beaver' gem.
9
+ #
10
+
11
+ # Parse a string from the command-line into a Date object
12
+ def parse_date(date)
13
+ case date
14
+ when /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/ then Date.parse(date)
15
+ when /^-\d+$/ then Date.today + date.to_i
16
+ else nil
17
+ end
18
+ end
19
+
20
+ options = {}
21
+ o = OptionParser.new do |opts|
22
+ opts.banner = 'Usage: beaver dams.rb [options] /path/to/rails/production.log [/another/log [/yet/another/log]]'
23
+ opts.on('--on DATE', 'Only include log entries from the given date (yyyy-mm-dd or -n days)') { |d| options[:on] = parse_date(d) }
24
+ 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) }
25
+ 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) }
26
+ opts.on('--today', 'Alias to --on=-0') { options[:on] = Date.today }
27
+ opts.on('--yesterday', 'Alias to --on=-1') { options[:on] = Date.today-1 }
28
+ end
29
+ o.parse!
30
+
31
+ if ARGV.size < 2
32
+ puts o.banner
33
+ exit 1
34
+ end
35
+
36
+ beaver_file = File.open(ARGV.shift, &:read)
37
+ Beaver.parse *[ARGV, options].flatten do
38
+ eval beaver_file
39
+ end
data/lib/beaver.rb ADDED
@@ -0,0 +1,12 @@
1
+ begin
2
+ require 'zlib'
3
+ rescue LoadError
4
+ $stderr.puts "Zlib not available; compressed log files will be skipped."
5
+ end
6
+
7
+ require 'beaver/utils'
8
+ require 'beaver/beaver'
9
+ require 'beaver/dam'
10
+ require 'beaver/request'
11
+
12
+ require 'beaver/parsers/rails'
@@ -0,0 +1,94 @@
1
+ # Not specifically a performance analyzer (like https://github.com/wvanbergen/request-log-analyzer/wiki)
2
+ # Rather, a DSL for finding out how people are using your Rails app (which could include performance).
3
+ module Beaver
4
+ # Alias to creating a new Beaver, parsing the files, and filtering them
5
+ def self.parse(*args, &blk)
6
+ raise ArgumentError, 'You must pass a block to Beaver#parse' unless block_given?
7
+ beaver = Beaver.new(*args)
8
+ beaver.parse
9
+ beaver.filter(&blk)
10
+ beaver
11
+ end
12
+
13
+ # Alias to creating a new Beaver
14
+ def self.new(*args)
15
+ Beaver.new(*args)
16
+ end
17
+
18
+ # The Beaver class, which keeps track of the files you're parsing, the Beaver::Dam objects you've defined,
19
+ # and parses and stores the matching Beaver::Request objects.
20
+ class Beaver
21
+ # The files to parse
22
+ attr_reader :files
23
+ # The Beaver::Dam objects you're defined
24
+ attr_reader :dams
25
+ # The Beaver::Request objects matched in the given files
26
+ attr_reader :requests
27
+
28
+ # Pass in globs or file paths. The final argument may be an options Hash.
29
+ # These options will be applied as matchers to all hits. See Beaver::Dam for available options.
30
+ def initialize(*args)
31
+ @global_matchers = args.last.is_a?(Hash) ? args.pop : {}
32
+ @files = args.map { |a| Dir.glob(a) }
33
+ @files.flatten!
34
+ @requests, @dams, @sums = [], {}, {}
35
+ end
36
+
37
+ # Creates a new Dam and appends it to this Beaver. name should be a unique symbol.
38
+ # See Beaver::Dam for available options.
39
+ def hit(dam_name, matchers={}, &callback)
40
+ raise ArgumentError, "A dam named #{dam_name} already exists" if @dams.has_key? dam_name
41
+ matchers = @global_matchers.merge matchers
42
+ @dams[dam_name] = Dam.new(dam_name, matchers, &callback)
43
+ end
44
+
45
+ # Define a sumarry for a Dam
46
+ def dam(name, &callback)
47
+ raise ArgumentError, "Unable to find a Dam named #{name}" unless @dams.has_key? name
48
+ @sums[name] = callback
49
+ end
50
+
51
+ # Parse the logs and filter them through the dams
52
+ # "parse" must be run before this, or there will be no requests
53
+ def filter(&blk)
54
+ instance_eval(&blk) if block_given?
55
+ @requests.each do |req|
56
+ @dams.each_value do |dam|
57
+ if dam.matches? req
58
+ catch :skip do
59
+ req.instance_eval(&dam.callback) if dam.callback
60
+ dam.hits << req
61
+ end
62
+ end
63
+ end
64
+ end
65
+ @sums.each do |dam_name, callback|
66
+ @dams[dam_name].instance_eval(&callback) if @dams[dam_name].hits.any?
67
+ end
68
+ end
69
+
70
+ # Parse the logs into @requests
71
+ def parse
72
+ @files.each do |file|
73
+ zipped = file =~ /\.gz\Z/i
74
+ next if zipped and not defined? Zlib
75
+ File.open(file, 'r:UTF-8') do |f|
76
+ handle = (zipped ? Zlib::GzipReader.new(f) : f)
77
+ request = nil
78
+ handle.each_line do |line|
79
+ request = Request.for(line).new if request.nil?
80
+ if request.bad?
81
+ request = nil
82
+ next
83
+ end
84
+ request << line
85
+ if request.completed?
86
+ @requests << request
87
+ request = nil
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/beaver/dam.rb ADDED
@@ -0,0 +1,181 @@
1
+ module Beaver
2
+ # A Dam "traps" certain Requests, using one or more matching options. A request must meet *all* of the
3
+ # matching options specified.
4
+ #
5
+ # Matchers:
6
+ #
7
+ # :path => String for exact match, or Regex
8
+ #
9
+ # :method => A Symbol of :get, :post, :update or :delete, or any array of any (reads the magic _method field if present)
10
+ #
11
+ # :status => A Fixnum like 404 or a Range like (500..503)
12
+ #
13
+ # :ip => String for exact match, or Regex
14
+ #
15
+ # :longer_than => Fixnum n. Matches any request which took longer than n milliseconds to complete.
16
+ #
17
+ # :shorter_than => Fixnum n. Matches any request which took less than n milliseconds to complete.
18
+ #
19
+ # :before => Date or Time for which the request must have ocurred before
20
+ #
21
+ # :after => Date or Time for which the request must have ocurred after
22
+ #
23
+ # :on => Date - the request must have ocurred on this date
24
+ #
25
+ # :params_str => A regular expressing matching the Parameters string
26
+ #
27
+ # :params => A Hash of Symbol=>String/Regexp pairs: {:username => 'bob', :email => /@gmail\.com$/}. All must match.
28
+ #
29
+ # :match => A "catch-all" Regex that will be matched against the entire request string
30
+ #
31
+ # The last argument may be a block, which will be called everytime this Dam is hit.
32
+ # The block will be run in the context of the Request object. This can be used for
33
+ # further checks or for reporting purposes.
34
+ class Dam
35
+ # The symbol name of this Beaver::Dam
36
+ attr_reader :name
37
+ # An optional callback when a Beaver::Request hits this Dam
38
+ attr_reader :callback
39
+ # An array of Beaver::Request objects that have hit this Dam
40
+ attr_reader :hits
41
+
42
+ # Name should be a unique symbol. Matchers is an options Hash. The callback will be evauluated within
43
+ # the context of a Beaver::Request.
44
+ def initialize(name, matchers, &callback)
45
+ @name = name
46
+ @callback = callback
47
+ @hits = []
48
+ set_matchers(matchers)
49
+ end
50
+
51
+ # Returns an array of IP address that hit this Dam.
52
+ def ips
53
+ @ips ||= @hits.map(&:ip).uniq
54
+ end
55
+
56
+ # Returns true if the given Request hits this Dam, false if not.
57
+ def matches?(request)
58
+ return false if request.final?
59
+ return false unless @match_path_s.nil? or @match_path_s == request.path
60
+ return false unless @match_path_r.nil? or @match_path_r =~ request.path
61
+ return false unless @match_longer.nil? or @match_longer < request.ms
62
+ return false unless @match_shorter.nil? or @match_shorter > request.ms
63
+ return false unless @match_method_s.nil? or @match_method_s == request.method
64
+ return false unless @match_method_a.nil? or @match_method_a.include? request.method
65
+ return false unless @match_status.nil? or @match_status === request.status
66
+ return false unless @match_ip_s.nil? or @match_ip_s == request.ip
67
+ return false unless @match_ip_r.nil? or @match_ip_r =~ request.ip
68
+ return false unless @match_before.nil? or @match_before > request.time
69
+ return false unless @match_after.nil? or @match_after < request.time
70
+ 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)
71
+ return false unless @match_params_str.nil? or @match_params_str =~ request.params_str
72
+ return false unless @match_r.nil? or @match_r =~ request.to_s
73
+ return false unless @match_params.nil? or matching_hashes?(@match_params, request.params)
74
+ return true
75
+ end
76
+
77
+ private
78
+
79
+ # Recursively compares to Hashes. If all of Hash A is in Hash B, they match.
80
+ def matching_hashes?(a,b)
81
+ intersecting_keys = a.keys & b.keys
82
+ if intersecting_keys.any?
83
+ a_values = a.values_at(*intersecting_keys)
84
+ b_values = b.values_at(*intersecting_keys)
85
+ indicies = (0..b_values.size-1)
86
+ indicies.all? do |i|
87
+ if a_values[i].is_a? String
88
+ a_values[i] == b_values[i]
89
+ elsif a_values[i].is_a?(Regexp) and b_values[i].is_a?(String)
90
+ a_values[i] =~ b_values[i]
91
+ elsif a_values[i].is_a?(Hash) and b_values[i].is_a?(Hash)
92
+ matching_hashes? a_values[i], b_values[i]
93
+ else
94
+ false
95
+ end
96
+ end
97
+ else
98
+ false
99
+ end
100
+ end
101
+
102
+ # Parses and checks the validity of the matching options passed to the Dam.
103
+ def set_matchers(matchers)
104
+ case matchers[:path].class.name
105
+ when String.name then @match_path_s = matchers[:path]
106
+ when Regexp.name then @match_path_r = matchers[:path]
107
+ else raise ArgumentError, "Path must be a String or a Regexp (it's a #{matchers[:path].class.name})"
108
+ end if matchers[:path]
109
+
110
+ case matchers[:method].class.name
111
+ when Symbol.name then @match_method_s = matchers[:method]
112
+ when Array.name then @match_method_a = matchers[:method]
113
+ else raise ArgumentError, "Method must be a Symbol or an Array (it's a #{matchers[:method].class.name})"
114
+ end if matchers[:method]
115
+
116
+ case matchers[:status].class.name
117
+ when Fixnum.name, Range.name then @match_status = matchers[:status]
118
+ else raise ArgumentError, "Status must be a Fixnum or a Range (it's a #{matchers[:status].class.name})"
119
+ end if matchers[:status]
120
+
121
+ case matchers[:ip].class.name
122
+ when String.name then @match_status_s = matchers[:ip]
123
+ when Regexp.name then @match_status_r = matchers[:ip]
124
+ else raise ArgumentError, "IP must be a String or a Regexp (it's a #{matchers[:ip].class.name})"
125
+ end if matchers[:ip]
126
+
127
+ case matchers[:longer_than].class.name
128
+ when Fixnum.name then @match_longer = matchers[:longer_than]
129
+ else raise ArgumentError, "longer_than must be a Fixnum (it's a #{matchers[:longer_than].class.name})"
130
+ end if matchers[:longer_than]
131
+
132
+ case matchers[:shorter_than].class.name
133
+ when Fixnum.name then @match_shorter = matchers[:shorter_than]
134
+ else raise ArgumentError, "shorter_than must be a Fixnum (it's a #{matchers[:shorter_than].class.name})"
135
+ end if matchers[:shorter_than]
136
+
137
+ if matchers[:before]
138
+ @match_before = if matchers[:before].is_a? Time
139
+ matchers[:before]
140
+ elsif matchers[:before].is_a? Date
141
+ Utils::NormalizedTime.new(matchers[:before].year, matchers[:before].month, matchers[:before].day)
142
+ else
143
+ raise ArgumentError, "before must be a Date or Time (it's a #{matchers[:before].class.name})"
144
+ end
145
+ end
146
+
147
+ if matchers[:after]
148
+ @match_after = if matchers[:after].is_a? Time
149
+ matchers[:after]
150
+ elsif matchers[:after].is_a? Date
151
+ Utils::NormalizedTime.new(matchers[:after].year, matchers[:after].month, matchers[:after].day)
152
+ else
153
+ raise ArgumentError, "after must be a Date or Time (it's a #{matchers[:after].class.name})"
154
+ end
155
+ end
156
+
157
+ if matchers[:on]
158
+ if matchers[:on].is_a? Date
159
+ @match_on = matchers[:on]
160
+ else
161
+ raise ArgumentError, "on must be a Date (it's a #{matchers[:on].class.name})"
162
+ end
163
+ end
164
+
165
+ case matchers[:params_str].class.name
166
+ when Regexp.name then @match_params_str = matchers[:params_str]
167
+ else raise ArgumentError, "Params String must be a Regexp (it's a #{matchers[:params_str].class.name})"
168
+ end if matchers[:params_str]
169
+
170
+ case matchers[:params].class.name
171
+ when Hash.name then @match_params = matchers[:params]
172
+ else raise ArgumentError, "Params must be a String or a Regexp (it's a #{matchers[:params].class.name})"
173
+ end if matchers[:params]
174
+
175
+ case matchers[:match].class.name
176
+ when Regexp.name then @match_r = matchers[:match]
177
+ else raise ArgumentError, "Match must be a Regexp (it's a #{matchers[:match].class.name})"
178
+ end if matchers[:match]
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,83 @@
1
+ module Beaver
2
+ module Parsers
3
+ # This appears to work with Rails 3 logs
4
+ class Rails < Request
5
+ # Tell the Request class to use this parser to parse logs
6
+ Request << self
7
+
8
+ REGEX_METHOD = /^Started ([A-Z]+)/
9
+ REGEX_METHOD_OVERRIDE = /"_method"=>"([A-Z]+)"/i
10
+ REGEX_COMPLETED = /^Completed (\d+)/
11
+ REGEX_PATH = /^Started \w{3,4} "([^"]+)"/
12
+ REGEX_PARAMS_STR = /^ Parameters: (\{.+\})$/
13
+ REGEX_IP = /" for (\d+[\d.]+) at /
14
+ REGEX_MS = / in (\d+)ms/
15
+ # Depending on the version of Rails, the time format may be wildly different
16
+ REGEX_TIME = / at ([a-z0-9:\+\- ]+)$/i
17
+
18
+ # Returns true if the given lines look like a Rails request
19
+ def self.match?(lines)
20
+ REGEX_METHOD =~ lines
21
+ end
22
+
23
+ # Returns true, always
24
+ def valid?; true; end
25
+
26
+ # Returns true if/when we have the final line of the multi-line Rails request
27
+ def completed?
28
+ REGEX_COMPLETED =~ @lines
29
+ end
30
+
31
+ protected
32
+
33
+ # Parses and returns the request path
34
+ def parse_path
35
+ m = REGEX_PATH.match(@lines)
36
+ m ? m.captures.first : BLANK_STR
37
+ end
38
+
39
+ # Parses and returns the request method
40
+ def parse_method
41
+ m = REGEX_METHOD_OVERRIDE.match(@lines)
42
+ m = REGEX_METHOD.match(@lines) if m.nil?
43
+ m ? m.captures.first.downcase.to_sym : :method
44
+ end
45
+
46
+ # Parses and returns the response status
47
+ def parse_status
48
+ m = REGEX_COMPLETED.match(@lines)
49
+ m ? m.captures.first.to_i : 0
50
+ end
51
+
52
+ # Parses and returns the request parameters as a String
53
+ def parse_params_str
54
+ m = REGEX_PARAMS_STR.match(@lines)
55
+ m ? m.captures.first : BLANK_STR
56
+ end
57
+
58
+ # Parses and returns the request parameters as a Hash (relatively expensive)
59
+ def parse_params
60
+ p = params_str
61
+ p.empty? ? BLANK_HASH : Utils.str_to_hash(p)
62
+ end
63
+
64
+ # Parses and returns the request's IP address
65
+ def parse_ip
66
+ m = REGEX_IP.match(@lines)
67
+ m ? m.captures.first : BLANK_STR
68
+ end
69
+
70
+ # Parses and returns the number of milliseconds it took for the request to complete
71
+ def parse_ms
72
+ m = REGEX_MS.match(@lines)
73
+ m ? m.captures.first.to_i : 0
74
+ end
75
+
76
+ # Parses and returns the time at which the request was made
77
+ def parse_time
78
+ m = REGEX_TIME.match(@lines)
79
+ m ? Time.parse(m.captures.first) : nil
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,150 @@
1
+ require 'time'
2
+
3
+ module Beaver
4
+ # Represents a single request from the logs.
5
+ class Request
6
+ BLANK_STR = ''
7
+ BLANK_HASH = {}
8
+
9
+ # Holds the Parser classes used to parser requests
10
+ @types = []
11
+
12
+ # Add a child Request parser.
13
+ def self.<<(type)
14
+ @types << type
15
+ end
16
+
17
+ # Returns the correct Request parser class for the given log lines. If one cannot be found,
18
+ # the BadRequest class is returned, which the caller will want to ignore.
19
+ def self.for(lines)
20
+ @types.select { |t| t.match? lines }.first || BadRequest
21
+ end
22
+
23
+ # Accepts a String of log lines, presumably ones which belong to a single request.
24
+ def initialize(lines=nil)
25
+ @lines = lines || ''
26
+ @final = false
27
+ end
28
+
29
+ # Returns the log lines that make up this Request.
30
+ def to_s; @lines; end
31
+
32
+ # Returns true if this is a "good" request.
33
+ def good?; true; end
34
+ # Returns true if this is a "bad" request.
35
+ def bad?; not good?; end
36
+
37
+ # Append a log line
38
+ def <<(line)
39
+ @lines << line << $/
40
+ end
41
+
42
+ # Returns the request path
43
+ def path
44
+ @path ||= parse_path
45
+ end
46
+
47
+ # Returns the request method
48
+ def method
49
+ @method ||= parse_method
50
+ end
51
+
52
+ # Returns the response status
53
+ def status
54
+ @status ||= parse_status
55
+ end
56
+
57
+ # Returns the request parameters as a String
58
+ def params_str
59
+ @params_str ||= parse_params_str
60
+ end
61
+
62
+ # Returns the request parameters as a Hash (this is more expensive than Request#params_str)
63
+ def params
64
+ @params ||= parse_params
65
+ end
66
+
67
+ # Returns the IP address of the request
68
+ def ip
69
+ @ip ||= parse_ip
70
+ end
71
+
72
+ # Returns the number of milliseconds it took for the request to complete
73
+ def ms
74
+ @ms ||= parse_ms
75
+ end
76
+
77
+ # Returns the time at which the request was made
78
+ def time
79
+ @time ||= parse_time
80
+ end
81
+
82
+ # When called inside of a Beaver::Dam#hit block, this Request will *not* be matched.
83
+ def skip!
84
+ throw :skip
85
+ end
86
+
87
+ # When called inside of a Beaver::Dam#hit block, this Request will not match against any other Beaver::Dam.
88
+ def final!
89
+ @final = true
90
+ end
91
+
92
+ # Returns true if this Request should not be matched against any more Dams.
93
+ def final?
94
+ @final
95
+ end
96
+
97
+ # Returns true if the request has all the information it needs to be properly parsed
98
+ def completed?
99
+ true
100
+ end
101
+
102
+ protected
103
+
104
+ # Parses and returns the request path
105
+ def parse_path
106
+ BLANK_STR
107
+ end
108
+
109
+ # Parses and returns the request method
110
+ def parse_method
111
+ :method
112
+ end
113
+
114
+ # Parses and returns the response status
115
+ def parse_status
116
+ 0
117
+ end
118
+
119
+ # Parses and returns the request params as a String
120
+ def parse_params_str
121
+ BLANK_STR
122
+ end
123
+
124
+ # Parses and returns the request params as a Hash
125
+ def parse_params
126
+ BLANK_HASH
127
+ end
128
+
129
+ # Parses and returns the request IP address
130
+ def parse_ip
131
+ BLANK_STR
132
+ end
133
+
134
+ # Parses and returns the number of milliseconds it took for the request to complete
135
+ def parse_ms
136
+ 0
137
+ end
138
+
139
+ # Parses and returns the time at which the request was made
140
+ def parse_time
141
+ nil
142
+ end
143
+ end
144
+
145
+ # Represents a BadRequest that no parser could figure out.
146
+ class BadRequest < Request
147
+ # Returns false, always
148
+ def good?; false; end
149
+ end
150
+ end
@@ -0,0 +1,107 @@
1
+ require 'yaml'
2
+ require 'time'
3
+
4
+ module Beaver
5
+ # Sundry utility methods and classes for use by Beaver
6
+ module Utils
7
+ LBRACE, RBRACE = '{', '}'
8
+ LBRACKET, RBRACKET = '[', ']'
9
+ QUOTE = '"'
10
+ ESCAPE = '\\'
11
+ EQUAL = '='
12
+ COLIN = ':'
13
+ TO_SPACE = ['>']
14
+ SPACE = ' '
15
+ COMMA = ','
16
+ LETTER_REGEX = /^[a-z]$/i
17
+
18
+ # Converts a string representation of a Hash into YAML, then into a Hash.
19
+ # This is targeted towards the Parameters value in Rails logs. It is assumed that every key is a represented as a String in the logs.
20
+ # All keys, except for numeric keys, will be converted to Symbols.
21
+ def self.str_to_hash(str)
22
+ s = ''
23
+ indent = 0
24
+ state = :pre_key
25
+ i = 0
26
+ str.each_char do |c|
27
+ i += 1
28
+ case c
29
+ when QUOTE
30
+ case state
31
+ when :pre_key
32
+ s << (SPACE * indent)
33
+ s << COLIN if str[i,1] =~ LETTER_REGEX
34
+ state = :key
35
+ next
36
+ when :key
37
+ s << COLIN << SPACE
38
+ state = :pre_val
39
+ next
40
+ when :pre_val, :escape
41
+ state = :val
42
+ next
43
+ when :val
44
+ state = :pre_key
45
+ s << "\n"
46
+ next
47
+ end
48
+ when LBRACE
49
+ case state
50
+ # Hash as a value, starting a new indent level
51
+ when :pre_val
52
+ state = :pre_key
53
+ indent += 2
54
+ s << "\n"# << (SPACE * indent)
55
+ next
56
+ when :pre_key
57
+ next
58
+ end
59
+ when RBRACE
60
+ case state
61
+ when :pre_key
62
+ indent -= 2 if indent > 0
63
+ end
64
+ when LBRACKET
65
+ case state
66
+ when :pre_val
67
+ state = :val_array
68
+ end
69
+ when RBRACKET
70
+ case state
71
+ when :val_array
72
+ state = :val_array_end
73
+ end
74
+ when ESCAPE
75
+ if state == :val
76
+ state = :escape
77
+ next
78
+ end
79
+ end
80
+
81
+ case state
82
+ when :key, :val, :val_array
83
+ s << c
84
+ when :escape
85
+ s << c
86
+ state = :val
87
+ when :val_array_end
88
+ s << c << "\n"
89
+ state = :pre_key
90
+ end
91
+ end
92
+ YAML.load s
93
+ end
94
+
95
+ # Normalizes Time.new across Ruby 1.8 and 1.9.
96
+ # Accepts the same arguments as Time.
97
+ class NormalizedTime < ::Time
98
+ if RUBY_VERSION < '1.9'
99
+ # Returns a new NormalizedTime object.
100
+ def self.new(*args)
101
+ args.pop if args.last.is_a? String
102
+ local(*args)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: beaver
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Jordan Hollinger
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-09-24 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: A simple DSL for helping you discover what people are doing with your Rails app
22
+ email: jordan@jordanhollinger.com
23
+ executables:
24
+ - beaver
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib/beaver.rb
31
+ - lib/beaver/dam.rb
32
+ - lib/beaver/beaver.rb
33
+ - lib/beaver/request.rb
34
+ - lib/beaver/parsers/rails.rb
35
+ - lib/beaver/utils.rb
36
+ - README.rdoc
37
+ - LICENSE
38
+ - bin/beaver
39
+ has_rdoc: true
40
+ homepage: http://github.com/jhollinger/beaver
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.3.7
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Rails production log parser
71
+ test_files: []
72
+