request-log-analyzer 1.0.2

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 (60) hide show
  1. data/DESIGN +14 -0
  2. data/HACKING +7 -0
  3. data/LICENSE +20 -0
  4. data/README.textile +36 -0
  5. data/Rakefile +5 -0
  6. data/bin/request-log-analyzer +123 -0
  7. data/lib/cli/bashcolorizer.rb +60 -0
  8. data/lib/cli/command_line_arguments.rb +301 -0
  9. data/lib/cli/progressbar.rb +236 -0
  10. data/lib/request_log_analyzer.rb +14 -0
  11. data/lib/request_log_analyzer/aggregator/base.rb +45 -0
  12. data/lib/request_log_analyzer/aggregator/database.rb +148 -0
  13. data/lib/request_log_analyzer/aggregator/echo.rb +25 -0
  14. data/lib/request_log_analyzer/aggregator/summarizer.rb +116 -0
  15. data/lib/request_log_analyzer/controller.rb +201 -0
  16. data/lib/request_log_analyzer/file_format.rb +81 -0
  17. data/lib/request_log_analyzer/file_format/merb.rb +33 -0
  18. data/lib/request_log_analyzer/file_format/rails.rb +90 -0
  19. data/lib/request_log_analyzer/filter/base.rb +29 -0
  20. data/lib/request_log_analyzer/filter/field.rb +36 -0
  21. data/lib/request_log_analyzer/filter/timespan.rb +32 -0
  22. data/lib/request_log_analyzer/line_definition.rb +159 -0
  23. data/lib/request_log_analyzer/log_parser.rb +173 -0
  24. data/lib/request_log_analyzer/log_processor.rb +121 -0
  25. data/lib/request_log_analyzer/request.rb +95 -0
  26. data/lib/request_log_analyzer/source/base.rb +42 -0
  27. data/lib/request_log_analyzer/source/log_file.rb +170 -0
  28. data/lib/request_log_analyzer/tracker/base.rb +54 -0
  29. data/lib/request_log_analyzer/tracker/category.rb +71 -0
  30. data/lib/request_log_analyzer/tracker/duration.rb +81 -0
  31. data/lib/request_log_analyzer/tracker/hourly_spread.rb +80 -0
  32. data/lib/request_log_analyzer/tracker/timespan.rb +54 -0
  33. data/spec/controller_spec.rb +40 -0
  34. data/spec/database_inserter_spec.rb +101 -0
  35. data/spec/file_format_spec.rb +78 -0
  36. data/spec/file_formats/spec_format.rb +26 -0
  37. data/spec/filter_spec.rb +137 -0
  38. data/spec/fixtures/merb.log +84 -0
  39. data/spec/fixtures/multiple_files_1.log +5 -0
  40. data/spec/fixtures/multiple_files_2.log +2 -0
  41. data/spec/fixtures/rails_1x.log +59 -0
  42. data/spec/fixtures/rails_22.log +12 -0
  43. data/spec/fixtures/rails_22_cached.log +10 -0
  44. data/spec/fixtures/rails_unordered.log +24 -0
  45. data/spec/fixtures/syslog_1x.log +5 -0
  46. data/spec/fixtures/test_file_format.log +13 -0
  47. data/spec/fixtures/test_language_combined.log +14 -0
  48. data/spec/fixtures/test_order.log +16 -0
  49. data/spec/line_definition_spec.rb +124 -0
  50. data/spec/log_parser_spec.rb +68 -0
  51. data/spec/log_processor_spec.rb +57 -0
  52. data/spec/merb_format_spec.rb +38 -0
  53. data/spec/rails_format_spec.rb +76 -0
  54. data/spec/request_spec.rb +72 -0
  55. data/spec/spec_helper.rb +67 -0
  56. data/spec/summarizer_spec.rb +9 -0
  57. data/tasks/github-gem.rake +177 -0
  58. data/tasks/request_log_analyzer.rake +10 -0
  59. data/tasks/rspec.rake +6 -0
  60. metadata +135 -0
