beaver 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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