beaver 0.0.1 → 1.0.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 CHANGED
@@ -7,21 +7,14 @@ Beaver is a light DSL for parsing Rails production logs back into usable data. I
7
7
  * How many Widgets were created yesterday, and with what data?
8
8
  * Did anyone submit a form with the words "kill them all"? Yikes.
9
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.
10
+ Read the full documentation at {jordanhollinger.com/docs/beaver/}[http://jordanhollinger.com/docs/beaver/].
11
+ For a full list of matchers available to "hit", see the Beaver::Dam class. For a full list of methods available inside a "hit" block, or to members
12
+ of the "hits" array in a "dam" block, see the Beaver::Request class.
16
13
 
17
14
  == Installation
18
- Beaver is still somewhat experimental, but it has proved stable and robust enough for a non-beta release.
19
-
20
15
  [sudo] gem install beaver
21
16
 
22
- == Use the command-line Beaver
23
- Write your beaver file:
24
-
17
+ == Run a DSL file with beaver
25
18
  hit :failed_login, :method => :post, :path => '/login', :status => 401
26
19
 
27
20
  hit :new_widgets, :path => '/widgets', :method => :post, :status => 302 do
@@ -56,23 +49,7 @@ Run ''beaver'' from the command line, passing in your beaver file and some logs:
56
49
 
57
50
  beaver my_beaver_file.rb /var/www/rails-app/log/production.log*
58
51
 
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
52
  == Use your own Beaver
74
- Use Beaver like a library:
75
-
76
53
  require 'rubygems'
77
54
  require 'beaver'
78
55
 
@@ -82,23 +59,43 @@ Use Beaver like a library:
82
59
 
83
60
  # Parse the logs, but only include requests from yesterday
84
61
  Beaver.parse logs, :on => Date.today-1 do
85
- # Same DSL as above
86
62
  hit :failed_login, :method => :post, :path => '/login', :status => 401
87
63
  ...
88
64
  end
89
65
 
66
+ == Use beaver like grep for Rails logs
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:
69
+
70
+ beaver --path="/widgets.*" --method=post,put /var/www/rails-app/log/production.log
71
+
72
+ Or format the output to a single line:
73
+
74
+ beaver --path="/widgets.*" --method=post,put /var/www/rails-app/log/production.log --print "%{ip} hit %{path} using %{method}"
75
+
76
+ The command-line matchers are nearly identical to the DSL's. See more details with 'beaver --help'.
77
+
78
+ == Example use with Logwatch
79
+ This assumes 1) you're rotating your Rails logs daily and 2) you're running logwatch daily.
80
+
81
+ Check your beaver DSL file into your Rails app, maybe under /script.
82
+
83
+ Add a logwatch config file for your new service at /etc/logwatch/conf/services/your_app.conf:
84
+
85
+ Title = "Your Rails App"
86
+ LogFile = NONE
87
+
88
+ In /etc/logwatch/scripts/services/your_app:
89
+
90
+ beaver /var/www/your_app/script/beaver.rb --yesterday /var/www/your_app/log/production.log{,.1}
91
+
90
92
  == Your Rails app should return appropriate HTTP statuses
91
93
  Rails does a lot of great things for us, but one thing largely up to us are
92
94
  HTTP status codes. For example, your failed logins are probably returning
93
- 200 when they should arguably be returning 400 or 401.
95
+ 200 when they should arguably be returning 400 or 401. It's easy to do, and very useful to Beaver.
94
96
 
95
97
  render :action => :login, :status => 401
96
98
 
97
- See, it's easy. And very useful to Beaver.
98
-
99
- == TODO
100
- * Add support for Apache/Nginx/Rack::CommonLogger
101
-
102
99
  == License
103
100
  Copyright 2011 Jordan Hollinger
104
101
 
data/bin/beaver CHANGED
@@ -4,10 +4,6 @@ require 'optparse'
4
4
  require 'rubygems'
5
5
  require 'beaver'
6
6
 
7
- #
8
- # This application was installed by RubyGems as part of the 'beaver' gem.
9
- #
10
-
11
7
  # Parse a string from the command-line into a Date object
12
8
  def parse_date(date)
13
9
  case date
@@ -19,21 +15,48 @@ end
19
15
 
20
16
  options = {}
21
17
  o = OptionParser.new do |opts|
22
- opts.banner = 'Usage: beaver dams.rb [options] /path/to/rails/production.log [/another/log [/yet/another/log]]'
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 }
23
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) }
24
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) }
25
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) }
26
30
  opts.on('--today', 'Alias to --on=-0') { options[:on] = Date.today }