@@ -0,0 +1,116 @@
1
+ require File.dirname(__FILE__) + '/../tracker/base'
2
+
3
+ module RequestLogAnalyzer::Aggregator
4
+
5
+ class Summarizer < Base
6
+
7
+ class Definer
8
+
9
+ attr_reader :trackers
10
+
11
+ def initialize
12
+ @trackers = []
13
+ end
14
+
15
+ def method_missing(tracker_method, *args)
16
+ track(tracker_method, args.first)
17
+ end
18
+
19
+ def category(category_field, options = {})
20
+ if category_field.kind_of?(Symbol)
21
+ track(:category, options.merge(:category => category_field))
22
+ elsif category_field.kind_of?(Hash)
23
+ track(:category, category_field.merge(options))
24
+ end
25
+ end
26
+
27
+ def duration(duration_field, options = {})
28
+ if duration_field.kind_of?(Symbol)
29
+ track(:duration, options.merge(:duration => duration_field))
30
+ elsif duration_field.kind_of?(Hash)
31
+ track(:duration, duration_field.merge(options))
32
+ end
33
+ end
34
+
35
+ def track(tracker_klass, options = {})
36
+ require "#{File.dirname(__FILE__)}/../tracker/#{tracker_klass}"
37
+ tracker_klass = RequestLogAnalyzer::Tracker.const_get(tracker_klass.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join('')) if tracker_klass.kind_of?(Symbol)
38
+ @trackers << tracker_klass.new(options)
39
+ end
40
+ end
41
+
42
+ attr_reader :trackers
43
+ attr_reader :warnings_encountered
44
+
45
+ def initialize(source, options = {})
46
+ super(source, options)
47
+ @warnings_encountered = {}
48
+ @trackers = source.file_format.report_trackers
49
+ setup
50
+ end
51
+
52
+ def setup
53
+ end
54
+
55
+ def prepare
56
+ raise "No trackers set up in Summarizer!" if @trackers.nil? || @trackers.empty?
57
+ @trackers.each { |tracker| tracker.prepare }
58
+ end
59
+
60
+ def aggregate(request)
61
+ @trackers.each do |tracker|
62
+ tracker.update(request) if tracker.should_update?(request)
63
+ end
64
+ end
65
+
66
+ def finalize
67
+ @trackers.each { |tracker| tracker.finalize }
68
+ end
69
+
70
+ def report(output=STDOUT, report_width = 80, color = false)
71
+ report_header(output, report_width, color)
72
+ if source.parsed_requests > 0
73
+ @trackers.each { |tracker| tracker.report(output, report_width, color) }
74
+ else
75
+ output << "\n"
76
+ output << "There were no requests analyzed.\n"
77
+ end
78
+ report_footer(output, report_width, color)
79
+ end
80
+
81
+ def report_header(output=STDOUT, report_width = 80, color = false)
82
+ output << "Request summary\n"
83
+ output << green("━" * report_width, color) + "\n"
84
+ output << "Parsed lines: #{green(source.parsed_lines, color)}\n"
85
+ output << "Parsed requests: #{green(source.parsed_requests, color)}\n"
86
+ output << "Skipped lines: #{green(source.skipped_lines, color)}\n" if source.skipped_lines > 0
87
+ if has_warnings?
88
+ output << "Warnings: " + @warnings_encountered.map { |(key, value)| "#{key.inspect}: #{blue(value, color)}" }.join(', ') + "\n"
89
+ end
90
+ output << "\n"
91
+ end
92
+
93
+ def report_footer(output=STDOUT, report_width = 80, color = false)
94
+ output << "\n"
95
+ if has_serious_warnings?
96
+ output << green("━" * report_width, color) + "\n"
97
+ output << "Multiple warnings were encountered during parsing. Possibly, your logging " + "\n"
98
+ output << "is not setup correctly. Visit this website for logging configuration tips:" + "\n"
99
+ output << blue("http://github.com/wvanbergen/request-log-analyzer/wikis/configure-logging", color) + "\n"
100
+ end
101
+ end
102
+
103
+ def has_warnings?
104
+ @warnings_encountered.inject(0) { |result, (key, value)| result += value } > 0
105
+ end
106
+
107
+ def has_serious_warnings?
108
+ @warnings_encountered.inject(0) { |result, (key, value)| result += value } > 10
109
+ end
110
+
111
+ def warning(type, message, lineno)
112
+ @warnings_encountered[type] ||= 0
113
+ @warnings_encountered[type] += 1
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,201 @@
1
+ module RequestLogAnalyzer
2
+
3
+ # The RequestLogAnalyzer::Controller class creates a LogParser instance for the
4
+ # requested file format, and connect it with sources and aggregators.
5
+ #
6
+ # Sources are streams or files from which the requests will be parsed.
7
+ # Aggregators will handle every passed request to yield a meaningfull results.
8
+ #
9
+ # - Use the build-function to build a controller instance using command line arguments.
10
+ # - Use add_aggregator to register a new aggregator
11
+ # - Use add_source to register a new aggregator
12
+ # - Use the run! method to start the parser and send the requests to the aggregators.
13
+ #
14
+ # Note that the order of sources can be imported if you have log files than succeed
15
+ # eachother. Requests that span over succeeding files will be parsed correctly if the
16
+ # sources are registered in the correct order. This can be helpful to parse requests
17
+ # from several logrotated log files.
18
+ class Controller
19
+
20
+ include RequestLogAnalyzer::FileFormat::Awareness
21
+
22
+ attr_reader :aggregators
23
+ attr_reader :filters
24
+ attr_reader :log_parser
25
+ attr_reader :source
26
+ attr_reader :output
27
+ attr_reader :options
28
+
29
+ # Builds a RequestLogAnalyzer::Controller given parsed command line arguments
30
+ # <tt>arguments<tt> A CommandLine::Arguments hash containing parsed commandline parameters.
31
+ # <rr>report_with</tt> Width of the report. Defaults to 80.
32
+ def self.build(arguments, report_width = 80)
33
+
34
+ options = { :report_width => arguments[:report_width].to_i, :output => STDOUT}
35
+
36
+ options[:database] = arguments[:database] if arguments[:database]
37
+ options[:debug] = arguments[:debug]
38
+ options[:colorize] = !arguments[:boring]
39
+
40
+ if arguments[:file]
41
+ options[:output] = File.new(arguments[:file], "w+")
42
+ options[:colorize] = false
43
+ end
44
+
45
+ # Create the controller with the correct file format
46
+ file_format = RequestLogAnalyzer::FileFormat.load(arguments[:format])
47
+
48
+ # register sources
49
+ if arguments.parameters.length == 1
50
+ file = arguments.parameters[0]
51
+ if file == '-' || file == 'STDIN'
52
+ options.store(:source_files, $stdin)
53
+ elsif File.exist?(file)
54
+ options.store(:source_files, file)
55
+ else
56
+ puts "File not found: #{file}"
57
+ exit(0)
58
+ end
59
+ else
60
+ options.store(:source_files, arguments.parameters)
61
+ end
62
+
63
+ controller = Controller.new(RequestLogAnalyzer::Source::LogFile.new(file_format, options), options)
64
+
65
+ options[:assume_correct_order] = arguments[:assume_correct_order]
66
+
67
+ # register filters
68
+ if arguments[:after] || arguments[:before]
69
+ filter_options = {}
70
+ filter_options[:after] = DateTime.parse(arguments[:after])
71
+ filter_options[:before] = DateTime.parse(arguments[:before]) if arguments[:before]
72
+ controller.add_filter(:timespan, filter_options)
73
+ end
74
+
75
+ arguments[:reject].each do |(field, value)|
76
+ controller.add_filter(:field, :mode => :reject, :field => field, :value => value)
77
+ end
78
+
79
+ arguments[:select].each do |(field, value)|
80
+ controller.add_filter(:field, :mode => :select, :field => field, :value => value)
81
+ end
82
+
83
+ # register aggregators
84
+ arguments[:aggregator].each { |agg| controller.add_aggregator(agg.to_sym) }
85
+
86
+ # register the database
87
+ controller.add_aggregator(:database) if arguments[:database] && !arguments[:aggregator].include?('database')
88
+ controller.add_aggregator(:summarizer) if arguments[:aggregator].empty?
89
+
90
+ # register the echo aggregator in debug mode
91
+ controller.add_aggregator(:echo) if arguments[:debug]
92
+
93
+ file_format.setup_environment(controller)
94
+
95
+ return controller
96
+ end
97
+
98
+ # Builds a new Controller for the given log file format.
99
+ # <tt>format</tt> Logfile format. Defaults to :rails
100
+ # Options are passd on to the LogParser.
101
+ # * <tt>:aggregator</tt> Aggregator array.
102
+ # * <tt>:database</tt> Database the controller should use.
103
+ # * <tt>:echo</tt> Output debug information.
104
+ # * <tt>:silent</tt> Do not output any warnings.
105
+ # * <tt>:colorize</tt> Colorize output
106
+ # * <tt>:output</tt> All report outputs get << through this output.
107
+ def initialize(source, options = {})
108
+
109
+ @source = source
110
+ @options = options
111
+ @aggregators = []
112
+ @filters = []
113
+ @output = options[:output]
114
+
115
+ # Requester format through RequestLogAnalyzer::FileFormat and construct the parser
116
+ register_file_format(@source.file_format)
117
+
118
+ # Pass all warnings to every aggregator so they can do something useful with them.
119
+ @source.warning = lambda { |type, message, lineno| @aggregators.each { |agg| agg.warning(type, message, lineno) } } if @source
120
+
121
+ # Handle progress messagess
122
+ @source.progress = lambda { |message, value| handle_progress(message, value) } if @source
123
+ end
124
+
125
+ # Progress function.
126
+ # Expects :started with file, :progress with current line and :finished or :interrupted when done.
127
+ # <tt>message</tt> Current state (:started, :finished, :interupted or :progress).
128
+ # <tt>value</tt> File or current line.
129
+ def handle_progress(message, value = nil)
130
+ case message
131
+ when :started
132
+ @progress_bar = ProgressBar.new(green(File.basename(value), options[:colorize]), File.size(value))
133
+ when :finished
134
+ @progress_bar.finish
135
+ @progress_bar = nil
136
+ when :interrupted
137
+ if @progress_bar
138
+ @progress_bar.halt
139
+ @progress_bar = nil
140
+ end
141
+ when :progress
142
+ @progress_bar.set(value)
143
+ end
144
+ end
145
+
146
+ # Adds an aggregator to the controller. The aggregator will be called for every request
147
+ # that is parsed from the provided sources (see add_source)
148
+ def add_aggregator(agg)
149
+ if agg.kind_of?(Symbol)
150
+ require File.dirname(__FILE__) + "/aggregator/#{agg}"
151
+ agg = RequestLogAnalyzer::Aggregator.const_get(agg.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join(''))
152
+ end
153
+
154
+ @aggregators << agg.new(@source, @options)
155
+ end
156
+
157
+ alias :>> :add_aggregator
158
+
159
+ # Adds a request filter to the controller.
160
+ def add_filter(filter, filter_options = {})
161
+ if filter.kind_of?(Symbol)
162
+ require File.dirname(__FILE__) + "/filter/#{filter}"
163
+ filter = RequestLogAnalyzer::Filter.const_get(filter.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join(''))
164
+ end
165
+
166
+ @filters << filter.new(file_format, @options.merge(filter_options))
167
+ end
168
+
169
+ # Runs RequestLogAnalyzer
170
+ # 1. Call prepare on every aggregator
171
+ # 2. Generate requests from source object
172
+ # 3. Filter out unwanted requests
173
+ # 4. Call aggregate for remaning requests on every aggregator
174
+ # 4. Call finalize on every aggregator
175
+ # 5. Call report on every aggregator
176
+ # 6. Finalize Source
177
+ def run!
178
+
179
+ @filters.each { |filter| filter.prepare }
180
+ @aggregators.each { |agg| agg.prepare }
181
+
182
+ begin
183
+ @source.requests do |request|
184
+ #@filters.each { |filter| request = filter.filter(request) }
185
+ @aggregators.each { |agg| agg.aggregate(request) } if request
186
+ end
187
+ rescue Interrupt => e
188
+ handle_progress(:interrupted)
189
+ puts "Caught interrupt! Stopped parsing."
190
+ end
191
+
192
+ puts "\n"
193
+
194
+ @aggregators.each { |agg| agg.finalize }
195
+ @aggregators.each { |agg| agg.report(@output, options[:report_width], options[:colorize]) }
196
+
197
+ @source.finalize
198
+ end
199
+
200
+ end
201
+ end
@@ -0,0 +1,81 @@
1
+ module RequestLogAnalyzer
2
+
3
+ class FileFormat
4
+
5
+ # Makes classes aware of a file format by registering the file_format variable
6
+ module Awareness
7
+
8
+ def self.included(base)
9
+ base.send(:attr_reader, :file_format)
10
+ end
11
+
12
+ def register_file_format(format)
13
+ @file_format = format
14
+ end
15
+ end
16
+
17
+ def self.inherited(subclass)
18
+ subclass.instance_variable_set(:@line_definer, RequestLogAnalyzer::LineDefinition::Definer.new)
19
+ subclass.class_eval { class << self; attr_accessor :line_definer; end }
20
+ subclass.class_eval { class << self; attr_accessor :report_definer; end }
21
+ end
22
+
23
+ def self.line_definition(name, &block)
24
+ @line_definer.send(name, &block)
25
+ end
26
+
27
+ def self.format_definition(&block)
28
+ if block_given?
29
+ yield(@line_definer)
30
+ else
31
+ return @line_definer
32
+ end
33
+ end
34
+
35
+ def self.report(&block)
36
+ @report_definer = RequestLogAnalyzer::Aggregator::Summarizer::Definer.new
37
+ yield(@report_definer)
38
+ end
39
+
40
+ def self.load(file_format)
41
+ if file_format.kind_of?(RequestLogAnalyzer::FileFormat)
42
+ # this already is a file format! return itself
43
+ return file_format
44
+
45
+ elsif file_format.kind_of?(Class) && file_format.ancestors.include?(RequestLogAnalyzer::FileFormat)
46
+ klass = file_format
47
+
48
+ elsif file_format.kind_of?(String) && File.exist?(file_format)
49
+ # load a format from a ruby file
50
+ require file_format
51
+ klass_name = File.basename(file_format, '.rb').split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join('')
52
+ klass = Object.const_get(klass_name)
53
+
54
+ elsif File.exist?("#{File.dirname(__FILE__)}/file_format/#{file_format}.rb")
55
+ # load a provided file format
56
+ require "#{File.dirname(__FILE__)}/file_format/#{file_format}"
57
+ klass_name = file_format.to_s.split(/[^a-z0-9]/i).map{ |w| w.capitalize }.join('')
58
+ klass = RequestLogAnalyzer::FileFormat.const_get(klass_name)
59
+
60
+ end
61
+
62
+ klass.new # return an instance of the class
63
+ end
64
+
65
+ def line_definitions
66
+ @line_definitions ||= self.class.line_definer.line_definitions
67
+ end
68
+
69
+ def report_trackers
70
+ self.class.instance_variable_get(:@report_definer).trackers rescue []
71
+ end
72
+
73
+ def valid?
74
+ line_definitions.detect { |(name, ld)| ld.header } && line_definitions.detect { |(name, ld)| ld.footer }
75
+ end
76
+
77
+ def setup_environment(controller)
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,33 @@
1
+ module RequestLogAnalyzer::FileFormat::Merb
2
+
3
+ LINE_DEFINITIONS = {
4
+
5
+ # ~ Started request handling: Fri Aug 29 11:10:23 +0200 2008
6
+ :started => {
7
+ :header => true,
8
+ :teaser => /Started/,
9
+ :regexp => /Started request handling\:\ (.+)/,
10
+ :captures => [{ :name => :timestamp, :type => :timestamp, :anonymize => :slightly }]
11
+ },
12
+
13
+ # ~ Params: {"action"=>"create", "controller"=>"session"}
14
+ # ~ Params: {"_method"=>"delete", "authenticity_token"=>"[FILTERED]", "action"=>"d}
15
+ :params => {
16
+ :teaser => /Params/,
17
+ :regexp => /Params\:\ \{(.+)\}/,
18
+ :captures => [{ :name => :raw_hash, :type => :string}]
19
+ },
20
+
21
+ # ~ {:dispatch_time=>0.006117, :after_filters_time=>6.1e-05, :before_filters_time=>0.000712, :action_time=>0.005833}
22
+ :completed => {
23
+ :footer => true,
24
+ :teaser => /\{:dispatch_time/,
25
+ :regexp => /\{\:dispatch_time=>(\d+\.\d+(?:e-?\d+)?), (?:\:after_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?(?:\:before_filters_time=>(\d+\.\d+(?:e-?\d+)?), )?\:action_time=>(\d+\.\d+(?:e-?\d+)?)\}/,
26
+ :captures => [{ :name => :dispatch_time, :type => :sec, :anonymize => :slightly },
27
+ { :name => :after_filters_time, :type => :sec, :anonymize => :slightly },
28
+ { :name => :before_filters_time, :type => :sec, :anonymize => :slightly },
29
+ { :name => :action_time, :type => :sec, :anonymize => :slightly }]
30
+ }
31
+ }
32
+
33
+ end
@@ -0,0 +1,90 @@
1
+ class RequestLogAnalyzer::FileFormat::Rails < RequestLogAnalyzer::FileFormat
2
+
3
+ # Processing EmployeeController#index (for 123.123.123.123 at 2008-07-13 06:00:00) [GET]
4
+ line_definition :processing do |line|
5
+ line.header = true # this line is the first log line for a request
6
+ line.teaser = /Processing /
7
+ 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]+)\]/
8
+ line.captures << { :name => :controller, :type => :string } \
9
+ << { :name => :action, :type => :string } \
10
+ << { :name => :format, :type => :string } \
11
+ << { :name => :ip, :type => :string, :anonymize => :ip } \
12
+ << { :name => :timestamp, :type => :timestamp, :anonymize => :slightly } \
13
+ << { :name => :method, :type => :string }
14
+ end
15
+
16
+ # 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.
17
+ line_definition :cache_hit do |line|
18
+ line.regexp = /Filter chain halted as \[\#<ActionController::Caching::Actions::ActionCacheFilter:.+>\] rendered_or_redirected/
19
+ end
20
+
21
+ # RuntimeError (Cannot destroy employee): /app/models/employee.rb:198:in `before_destroy'
22
+ line_definition :failed do |line|
23
+ line.footer = true
24
+ line.regexp = /((?:[A-Z]\w+\:\:)*[A-Z]\w+) \((.*)\)(?: on line #(\d+) of .+)?\:(.*)/
25
+ line.captures << { :name => :error, :type => :string } \
26
+ << { :name => :message, :type => :string } \
27
+ << { :name => :line, :type => :integer } \
28
+ << { :name => :file, :type => :string } \
29
+ << { :name => :stack_trace, :type => :string, :anonymize => true }
30
+ end
31
+
32
+
33
+ # Rails < 2.1 completed line example
34
+ # Completed in 0.21665 (4 reqs/sec) | Rendering: 0.00926 (4%) | DB: 0.00000 (0%) | 200 OK [http://demo.nu/employees]
35
+ 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.+)\]/
36
+
37
+ # Rails > 2.1 completed line example
38
+ # Completed in 614ms (View: 120, DB: 31) | 200 OK [http://floorplanner.local/demo]
39
+ RAILS_22_COMPLETED = /Completed in (\d+)ms \((?:View: (\d+), )?DB: (\d+)\) \| (\d\d\d).+\[(http.+)\]/
40
+
41
+ # The completed line uses a kind of hack to ensure that both old style logs and new style logs
42
+ # are both parsed by the same regular expression. The format in Rails 2.2 was slightly changed,
43
+ # but the line contains exactly the same information.
44
+ line_definition :completed do |line|
45
+
46
+ line.footer = true
47
+ line.teaser = /Completed in /
48
+ line.regexp = Regexp.new("(?:#{RAILS_21_COMPLETED}|#{RAILS_22_COMPLETED})")
49
+
50
+ line.captures << { :name => :duration, :type => :sec, :anonymize => :slightly } \
51
+ << { :name => :view, :type => :sec, :anonymize => :slightly } \
52
+ << { :name => :db, :type => :sec, :anonymize => :slightly } \
53
+ << { :name => :status, :type => :integer } \
54
+ << { :name => :url, :type => :string, :anonymize => :url } # Old variant
55
+
56
+ line.captures << { :name => :duration, :type => :msec, :anonymize => :slightly } \
57
+ << { :name => :view, :type => :msec, :anonymize => :slightly } \
58
+ << { :name => :db, :type => :msec, :anonymize => :slightly } \
59
+ << { :name => :status, :type => :integer} \
60
+ << { :name => :url, :type => :string, :anonymize => :url } # 2.2 variant
61
+ end
62
+
63
+
64
+
65
+ REQUEST_CATEGORIZER = Proc.new do |request|
66
+ format = request[:format] || 'html'
67
+ "#{request[:controller]}##{request[:action]}.#{format} [#{request[:method]}]"
68
+ end
69
+
70
+ report do |analyze|
71
+ analyze.timespan :line_type => :processing
72
+ analyze.category :category => REQUEST_CATEGORIZER, :title => 'Top 20 hits', :amount => 20, :line_type => :processing
73
+ analyze.category :method, :title => 'HTTP methods'
74
+ analyze.category :status, :title => 'HTTP statuses returned'
75
+ analyze.category :category => lambda { |request| request =~ :cache_hit ? 'Cache hit' : 'No hit' }, :title => 'Rails action cache hits'
76
+
77
+ analyze.duration :duration, :category => REQUEST_CATEGORIZER, :title => "Request duration", :line_type => :completed
78
+ analyze.duration :view, :category => REQUEST_CATEGORIZER, :title => "Database time", :line_type => :completed
79
+ analyze.duration :db, :category => REQUEST_CATEGORIZER, :title => "View rendering time", :line_type => :completed
80
+
81
+ analyze.category :category => REQUEST_CATEGORIZER, :title => 'Process blockers (> 1 sec duration)',
82
+ :if => lambda { |request| request[:duration] && request[:duration] > 1.0 }, :amount => 20
83
+
84
+ analyze.hourly_spread :line_type => :processing
85
+ analyze.category :error, :title => 'Failed requests', :line_type => :failed, :amount => 20
86
+ end
87
+
88
+
89
+
90
+ end