request-log-analyzer 1.1.2 → 1.2.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.
Files changed (58) hide show
  1. data/DESIGN +24 -10
  2. data/README.rdoc +1 -1
  3. data/Rakefile +1 -2
  4. data/bin/request-log-analyzer +4 -27
  5. data/lib/cli/progressbar.rb +2 -19
  6. data/lib/cli/tools.rb +46 -0
  7. data/lib/request_log_analyzer/aggregator/database.rb +33 -8
  8. data/lib/request_log_analyzer/aggregator/echo.rb +1 -0
  9. data/lib/request_log_analyzer/aggregator/summarizer.rb +15 -13
  10. data/lib/request_log_analyzer/controller.rb +8 -4
  11. data/lib/request_log_analyzer/file_format/merb.rb +17 -4
  12. data/lib/request_log_analyzer/file_format/rails.rb +2 -2
  13. data/lib/request_log_analyzer/file_format/rails_development.rb +30 -92
  14. data/lib/request_log_analyzer/file_format.rb +8 -4
  15. data/lib/request_log_analyzer/filter/anonymize.rb +0 -3
  16. data/lib/request_log_analyzer/filter/field.rb +6 -1
  17. data/lib/request_log_analyzer/filter/timespan.rb +7 -1
  18. data/lib/request_log_analyzer/filter.rb +0 -4
  19. data/lib/request_log_analyzer/line_definition.rb +12 -2
  20. data/lib/request_log_analyzer/output/fixed_width.rb +14 -3
  21. data/lib/request_log_analyzer/output/html.rb +1 -0
  22. data/lib/request_log_analyzer/request.rb +6 -0
  23. data/lib/request_log_analyzer/source/database.rb +76 -0
  24. data/lib/request_log_analyzer/source/log_parser.rb +101 -48
  25. data/lib/request_log_analyzer/source.rb +28 -6
  26. data/lib/request_log_analyzer/tracker/duration.rb +90 -15
  27. data/lib/request_log_analyzer/tracker/frequency.rb +22 -10
  28. data/lib/request_log_analyzer/tracker/hourly_spread.rb +20 -9
  29. data/lib/request_log_analyzer/tracker/timespan.rb +15 -8
  30. data/lib/request_log_analyzer.rb +29 -16
  31. data/spec/integration/command_line_usage_spec.rb +71 -0
  32. data/spec/lib/helper.rb +33 -0
  33. data/spec/lib/mocks.rb +47 -0
  34. data/spec/{file_formats/spec_format.rb → lib/testing_format.rb} +6 -1
  35. data/spec/spec_helper.rb +5 -41
  36. data/spec/{database_inserter_spec.rb → unit/aggregator/database_inserter_spec.rb} +40 -37
  37. data/spec/unit/aggregator/summarizer_spec.rb +28 -0
  38. data/spec/unit/controller/controller_spec.rb +43 -0
  39. data/spec/{log_processor_spec.rb → unit/controller/log_processor_spec.rb} +4 -3
  40. data/spec/{file_format_spec.rb → unit/file_format/file_format_api_spec.rb} +16 -4
  41. data/spec/{line_definition_spec.rb → unit/file_format/line_definition_spec.rb} +13 -6
  42. data/spec/{merb_format_spec.rb → unit/file_format/merb_format_spec.rb} +2 -2
  43. data/spec/{rails_format_spec.rb → unit/file_format/rails_format_spec.rb} +19 -11
  44. data/spec/unit/filter/anonymize_filter_spec.rb +22 -0
  45. data/spec/unit/filter/field_filter_spec.rb +69 -0
  46. data/spec/unit/filter/timespan_filter_spec.rb +61 -0
  47. data/spec/{log_parser_spec.rb → unit/source/log_parser_spec.rb} +7 -7
  48. data/spec/{request_spec.rb → unit/source/request_spec.rb} +5 -5
  49. data/spec/unit/tracker/duration_tracker_spec.rb +99 -0
  50. data/spec/unit/tracker/frequency_tracker_spec.rb +83 -0
  51. data/spec/unit/tracker/hourly_spread_spec.rb +75 -0
  52. data/spec/unit/tracker/timespan_tracker_spec.rb +65 -0
  53. data/spec/unit/tracker/tracker_api_test.rb +45 -0
  54. data/tasks/rspec.rake +12 -0
  55. metadata +54 -26
  56. data/spec/controller_spec.rb +0 -64
  57. data/spec/filter_spec.rb +0 -157
  58. data/spec/summarizer_spec.rb +0 -9