27
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 }
34
+ opts.on('-v', '--version', 'Show version') { puts "beaver #{Beaver::VERSION}"; exit }
28
35
  end
29
36
  o.parse!
30
37
 
31
- if ARGV.size < 2
38
+ # Separate the beaver files and log files
39
+ beaver_files, log_files = ARGV.uniq.partition { |a| a =~ /\.rb$/ }
40
+
41
+ # There have to be log files
42
+ if log_files.empty?
32
43
  puts o.banner
33
44
  exit 1
34
45
  end
35
46
 
36
- beaver_file = File.open(ARGV.shift, &:read)
37
- Beaver.parse *[ARGV, options].flatten do
38
- eval beaver_file
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
53
+ 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 << $/
60
+ end
61
+ end
39
62
  end
data/lib/beaver/beaver.rb CHANGED
@@ -1,6 +1,9 @@
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
3
  module Beaver
4
+ MAJOR_VERSION, MINOR_VERSION, TINY_VERSION, PRE_VERSION = 1, 0, 0, nil
5
+ VERSION = [MAJOR_VERSION, MINOR_VERSION, TINY_VERSION, PRE_VERSION].compact.join '.'
6
+
4
7
  # Alias to creating a new Beaver, parsing the files, and filtering them
5
8
  def self.parse(*args, &blk)
6
9
  raise ArgumentError, 'You must pass a block to Beaver#parse' unless block_given?
@@ -37,14 +40,14 @@ module Beaver
37
40
  # Creates a new Dam and appends it to this Beaver. name should be a unique symbol.
38
41
  # See Beaver::Dam for available options.
39
42
  def hit(dam_name, matchers={}, &callback)
40
- raise ArgumentError, "A dam named #{dam_name} already exists" if @dams.has_key? dam_name
43
+ STDERR.puts "WARNING Overwriting existing hit '#{dam_name}'" if @dams.has_key? dam_name
41
44
  matchers = @global_matchers.merge matchers
42
45
  @dams[dam_name] = Dam.new(dam_name, matchers, &callback)
43
46
  end
44
47
 
45
48
  # Define a sumarry for a Dam
46
49
  def dam(name, &callback)
47
- raise ArgumentError, "Unable to find a Dam named #{name}" unless @dams.has_key? name
50
+ STDERR.puts "WARNING Overwriting existing dam '#{name}'" if @sums.has_key? name
48
51
  @sums[name] = callback
49
52
  end
50
53
 
@@ -63,7 +66,11 @@ module Beaver
63
66
  end
64
67
  end
65
68
  @sums.each do |dam_name, callback|
66
- @dams[dam_name].instance_eval(&callback) if @dams[dam_name].hits.any?
69
+ if @dams.has_key? dam_name
70
+ @dams[dam_name].instance_eval(&callback) if @dams[dam_name].hits.any?
71
+ else
72
+ STDERR.puts "WARNING You have defined a dam for '#{dam_name}', but there is no hit defined for '#{dam_name}'"
73
+ end
67
74
  end
68
75
  end
69
76
 
data/lib/beaver/dam.rb CHANGED
@@ -6,11 +6,13 @@ module Beaver
6
6
  #
7
7
  # :path => String for exact match, or Regex
8
8
  #
9
- # :method => A Symbol of :get, :post, :update or :delete, or any array of any (reads the magic _method field if present)
9
+ # :method => A Symbol of :get, :post, :put or :delete, or any array of any (reads the magic _method field if present)
10
10
  #
11
11
  # :status => A Fixnum like 404 or a Range like (500..503)
12
12
  #
13
13
  # :ip => String for exact match, or Regex
14
+ #
15
+ # :format => A symbol or array of symbols of response formats like :html, :json
14
16
  #
15
17
  # :longer_than => Fixnum n. Matches any request which took longer than n milliseconds to complete.
16
18
  #
@@ -65,6 +67,8 @@ module Beaver
65
67
  return false unless @match_status.nil? or @match_status === request.status
66
68
  return false unless @match_ip_s.nil? or @match_ip_s == request.ip
67
69
  return false unless @match_ip_r.nil? or @match_ip_r =~ request.ip
70
+ return false unless @match_format_s.nil? or @match_format_s == request.format
71
+ return false unless @match_format_a.nil? or @match_format_a.include? request.format
68
72
  return false unless @match_before.nil? or @match_before > request.time
69
73
  return false unless @match_after.nil? or @match_after < request.time
70
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)
@@ -108,8 +112,8 @@ module Beaver
108
112
  end if matchers[:path]
109
113
 
