request-log-analyzer 1.1.2 → 1.1.3

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 (52) hide show
  1. data/DESIGN +24 -10
  2. data/bin/request-log-analyzer +2 -27
  3. data/lib/cli/progressbar.rb +2 -19
  4. data/lib/cli/tools.rb +46 -0
  5. data/lib/request_log_analyzer/aggregator/database.rb +16 -5
  6. data/lib/request_log_analyzer/aggregator/echo.rb +1 -0
  7. data/lib/request_log_analyzer/aggregator/summarizer.rb +15 -13
  8. data/lib/request_log_analyzer/controller.rb +8 -4
  9. data/lib/request_log_analyzer/file_format/merb.rb +17 -4
  10. data/lib/request_log_analyzer/file_format/rails_development.rb +30 -92
  11. data/lib/request_log_analyzer/file_format.rb +8 -4
  12. data/lib/request_log_analyzer/filter/anonymize.rb +0 -3
  13. data/lib/request_log_analyzer/filter/field.rb +6 -1
  14. data/lib/request_log_analyzer/filter/timespan.rb +7 -1
  15. data/lib/request_log_analyzer/filter.rb +0 -4
  16. data/lib/request_log_analyzer/line_definition.rb +13 -2
  17. data/lib/request_log_analyzer/output/fixed_width.rb +7 -1
  18. data/lib/request_log_analyzer/output/html.rb +1 -0
  19. data/lib/request_log_analyzer/request.rb +4 -0
  20. data/lib/request_log_analyzer/source/log_parser.rb +19 -21
  21. data/lib/request_log_analyzer/source.rb +2 -1
  22. data/lib/request_log_analyzer/tracker/duration.rb +74 -14
  23. data/lib/request_log_analyzer/tracker/frequency.rb +22 -10
  24. data/lib/request_log_analyzer/tracker/hourly_spread.rb +18 -6
  25. data/lib/request_log_analyzer/tracker/timespan.rb +15 -8
  26. data/lib/request_log_analyzer.rb +4 -8
  27. data/spec/integration/command_line_usage_spec.rb +71 -0
  28. data/spec/lib/helper.rb +33 -0
  29. data/spec/lib/mocks.rb +47 -0
  30. data/spec/{file_formats/spec_format.rb → lib/testing_format.rb} +6 -1
  31. data/spec/spec_helper.rb +5 -41
  32. data/spec/{database_inserter_spec.rb → unit/aggregator/database_inserter_spec.rb} +40 -37
  33. data/spec/unit/aggregator/summarizer_spec.rb +28 -0
  34. data/spec/unit/controller/controller_spec.rb +43 -0
  35. data/spec/{log_processor_spec.rb → unit/controller/log_processor_spec.rb} +4 -3
  36. data/spec/{file_format_spec.rb → unit/file_format/file_format_api_spec.rb} +16 -4
  37. data/spec/{line_definition_spec.rb → unit/file_format/line_definition_spec.rb} +13 -6
  38. data/spec/{merb_format_spec.rb → unit/file_format/merb_format_spec.rb} +2 -2
  39. data/spec/{rails_format_spec.rb → unit/file_format/rails_format_spec.rb} +19 -11
  40. data/spec/unit/filter/anonymize_filter_spec.rb +22 -0
  41. data/spec/unit/filter/field_filter_spec.rb +69 -0
  42. data/spec/unit/filter/timespan_filter_spec.rb +61 -0
  43. data/spec/{log_parser_spec.rb → unit/source/log_parser_spec.rb} +7 -7
  44. data/spec/{request_spec.rb → unit/source/request_spec.rb} +5 -5
  45. data/spec/unit/tracker/duration_tracker_spec.rb +90 -0
  46. data/spec/unit/tracker/frequency_tracker_spec.rb +83 -0
  47. data/spec/unit/tracker/timespan_tracker_spec.rb +64 -0
  48. data/spec/unit/tracker/tracker_api_test.rb +45 -0
  49. metadata +50 -26
  50. data/spec/controller_spec.rb +0 -64
  51. data/spec/filter_spec.rb +0 -157
  52. 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
 