data/DESIGN CHANGED
@@ -4,21 +4,35 @@ This allows you to easily add extra reports, filters and outputs.
4
4
 
5
5
  1) Build pipeline.
6
6
  -> Aggregator (database)
7
- Source -> Filter -> Filter -> Aggregator (summary report)
7
+ Source -> Filter -> Filter -> Aggregator (summary report) -> Output
8
8
  -> Aggregator (...)
9
-
9
+
10
10
  2) Start chunk producer and push chunks through pipeline.
11
- Controller.start
11
+ Controller.start
12
+
13
+ RequestLogAnalyzer::Source is an Object that pushes requests into the chain.
14
+ At the moment you can only use the log-parser as a source.
15
+ It accepts files or stdin and can parse then into request objects using a RequestLogAnalyzer::FileFormat definition.
16
+ In the future we want to be able to have a generated request database as source as this will make interactive
17
+ down drilling possible.
18
+
19
+ The filters are all subclasses of the RequestLogAnalyzer::Filter class.
20
+ They accept a request object, manipulate or drop it, and then pass the request object on to the next filter
21
+ in the chain.
22
+ At the moment there are three types of filters available: Anonymize, Field and Timespan.
23
+
24
+ The Aggregators all inherit from the RequestLogAnalyzer::Aggregator class.
25
+ All the requests that come out of the Filterchain are fed into all the aggregators in parallel.
26
+ These aggregators can do anything what they want with the given request.
27
+ For example: the Database aggregator will just store all the requests into a SQLite database while the Summarizer will
28
+ generate a wide range of statistical reports from them.
12
29
 
13
30
  3) Gather output from pipeline.
14
31
  Controller.report
15
32
 
16
- At the moment the supported sources are file and STDIN.
17
- In the future we want to be able to have a generated request database as source.
18
- This will make interactive downdrilling possible.
19
-
33
+ All Aggregators are asked to report what they have done. For example the database will report: I stuffed x requests
34
+ into SQLite database Y. The Summarizer will output its reports.
20
35
 
21
- For the report generation output we now use the File and the STDOUT class, as they both support <<.
22
- In the future we want to have a OutputFile, OutputSTDOUT and OutputHTML class, so that reports can generate
23
- tables, lines and comments and push them into the output class.
36
+ The output is pushed to a RequestLogAnalyzer::Output object, which takes care of the output.
37
+ It can generate either ASCII, UTF8 or even HTML output.
24
38
 
data/README.rdoc CHANGED
@@ -3,7 +3,7 @@
3
3
  This is a simple command line tool to analyze request log files of both Rails and
4
4
  Merb to produce a performance report. Its purpose is to find what actions are best candidates for optimization.
5
5
 
6
- * Analyzes Rails log files (all versions)
6
+ * Analyzes Rails log files (all versions), Merb logs, or any other log format
7
7
  * Can combine multiple files (handy if you are using logrotate)