110
114
  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]
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 }
113
117
  else raise ArgumentError, "Method must be a Symbol or an Array (it's a #{matchers[:method].class.name})"
114
118
  end if matchers[:method]
115
119
 
@@ -119,11 +123,17 @@ module Beaver
119
123
  end if matchers[:status]
120
124
 
121
125
  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]
126
+ when String.name then @match_ip_s = matchers[:ip]
127
+ when Regexp.name then @match_ip_r = matchers[:ip]
124
128
  else raise ArgumentError, "IP must be a String or a Regexp (it's a #{matchers[:ip].class.name})"
125
129
  end if matchers[:ip]
126
130
 
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 }
134
+ else raise ArgumentError, "Format must be a Symbol or an Array (it's a #{matchers[:format].class.name})"
135
+ end if matchers[:format]
136
+
127
137
  case matchers[:longer_than].class.name
128
138
  when Fixnum.name then @match_longer = matchers[:longer_than]
129
139
  else raise ArgumentError, "longer_than must be a Fixnum (it's a #{matchers[:longer_than].class.name})"
@@ -11,6 +11,7 @@ module Beaver
11
11
  REGEX_PATH = /^Started \w{3,4} "([^"]+)"/
12
12
  REGEX_PARAMS_STR = /^ Parameters: (\{.+\})$/
13
13
  REGEX_IP = /" for (\d+[\d.]+) at /
14
+ REGEX_FORMAT = /Processing by .+ as (\w+)$/
14
15
  REGEX_MS = / in (\d+)ms/
15
16
  # Depending on the version of Rails, the time format may be wildly different
16
17
  REGEX_TIME = / at ([a-z0-9:\+\- ]+)$/i
@@ -40,7 +41,7 @@ module Beaver
40
41
  def parse_method
41
42
  m = REGEX_METHOD_OVERRIDE.match(@lines)
42
43
  m = REGEX_METHOD.match(@lines) if m.nil?
43
- m ? m.captures.first.downcase.to_sym : :method
44
+ m ? m.captures.first.downcase.to_sym : :unknown
44
45
  end
45
46
 
46
47
  # Parses and returns the response status
@@ -67,6 +68,12 @@ module Beaver
67
68
  m ? m.captures.first : BLANK_STR
68
69
  end
69
70
 
71
+ # Parses and returns the respones format
72
+ def parse_format
73
+ m = REGEX_FORMAT.match(@lines)
74
+ m ? m.captures.first.to_s.downcase.to_sym : :unknown
75
+ end
76
+
70
77
  # Parses and returns the number of milliseconds it took for the request to complete
71
78
  def parse_ms
72
79
  m = REGEX_MS.match(@lines)
@@ -36,7 +36,7 @@ module Beaver
36
36
 
37
37
  # Append a log line
38
38
  def <<(line)
39
- @lines << line << $/
39
+ @lines << line
40
40
  end
41
41
 
42
42
  # Returns the request path
@@ -69,6 +69,11 @@ module Beaver
69
69
  @ip ||= parse_ip
70
70
  end
71
71
 
72
+ # Returns the responses format (html, json, etc)
73
+ def format
74
+ @format ||= parse_format
75
+ end
76
+
72
77
  # Returns the number of milliseconds it took for the request to complete
73
78
  def ms
74
79
  @ms ||= parse_ms
@@ -131,6 +136,11 @@ module Beaver
131
136
  BLANK_STR
132
137
  end
133
138
 
139
+ # Parses and returns the respones format
140
+ def parse_format
141
+ :format
142
+ end
143
+
134
144
  # Parses and returns the number of milliseconds it took for the request to complete
135
145
  def parse_ms
136
146
  0
metadata CHANGED
@@ -3,10 +3,10 @@ name: beaver
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
+ - 1
6
7
  - 0
7
8
  - 0
8
- - 1
9
- version: 0.0.1
9
+ version: 1.0.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Jordan Hollinger
@@ -14,11 +14,11 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-09-24 00:00:00 -04:00
17
+ date: 2011-12-03 00:00:00 -05:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
21
- description: A simple DSL for helping you discover what people are doing with your Rails app
21
+ description: A simple DSL and command-line tool for discovering what people are up to in your Rails app
22
22
  email: jordan@jordanhollinger.com
23
23
  executables:
24
24
  - beaver
@@ -67,6 +67,6 @@ rubyforge_project:
67
67
  rubygems_version: 1.3.7
68
68
  signing_key:
69
69
  specification_version: 3
70
- summary: Rails production log parser
70
+ summary: Rails log parser
71
71
  test_files: []
72
72