@@ -1,15 +1,7 @@
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/tools'
13
5
 
14
6
  # Parse the arguments given via commandline
15
7
  begin
@@ -29,7 +21,7 @@ begin
29
21
 
30
22
  command_line.option(:format, :alias => :f, :default => 'rails')
31
23
  command_line.option(:file, :alias => :e)
32
- command_line.switch(:assume_correct_order)
24
+ command_line.option(:parse_strategy, :default => 'assume-correct')
33
25
 
34
26
  command_line.option(:aggregator, :alias => :a, :multiple => true)
35
27
  command_line.option(:database, :alias => :d)
@@ -80,23 +72,6 @@ rescue CommandLine::Error => e
80
72
  exit(0)
81
73
  end
82
74
 
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
75
  case arguments.command
101
76
  when :install
102
77
  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])
@@ -136,12 +146,13 @@ module RequestLogAnalyzer::Aggregator
136
146
 
137
147
  # Function to determine the column type for a field
138
148
  # TODO: make more robust / include in file-format definition
139
- def column_type(capture)
140
- case capture[:type]
149
+ def column_type(type_indicator)
150
+ case type_indicator
151
+ when :eval; :text
141
152
  when :sec; :double
142
153
  when :msec; :double
143
154
  when :float; :double
144
- else capture[:type]
155
+ else type_indicator
145
156
  end
146
157
  end
147
158
  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.hourly_spread :line_type => :started
43
+
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
@@ -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
 
@@ -8,9 +8,15 @@ module RequestLogAnalyzer::Filter
8
8
 
9
9
  attr_reader :before, :after
10
10
 
11
+ def initialize(file_format, options = {})
12
+ super(file_format, options)
13
+ setup_filter
14
+ end
15
+
16
+
11
17
  # Convert the timestamp to the correct formats for quick timestamp comparisons.
12
18
  # These are stored in the before and after attr_reader fields.
13
- def prepare
19
+ def setup_filter
14
20
  @after = @options[:after].strftime('%Y%m%d%H%M%S').to_i if options[:after]
15
21
  @before = @options[:before].strftime('%Y%m%d%H%M%S').to_i if options[:before]
16
22
  end
@@ -23,10 +23,6 @@ module RequestLogAnalyzer::Filter
23
23
  register_file_format(format)
24
24
  end
25
25
 
26
- # Initialize the filter
27
- def prepare
28
- end
29
-
30
26
  # Return the request if the request should be kept.
31
27
  # Return nil otherwise.
32
28
  def filter(request)
@@ -12,6 +12,10 @@ module RequestLogAnalyzer
12
12
  def initialize
13
13
  @line_definitions = {}
14
14
  end
15
+
16
+ def initialize_copy(other)
17
+ @line_definitions = other.line_definitions.dup
18
+ end
15
19
 
16
20
  def method_missing(name, *args, &block)
17
21
  if block_given?
@@ -33,7 +37,7 @@ module RequestLogAnalyzer
33
37
  @captures = []
34
38
  definition.each { |key, value| self.send("#{key.to_s}=".to_sym, value) }
35
39
  end
36
-
40
+
37
41
  def self.define(name, &block)
38
42
  definition = self.new(name)
39
43
  yield(definition) if block_given?
@@ -73,7 +77,14 @@ module RequestLogAnalyzer
73
77
  def convert_captured_values(values, request)
74
78
  value_hash = {}
75
79
  captures.each_with_index do |capture, index|
76
- value_hash[capture[:name]] ||= request.convert_value(values[index], capture)
80
+ converted = request.convert_value(values[index], capture)
81
+ if converted.kind_of?(Hash)
82
+ value_hash[capture[:name]] = values[index]
83
+ converted = converted.inject({}) { |h, (key, value)| h[key.to_sym] = value; h }
84
+ value_hash = converted.merge(value_hash)
85
+ else
86
+ value_hash[capture[:name]] ||= converted
87
+ end
77
88
  end
78
89
  return value_hash
79
90
  end