8
8
  * Uses several metrics, including cumulative request time, average request time, process blockers, database and rendering time, HTTP methods and statuses, Rails action cache statistics, etc.) (Sample output: http://wiki.github.com/wvanbergen/request-log-analyzer/sample-output)
9
9
  * Low memory footprint (server-safe)
data/Rakefile CHANGED
@@ -1,4 +1,3 @@
1
1
  Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
2
2
 
3
- task :default => :spec
4
-
3
+ task :default => :spec
@@ -1,15 +1,8 @@
1
1
  #!/usr/bin/ruby
2
2
  require File.dirname(__FILE__) + '/../lib/request_log_analyzer'
3
3
  require File.dirname(__FILE__) + '/../lib/cli/command_line_arguments'
4
-
5
- def terminal_width(default = 81)
6
- IO.popen('stty -a') do |pipe|
7
- column_line = pipe.detect { |line| /(\d+) columns/ =~ line }
8
- width = column_line ? $1.to_i : default
9
- end
10
- rescue
11
- default
12
- end
4
+ require File.dirname(__FILE__) + '/../lib/cli/progressbar'
5
+ require File.dirname(__FILE__) + '/../lib/cli/tools'
13
6
 
14
7
  # Parse the arguments given via commandline
15
8
  begin
@@ -29,7 +22,7 @@ begin
29
22
 
30
23
  command_line.option(:format, :alias => :f, :default => 'rails')
31
24
  command_line.option(:file, :alias => :e)
32
- command_line.switch(:assume_correct_order)
25
+ command_line.option(:parse_strategy, :default => 'assume-correct')
33
26
 
34
27
  command_line.option(:aggregator, :alias => :a, :multiple => true)
35
28
  command_line.option(:database, :alias => :d)
@@ -50,6 +43,7 @@ begin
50
43
  end
51
44
 
52
45
  rescue CommandLine::Error => e
46
+ puts "Request-log-analyzer, by Willem van Bergen and Bart ten Brinke - version #{RequestLogAnalyzer::VERSION}"
53
47
  puts "ARGUMENT ERROR: " + e.message if e.message
54
48
  puts
55
49
  puts "Usage: request-log-analyzer [LOGFILES*] <OPTIONS>"
@@ -80,23 +74,6 @@ rescue CommandLine::Error => e
80
74
  exit(0)
81
75
  end
82
76
 
83
- def install_rake_tasks(install_type)
84
- if install_type == 'rails'
85
- require 'ftools'
86
- if File.directory?('./lib/tasks/')
87
- File.copy(File.dirname(__FILE__) + '/../tasks/request_log_analyzer.rake', './lib/tasks/request_log_analyze.rake')
88
- puts "Installed rake tasks."
89
- puts "To use, run: rake log:analyze"
90
- else
91
- puts "Cannot find /lib/tasks folder. Are you in your Rails directory?"
92
- puts "Installation aborted."
93
- end
94
- else
95
- raise "Cannot perform this install type! (#{install_type})"
96
- end
97
- end
98
-
99
-
100
77
  case arguments.command
101
78
  when :install
102
79
  install_rake_tasks(arguments.parameters[0])
@@ -119,23 +119,6 @@ module CommandLine
119
119
  end
120
120
  end
121
121
 
122
- def get_width
123
- # FIXME: I don't know how portable it is.
124
- default_width = 80
125
- begin
126
- tiocgwinsz = 0x5413
127
- data = [0, 0, 0, 0].pack("SSSS")
128
- if @out.ioctl(tiocgwinsz, data) >= 0 then
129
- rows, cols, xpixels, ypixels = data.unpack("SSSS")
130
- if cols >= 0 then cols else default_width end
131
- else
132
- default_width
133
- end
134
- rescue Exception
135
- default_width
136
- end
137
- end
138
-
139
122
  def show
140
123
  arguments = @format_arguments.map {|method|
141
124
  method = sprintf("fmt_%s", method)
@@ -143,7 +126,7 @@ module CommandLine
143
126
  }
144
127
  line = sprintf(@format, *arguments)
145
128
 
146
- width = get_width
129
+ width = terminal_width(80)
147
130
  if line.length == width - 1
148
131
  @out.print(line + eol)
149
132
  @out.flush
@@ -176,7 +159,7 @@ module CommandLine
176
159
  public
177
160
  def clear
178
161
  @out.print "\r"
179
- @out.print(" " * (get_width - 1))
162
+ @out.print(" " * (terminal_width(80) - 1))
180
163
  @out.print "\r"
181
164
  end
182
165
 
data/lib/cli/tools.rb ADDED
@@ -0,0 +1,46 @@
1
+ # Try to determine the terminal with.
2
+ # If it is not possible to to so, it returns the default_width.
3
+ # <tt>default_width</tt> Defaults to 81
4
+ def terminal_width(default_width = 81)
5
+ tiocgwinsz = 0x5413
6
+ data = [0, 0, 0, 0].pack("SSSS")
7
+ if @out.ioctl(tiocgwinsz, data) >= 0
8
+ rows, cols, xpixels, ypixels = data.unpack("SSSS")
9
+ raise unless cols > 0
10
+ cols
11
+ else
12
+ raise
13
+ end
14
+ rescue
15
+ begin
16
+ IO.popen('stty -a') do |pipe|
17
+ column_line = pipe.detect { |line| /(\d+) columns/ =~ line }
18
+ raise unless column_line
19
+ $1.to_i
20
+ end
21
+ rescue
22
+ default_width
23
+ end
24
+ end
25
+
26
+ # Copies request-log-analyzer analyzer rake tasks into the /lib/tasks folder of a project, for easy access and
27
+ # environment integration.
28
+ # <tt>install_type</tt> Type of project to install into. Defaults to :rails.
29
+ # Raises if it cannot find the project folder or if the install_type is now known.
30
+ def install_rake_tasks(install_type = :rails)
31
+ if install_type.to_sym == :rails
32
+ require 'ftools'
33
+ if File.directory?('./lib/tasks/')
34
+ File.copy(File.dirname(__FILE__) + '/../tasks/request_log_analyzer.rake', './lib/tasks/request_log_analyze.rake')
35
+ puts "Installed rake tasks."
36
+ puts "To use, run: rake log:analyze"
37
+ else
38
+ puts "Cannot find /lib/tasks folder. Are you in your Rails directory?"
39
+ puts "Installation aborted."
40
+ end
41
+ else
42
+ raise "Cannot perform this install type! (#{install_type.to_s})"
43
+ end
44
+ end
45
+
46
+
@@ -30,7 +30,8 @@ module RequestLogAnalyzer::Aggregator
30
30
  def aggregate(request)
31
31
  @request_object = @request_class.new(:first_lineno => request.first_lineno, :last_lineno => request.last_lineno)
32
32
  request.lines.each do |line|
33
- attributes = line.reject { |k, v| [:line_type].include?(k) }
33
+ class_columns = @orm_module.const_get("#{line[:line_type]}_line".classify).column_names.reject { |column| ['id'].include?(column) }
34
+ attributes = Hash[*line.select { |(k, v)| class_columns.include?(k.to_s) }.flatten]
34
35
  @request_object.send("#{line[:line_type]}_lines").build(attributes)
35
36
  end
36
37
  @request_object.save!
@@ -72,7 +73,16 @@ module RequestLogAnalyzer::Aggregator
72
73
  t.column(:request_id, :integer)
73
74
  t.column(:lineno, :integer)
74
75
  definition.captures.each do |capture|
75
- t.column(capture[:name], column_type(capture))
76
+
77
+ # Add a field for every capture
78
+ t.column(capture[:name], column_type(capture[:type]))
79
+
80
+ # If the capture provides other field as well, create them too
81
+ if capture[:provides].kind_of?(Hash)
82
+ capture[:provides].each do |field, field_type|
83
+ t.column(field, column_type(field_type))
84
+ end
85
+ end
76
86
  end
77
87
  end
78
88
  ActiveRecord::Migration.add_index("#{name}_lines", [:request_id])
@@ -86,6 +96,11 @@ module RequestLogAnalyzer::Aggregator
86
96
  class_name = "#{name}_line".camelize
87
97
  klass = Class.new(ActiveRecord::Base)
88
98
  klass.send(:belongs_to, :request)
99
+
100
+ definition.captures.each do |capture|
101
+ klass.send(:serialize, capture[:name], Hash) if capture[:provides]
102
+ end
103
+
89
104
  @orm_module.const_set(class_name, klass) unless @orm_module.const_defined?(class_name)
90
105
  @request_class.send(:has_many, "#{name}_lines".to_sym)
91
106
  end
@@ -136,12 +151,22 @@ module RequestLogAnalyzer::Aggregator
136
151
 
137
152
  # Function to determine the column type for a field
138
153
  # TODO: make more robust / include in file-format definition
139
- def column_type(capture)
140
- case capture[:type]
141
- when :sec; :double
142
- when :msec; :double
143
- when :float; :double
144
- else capture[:type]
154
+ def column_type(type_indicator)
155
+ case type_indicator
156
+ when :eval; :text
157
+ when :text; :text
158
+ when :string; :string
159
+ when :sec; :double
160
+ when :msec; :double
161
+ when :duration; :double
162
+ when :float; :double
163
+ when :double; :double
164
+ when :integer; :integer
165
+ when :int; :int
166
+ when :timestamp; :datetime
167
+ when :datetime; :datetime
168
+ when :date; :date
169
+ else :string
145
170
  end
146
171
  end
147
172
  end
@@ -1,5 +1,6 @@
1
1
  module RequestLogAnalyzer::Aggregator
2
2
 
3
+ # Echo Aggregator. Writes everything passed to it
3
4
  class Echo < Base
4
5
 
5
6
  def prepare
@@ -1,5 +1,3 @@
1
- require File.dirname(__FILE__) + '/../tracker'
2
-
3
1
  module RequestLogAnalyzer::Aggregator
4
2
 
5
3
  class Summarizer < Base
@@ -11,6 +9,10 @@ module RequestLogAnalyzer::Aggregator
11
9
  def initialize
12
10
  @trackers = []
13
11
  end
12
+
13
+ def initialize_copy(other)
14
+ @trackers = other.trackers.dup
15
+ end
14
16
 
15
17
  def reset!
16
18
  @trackers = []
@@ -58,7 +60,7 @@ module RequestLogAnalyzer::Aggregator
58
60
  def prepare
59
61
  raise "No trackers set up in Summarizer!" if @trackers.nil? || @trackers.empty?
60
62
  @trackers.each { |tracker| tracker.prepare }
61
- end
63
+ end
62
64
 
63
65
  def aggregate(request)
64
66
  @trackers.each do |tracker|
@@ -86,23 +88,23 @@ module RequestLogAnalyzer::Aggregator
86
88
 
87
89
  output.with_style(:cell_separator => false) do
88
90
  output.table({:width => 20}, {:font => :bold}) do |rows|
89
- rows << ['Parsed lines:', source.parsed_lines]
90
- rows << ['Parsed request:', source.parsed_requests]
91
- rows << ['Skipped lines:', source.skipped_lines]
91
+ rows << ['Parsed lines:', source.parsed_lines]
92
+ rows << ['Parsed requests:', source.parsed_requests]
93
+ rows << ['Skipped lines:', source.skipped_lines]
92
94
 
93
- rows << ["Warnings:", @warnings_encountered.map { |(key, value)| "#{key.inspect}: #{value}" }.join(', ')] if has_warnings?
95
+ rows << ["Warnings:", @warnings_encountered.map { |(key, value)| "#{key}: #{value}" }.join(', ')] if has_warnings?
94
96
  end
95
97
  end
96
98
  output << "\n"
97
99
  end
98
100
 
99
101
  def report_footer(output)
100
- if has_serious_warnings?
101
-
102
+ if has_log_ordering_warnings?
102
103
  output.title("Parse warnings")
103
104
 
104
- output.puts "Multiple warnings were encountered during parsing. Possibly, your logging "
105
- output.puts "is not setup correctly. Visit this website for logging configuration tips:"
105
+ output.puts "Parseable lines were ancountered without a header line before it. It"
106
+ output.puts "could be that logging is not setup correctly for your application."
107
+ output.puts "Visit this website for logging configuration tips:"
106
108
  output.puts output.link("http://github.com/wvanbergen/request-log-analyzer/wikis/configure-logging")
107
109
  output.puts
108
110
  end
@@ -112,8 +114,8 @@ module RequestLogAnalyzer::Aggregator
112
114
  @warnings_encountered.inject(0) { |result, (key, value)| result += value } > 0
113
115
  end
114
116
 
115
- def has_serious_warnings?
116
- @warnings_encountered.inject(0) { |result, (key, value)| result += value } > 10
117
+ def has_log_ordering_warnings?
118
+ @warnings_encountered[:no_current_request] && @warnings_encountered[:no_current_request] > 0
117
119
  end
118
120
 
119
121
  def warning(type, message, lineno)
@@ -64,7 +64,7 @@ module RequestLogAnalyzer
64
64
 
65
65
  controller = Controller.new(RequestLogAnalyzer::Source::LogParser.new(file_format, options), options)
66
66
 
67
- options[:assume_correct_order] = arguments[:assume_correct_order]
67
+ options[:parse_strategy] = arguments[:parse_strategy]
68
68
 
69
69
  # register filters
70
70
  if arguments[:after] || arguments[:before]
@@ -160,6 +160,9 @@ module RequestLogAnalyzer
160
160
  @filters << filter.new(file_format, @options.merge(filter_options))
161
161
  end
162
162
 
163
+ # Push a request through the entire filterchain (@filters).
164
+ # <tt>request</tt> The request to filter.
165
+ # Returns the filtered request or nil.
163
166
  def filter_request(request)
164
167
  @filters.each do |filter|
165
168
  request = filter.filter(request)
@@ -168,7 +171,10 @@ module RequestLogAnalyzer
168
171
  return request
169
172
  end
170
173
 
174
+ # Push a request to all the aggregators (@aggregators).
175
+ # <tt>request</tt> The request to push to the aggregators.
171
176
  def aggregate_request(request)
177
+ return unless request
172
178
  @aggregators.each { |agg| agg.aggregate(request) }
173
179
  end
174
180
 
@@ -182,13 +188,11 @@ module RequestLogAnalyzer
182
188
  # 6. Finalize Source
183
189
  def run!
184
190
 
185
- @filters.each { |filter| filter.prepare }
186
191
  @aggregators.each { |agg| agg.prepare }
187
192
 
188
193
  begin
189
194
  @source.each_request do |request|
190
- request = filter_request(request)
191
- aggregate_request(request) unless request.nil?
195
+ aggregate_request(filter_request(request))
192
196
  end
193
197
  rescue Interrupt => e
194
198
  handle_progress(:interrupted)
@@ -14,8 +14,9 @@ module RequestLogAnalyzer::FileFormat
14
14
  # ~ Params: {"_method"=>"delete", "authenticity_token"=>"[FILTERED]", "action"=>"d}
15
15
  line_definition :params do |line|
16
16
  line.teaser = /Params/
17
- line.regexp = /Params\:\ \{(.+)\}/
18
- line.captures << { :name => :raw_hash, :type => :string}
17
+ line.regexp = /Params\:\ (\{.+\})/
18
+ line.captures << { :name => :params, :type => :eval, :provides => {
19
+ :namespace => :string, :controller => :string, :action => :string, :format => :string } }
19
20
  end
20
21
 
21
22
  # ~ {:dispatch_time=>0.006117, :after_filters_time=>6.1e-05, :before_filters_time=>0.000712, :action_time=>0.005833}
@@ -29,11 +30,23 @@ module RequestLogAnalyzer::FileFormat
29
30
  << { :name => :action_time, :type => :duration }
30
31
  end
31
32
 
33
+ REQUEST_CATEGORIZER = Proc.new do |request|
34
+ category = "#{request[:controller]}##{request[:action]}"
35
+ category = "#{request[:namespace]}::#{category}" if request[:namespace]
36
+ category = "#{category}.#{request[:format]}" if request[:format]
37
+ category
38
+ end
32
39
 
33
40
  report do |analyze|
34
- # FIX ME
41
+ analyze.timespan :line_type => :started
42
+ analyze.frequency :category => REQUEST_CATEGORIZER, :amount => 20, :title => "Top 20 by hits"
43
+ analyze.hourly_spread :line_type => :started
44
+ analyze.duration :dispatch_time, :category => REQUEST_CATEGORIZER, :title => 'Request dispatch duration'
45
+ # analyze.duration :action_time, :category => REQUEST_CATEGORIZER, :title => 'Request action duration'
46
+ # analyze.duration :after_filters_time, :category => REQUEST_CATEGORIZER, :title => 'Request after_filter duration'
47
+ # analyze.duration :before_filters_time, :category => REQUEST_CATEGORIZER, :title => 'Request before_filter duration'
35
48
  end
36
-
49
+
37
50
  end
38
51
 
39
52
  end
@@ -76,8 +76,8 @@ module RequestLogAnalyzer::FileFormat
76
76
  analyze.frequency :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
77
77
 
78
78
  analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
79
- analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
80
- analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
79
+ analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
80
+ analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
81
81
 
82
82
  analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
83
83
  :if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
@@ -1,28 +1,19 @@
1
1
  module RequestLogAnalyzer::FileFormat
2
2
 
3
- class RailsDevelopment < Base
3
+ # The RailsDevelopment FileFormat is an extention to the default Rails file format. It includes
4
+ # all lines of the normal Rails file format, but parses SQL queries and partial rendering lines
5
+ # as well.
6
+ class RailsDevelopment < Rails
4
7
 
5
- # Processing EmployeeController#index (for 123.123.123.123 at 2008-07-13 06:00:00) [GET]
6
- line_definition :processing do |line|
7
- line.header = true # this line is the first log line for a request
8
- line.teaser = /Processing /
9
- line.regexp = /Processing ((?:\w+::)?\w+)#(\w+)(?: to (\w+))? \(for (\d+\.\d+\.\d+\.\d+) at (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\) \[([A-Z]+)\]/
10
- line.captures << { :name => :controller, :type => :string } \
11
- << { :name => :action, :type => :string } \
12
- << { :name => :format, :type => :string } \
13
- << { :name => :ip, :type => :string } \
14
- << { :name => :timestamp, :type => :timestamp } \
15
- << { :name => :method, :type => :string }
16
- end
17
-
18
- # Filter chain halted as [#<ActionController::Caching::Actions::ActionCacheFilter:0x2a999ad620 @check=nil, @options={:store_options=>{}, :layout=>nil, :cache_path=>#<Proc:0x0000002a999b8890@/app/controllers/cached_controller.rb:8>}>] rendered_or_redirected.
19
- line_definition :cache_hit do |line|
20
- line.regexp = /Filter chain halted as \[\#<ActionController::Caching::Actions::ActionCacheFilter:.+>\] rendered_or_redirected/
8
+ # Parameters: {"action"=>"demo", "controller"=>"page"}
9
+ line_definition :parameters do |line|
10
+ line.teaser = /Parameters/
11
+ line.regexp = /\s+Parameters:\s+(\{.*\})/
12
+ line.captures << { :name => :params, :type => :eval }
21
13
  end
22
14
 
23
15
  # Rendered layouts/_footer (2.9ms)
24
16
  line_definition :rendered do |line|
25
- line.teaser = /Rendered /
26
17
  line.regexp = /Rendered (\w+(?:\/\w+)+) \((\d+\.\d+)ms\)/
27
18
  line.captures << { :name => :render_file, :type => :string } \
28
19
  << { :name => :render_duration, :type => :duration, :unit => :msec }
@@ -30,90 +21,37 @@ module RequestLogAnalyzer::FileFormat
30
21
 
31
22
  # User Load (0.4ms) SELECT * FROM `users` WHERE (`users`.`id` = 18205844) 
32
23
  line_definition :query_executed do |line|
33
- line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?(.+) (?:\e\[0m)?/
24
+ line.regexp = /\s+(?:\e\[4;36;1m)?((?:\w+::)*\w+) Load \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0;1m)?([^\e]+) ?(?:\e\[0m)?/
34
25
  line.captures << { :name => :query_class, :type => :string } \
35
26
  << { :name => :query_duration, :type => :duration, :unit => :msec } \
36
- << { :name => :query_sql, :type => :string }
27
+ << { :name => :query_sql, :type => :sql }
37
28
  end
38
29
 
39
30
  # CACHE (0.0ms) SELECT * FROM `users` WHERE (`users`.`id` = 0) 
40
31
  line_definition :query_cached do |line|
41
- line.teaser = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
42
- line.regexp = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?(.+) (?:\e\[0m)?/
32
+ line.regexp = /\s+(?:\e\[4;35;1m)?CACHE \((\d+\.\d+)ms\)(?:\e\[0m)?\s+(?:\e\[0m)?([^\e]+) ?(?:\e\[0m)?/
43
33
  line.captures << { :name => :cached_duration, :type => :duration, :unit => :msec } \
44
- << { :name => :cached_sql, :type => :string }
34
+ << { :name => :cached_sql, :type => :sql }
45
35
  end
46
-
47
- # RuntimeError (Cannot destroy employee): /app/models/employee.rb:198:in `before_destroy'
48
- line_definition :failed do |line|
49
- line.footer = true
50
- line.regexp = /((?:[A-Z]\w+\:\:)*[A-Z]\w+) \((.*)\)(?: on line #(\d+) of .+)?\:(.*)/
51
- line.captures << { :name => :error, :type => :string } \
52
- << { :name => :message, :type => :string } \
53
- << { :name => :line, :type => :integer } \
54
- << { :name => :file, :type => :string } \
55
- << { :name => :stack_trace, :type => :string }
56
- end
57
-
58
36
 
59
- # Rails < 2.1 completed line example
60
- # Completed in 0.21665 (4 reqs/sec) | Rendering: 0.00926 (4%) | DB: 0.00000 (0%) | 200 OK [http://demo.nu/employees]
61
- RAILS_21_COMPLETED = /Completed in (\d+\.\d{5}) \(\d+ reqs\/sec\) (?:\| Rendering: (\d+\.\d{5}) \(\d+\%\) )?(?:\| DB: (\d+\.\d{5}) \(\d+\%\) )?\| (\d\d\d).+\[(http.+)\]/
62
-
63
- # Rails > 2.1 completed line example
64
- # Completed in 614ms (View: 120, DB: 31) | 200 OK [http://floorplanner.local/demo]
65
- RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+), )?DB: (\d+)\) \| (\d\d\d).+\[(http.+)\]/
66
-
67
- # The completed line uses a kind of hack to ensure that both old style logs and new style logs
68
- # are both parsed by the same regular expression. The format in Rails 2.2 was slightly changed,
69
- # but the line contains exactly the same information.
70
- line_definition :completed do |line|
71
-
72
- line.footer = true
73
- line.teaser = /Completed in /
74
- line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
75
-
76
- line.captures << { :name => :duration, :type => :duration } \
77
- << { :name => :view, :type => :duration } \
78
- << { :name => :db, :type => :duration } \
79
- << { :name => :status, :type => :integer } \
80
- << { :name => :url, :type => :string } # Old variant
81
-
82
- line.captures << { :name => :duration, :type => :duration, :unit => :msec } \
83
- << { :name => :view, :type => :duration, :unit => :msec } \
84
- << { :name => :db, :type => :duration, :unit => :msec } \
85
- << { :name => :status, :type => :integer} \
86
- << { :name => :url, :type => :string } # 2.2 variant
87
- end
88
-
89
- REQUEST_CATEGORIZER = Proc.new do |request|
90
- format = request[:format] || 'html'
91
- "#{request[:controller]}##{request[:action]}.#{format} [#{request[:method]}]"
92
- end
93
-
94
- report do |analyze|
95
- analyze.timespan :line_type => :processing
96
- analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
97
- analyze.frequency :method, :title => 'HTTP methods'
98
- analyze.frequency :status, :title => 'HTTP statuses returned'
99
- analyze.frequency :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
100
-
101
- analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
102
- analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
103
- analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
104
-
105
- analyze.frequency :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
106
- :if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
107
-
108
- analyze.hourly_spread :line_type => :processing
109
- analyze.frequency :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
110
- end
37
+ # Define the reporting for the additional parsed lines
38
+ report(:append) do |analyze|
39
+
40
+ analyze.duration :render_duration, :category => :render_file, :multiple_per_request => true,
41
+ :amount => 20, :title => 'Partial rendering duration'
42
+
43
+ analyze.duration :query_duration, :category => :query_sql, :multiple_per_request => true,
44
+ :amount => 20, :title => 'Query duration'
45
+
46
+ end
111
47
 
112
- class Request < RequestLogAnalyzer::Request
113
-
114
- def convert_timestamp(value, definition)
115
- value.gsub(/[^0-9]/)[0...14].to_i unless value.nil?
48
+ # Add a converter method for the SQL fields the the Rails request class
49
+ class Request < Rails::Request
50
+
51
+ # Sanitizes SQL queries so that they can be grouped
52
+ def convert_sql(sql, definition)
53
+ sql.gsub(/\b\d+\b/, ':int').gsub(/`([^`]+)`/, '\1').gsub(/'[^']*'/, ':string').rstrip
116
54
  end
117
- end
55
+ end
118
56
  end
119
57
  end
@@ -80,8 +80,8 @@ module RequestLogAnalyzer::FileFormat
80
80
 
81
81
  # Copy the line and report definer from the parent class.
82
82
  subclass.class_eval do
83
- instance_variable_set(:@line_definer, superclass.line_definer)
84
- instance_variable_set(:@report_definer, superclass.report_definer)
83
+ instance_variable_set(:@line_definer, superclass.line_definer.clone)
84
+ instance_variable_set(:@report_definer, superclass.report_definer.clone)
85
85
  class << self; attr_accessor :line_definer, :report_definer; end
86
86
  end
87
87
 
@@ -95,8 +95,12 @@ module RequestLogAnalyzer::FileFormat
95
95
  @line_definer.send(name, &block)
96
96
  end
97
97
 
98
- def create_request(*hashes)
99
- self.class::Request.create(self, *hashes)
98
+ def request_class
99
+ self.class::Request
100
+ end
101
+
102
+ def request(*hashes)
103
+ request_class.create(self, *hashes)
100
104
  end
101
105
 
102
106
  # Specifies multiple line definitions at once using a block
@@ -7,9 +7,6 @@ module RequestLogAnalyzer::Filter
7
7
  # * <tt>:value</tt> Value that the field should match to be accepted or rejected.
8
8
  class Anonymize < Base
9
9
 
10
- def prepare
11
- end
12
-
13
10
  def generate_random_ip
14
11
  "#{rand(256)}.#{rand(256)}.#{rand(256)}.#{rand(256)}"
15
12
  end
@@ -9,8 +9,13 @@ module RequestLogAnalyzer::Filter
9
9
 
10
10
  attr_reader :field, :value, :mode
11
11
 
12
+ def initialize(file_format, options = {})
13
+ super(file_format, options)
14
+ setup_filter
15
+ end
16
+
12
17
  # Setup mode, field and value.
13
- def prepare
18
+ def setup_filter
14
19
  @mode = (@options[:mode] || :accept).to_sym
15
20
  @field = @options[:field].to_sym
16
21