request-log-analyzer 1.3.4 → 1.4.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 (86) hide show
  1. data/LICENSE +3 -3
  2. data/README.rdoc +1 -1
  3. data/bin/request-log-analyzer +17 -14
  4. data/lib/cli/command_line_arguments.rb +51 -51
  5. data/lib/cli/database_console.rb +3 -3
  6. data/lib/cli/database_console_init.rb +4 -3
  7. data/lib/cli/progressbar.rb +10 -10
  8. data/lib/cli/tools.rb +3 -3
  9. data/lib/request_log_analyzer/aggregator/database_inserter.rb +19 -17
  10. data/lib/request_log_analyzer/aggregator/echo.rb +14 -9
  11. data/lib/request_log_analyzer/aggregator/summarizer.rb +26 -26
  12. data/lib/request_log_analyzer/aggregator.rb +11 -15
  13. data/lib/request_log_analyzer/controller.rb +162 -89
  14. data/lib/request_log_analyzer/database/base.rb +22 -21
  15. data/lib/request_log_analyzer/database/connection.rb +3 -3
  16. data/lib/request_log_analyzer/database/request.rb +22 -0
  17. data/lib/request_log_analyzer/database/source.rb +13 -0
  18. data/lib/request_log_analyzer/database/warning.rb +14 -0
  19. data/lib/request_log_analyzer/database.rb +28 -103
  20. data/lib/request_log_analyzer/file_format/amazon_s3.rb +20 -20
  21. data/lib/request_log_analyzer/file_format/apache.rb +37 -30
  22. data/lib/request_log_analyzer/file_format/merb.rb +30 -13
  23. data/lib/request_log_analyzer/file_format/rack.rb +11 -0
  24. data/lib/request_log_analyzer/file_format/rails.rb +143 -73
  25. data/lib/request_log_analyzer/file_format/rails_development.rb +4 -49
  26. data/lib/request_log_analyzer/file_format.rb +16 -28
  27. data/lib/request_log_analyzer/filter/anonymize.rb +8 -7
  28. data/lib/request_log_analyzer/filter/field.rb +9 -9
  29. data/lib/request_log_analyzer/filter/timespan.rb +12 -10
  30. data/lib/request_log_analyzer/filter.rb +12 -16
  31. data/lib/request_log_analyzer/line_definition.rb +15 -14
  32. data/lib/request_log_analyzer/log_processor.rb +27 -29
  33. data/lib/request_log_analyzer/mailer.rb +15 -9
  34. data/lib/request_log_analyzer/output/fixed_width.rb +48 -48
  35. data/lib/request_log_analyzer/output/html.rb +20 -20
  36. data/lib/request_log_analyzer/output.rb +53 -12
  37. data/lib/request_log_analyzer/request.rb +88 -69
  38. data/lib/request_log_analyzer/source/database_loader.rb +10 -14
  39. data/lib/request_log_analyzer/source/log_parser.rb +57 -50
  40. data/lib/request_log_analyzer/source.rb +10 -12
  41. data/lib/request_log_analyzer/tracker/duration.rb +41 -134
  42. data/lib/request_log_analyzer/tracker/frequency.rb +31 -32
  43. data/lib/request_log_analyzer/tracker/hourly_spread.rb +21 -21
  44. data/lib/request_log_analyzer/tracker/timespan.rb +17 -17
  45. data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
  46. data/lib/request_log_analyzer/tracker.rb +139 -32
  47. data/lib/request_log_analyzer.rb +5 -5
  48. data/request-log-analyzer.gemspec +19 -15
  49. data/spec/database.yml +6 -0
  50. data/spec/fixtures/rails_22.log +1 -1
  51. data/spec/integration/command_line_usage_spec.rb +7 -1
  52. data/spec/lib/helpers.rb +7 -7
  53. data/spec/lib/macros.rb +3 -3
  54. data/spec/lib/matchers.rb +41 -27
  55. data/spec/lib/mocks.rb +19 -15
  56. data/spec/lib/testing_format.rb +9 -9
  57. data/spec/spec_helper.rb +6 -6
  58. data/spec/unit/aggregator/database_inserter_spec.rb +16 -16
  59. data/spec/unit/aggregator/summarizer_spec.rb +4 -4
  60. data/spec/unit/controller/controller_spec.rb +2 -2
  61. data/spec/unit/controller/log_processor_spec.rb +1 -1
  62. data/spec/unit/database/base_class_spec.rb +26 -33
  63. data/spec/unit/database/connection_spec.rb +3 -3
  64. data/spec/unit/database/database_spec.rb +33 -38
  65. data/spec/unit/file_format/amazon_s3_format_spec.rb +5 -5
  66. data/spec/unit/file_format/apache_format_spec.rb +48 -11
  67. data/spec/unit/file_format/file_format_api_spec.rb +13 -13
  68. data/spec/unit/file_format/line_definition_spec.rb +24 -17
  69. data/spec/unit/file_format/merb_format_spec.rb +41 -45
  70. data/spec/unit/file_format/rails_format_spec.rb +157 -117
  71. data/spec/unit/filter/anonymize_filter_spec.rb +2 -2
  72. data/spec/unit/filter/field_filter_spec.rb +13 -13
  73. data/spec/unit/filter/filter_spec.rb +1 -1
  74. data/spec/unit/filter/timespan_filter_spec.rb +15 -15
  75. data/spec/unit/mailer_spec.rb +30 -0
  76. data/spec/unit/{source/request_spec.rb → request_spec.rb} +30 -30
  77. data/spec/unit/source/log_parser_spec.rb +27 -27
  78. data/spec/unit/tracker/duration_tracker_spec.rb +115 -78
  79. data/spec/unit/tracker/frequency_tracker_spec.rb +74 -63
  80. data/spec/unit/tracker/hourly_spread_spec.rb +28 -20
  81. data/spec/unit/tracker/timespan_tracker_spec.rb +25 -13
  82. data/spec/unit/tracker/tracker_api_spec.rb +117 -42
  83. data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
  84. data/tasks/github-gem.rake +125 -75
  85. data/tasks/request_log_analyzer.rake +2 -2
  86. metadata +19 -10
@@ -1,13 +1,13 @@
1
1
  module RequestLogAnalyzer::Source
2
-
2
+
3
3
  # The LogParser class reads log data from a given source and uses a file format definition
4
- # to parse all relevent information about requests from the file. A FileFormat module should
4
+ # to parse all relevent information about requests from the file. A FileFormat module should
5
5
  # be provided that contains the definitions of the lines that occur in the log data.
6
6
  #
7
- # De order in which lines occur is used to combine lines to a single request. If these lines
8
- # are mixed, requests cannot be combined properly. This can be the case if data is written to
9
- # the log file simultaneously by different mongrel processes. This problem is detected by the
10
- # parser. It will emit warnings when this occurs. LogParser supports multiple parse strategies
7
+ # De order in which lines occur is used to combine lines to a single request. If these lines
8
+ # are mixed, requests cannot be combined properly. This can be the case if data is written to
9
+ # the log file simultaneously by different mongrel processes. This problem is detected by the
10
+ # parser. It will emit warnings when this occurs. LogParser supports multiple parse strategies
11
11
  # that deal differently with this problem.
12
12
  class LogParser < Base
13
13
 
@@ -15,7 +15,7 @@ module RequestLogAnalyzer::Source
15
15
 
16
16
  # The default parse strategy that will be used to parse the input.
17
17
  DEFAULT_PARSE_STRATEGY = 'assume-correct'
18
-
18
+
19
19
  # All available parse strategies.
20
20
  PARSE_STRATEGIES = ['cautious', 'assume-correct']
21
21
 
@@ -27,42 +27,44 @@ module RequestLogAnalyzer::Source
27
27
  #
28
28
  # <tt>format</tt>:: The current file format instance
29
29
  # <tt>options</tt>:: A hash of options that are used by the parser
30
- def initialize(format, options = {})
31
- @line_definitions = {}
32
- @options = options
30
+ def initialize(format, options = {})
31
+ super(format, options)
33
32
  @parsed_lines = 0
34
33
  @parsed_requests = 0
35
34
  @skipped_lines = 0
36
35
  @skipped_requests = 0
36
+ @current_request = nil
37
+ @current_source = nil
37
38
  @current_file = nil
38
39
  @current_lineno = nil
39
40
  @source_files = options[:source_files]
40
-
41
+ @progress_handler = nil
42
+
41
43
  @options[:parse_strategy] ||= DEFAULT_PARSE_STRATEGY
42
44
  raise "Unknown parse strategy" unless PARSE_STRATEGIES.include?(@options[:parse_strategy])
43
-
44
- self.register_file_format(format)
45
45
  end
46
-
46
+
47
47
  # Reads the input, which can either be a file, sequence of files or STDIN to parse
48
48
  # lines specified in the FileFormat. This lines will be combined into Request instances,
49
49
  # that will be yielded. The actual parsing occurs in the parse_io method.
50
50
  # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
51
51
  def each_request(options = {}, &block) # :yields: :request, request
52
-
52
+
53
53
  case @source_files
54
54
  when IO
55
- puts "Parsing from the standard input. Press CTRL+C to finish." # FIXME: not here
56
- parse_stream(@source_files, options, &block)
55
+ if @source_files == $stdin
56
+ puts "Parsing from the standard input. Press CTRL+C to finish." # FIXME: not here
57
+ end
58
+ parse_stream(@source_files, options, &block)
57
59
  when String
58
- parse_file(@source_files, options, &block)
60
+ parse_file(@source_files, options, &block)
59
61
  when Array
60
- parse_files(@source_files, options, &block)
62
+ parse_files(@source_files, options, &block)
61
63
  else
62
64
  raise "Unknown source provided"
63
65
  end
64
66
  end
65
-
67
+
66
68
  # Make sure the Enumerable methods work as expected
67
69
  alias_method :each, :each_request
68
70
 
@@ -73,12 +75,12 @@ module RequestLogAnalyzer::Source
73
75
  def parse_files(files, options = {}, &block) # :yields: request
74
76
  files.each { |file| parse_file(file, options, &block) }
75
77
  end
76
-
78
+
77
79
  # Check if a file has a compressed extention in the filename.
78
80
  # If recognized, return the command string used to decompress the file
79
81
  def decompress_file?(filename)
80
82
  nice_command = "nice -n 5"
81
-
83
+
82
84
  return "#{nice_command} gunzip -c -d #{filename}" if filename.match(/\.tar.gz$/) || filename.match(/\.tgz$/) || filename.match(/\.gz$/)
83
85
  return "#{nice_command} bunzip2 -c -d #{filename}" if filename.match(/\.bz2$/)
84
86
  return "#{nice_command} unzip -p #{filename}" if filename.match(/\.zip$/)
@@ -97,27 +99,32 @@ module RequestLogAnalyzer::Source
97
99
  # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
98
100
  def parse_file(file, options = {}, &block)
99
101
 
100
- @progress_handler = @dormant_progress_handler
101
102
  @current_source = File.expand_path(file)
102
- @progress_handler.call(:started, file) if @progress_handler
103
103
  @source_changes_handler.call(:started, @current_source) if @source_changes_handler
104
-
104
+
105
105
  if decompress_file?(file).empty?
106
+
107
+ @progress_handler = @dormant_progress_handler
108
+ @progress_handler.call(:started, file) if @progress_handler
109
+
106
110
  File.open(file, 'r') { |f| parse_io(f, options, &block) }
111
+
112
+ @progress_handler.call(:finished, file) if @progress_handler
113
+ @progress_handler = nil
107
114
  else
108
115
  IO.popen(decompress_file?(file), 'r') { |f| parse_io(f, options, &block) }
109
116
  end
110
-
117
+
111
118
  @source_changes_handler.call(:finished, @current_source) if @source_changes_handler
112
- @progress_handler.call(:finished, file) if @progress_handler
119
+
113
120
  @current_source = nil
114
- @progress_handler = nil
121
+
115
122
  end
116
123
 
117
124
  # Parses an IO stream. It will simply call parse_io. This function does not support progress updates
118
125
  # because the length of a stream is not known.
119
126
  # <tt>stream</tt>:: The IO stream that should be parsed.
120
- # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
127
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
121
128
  def parse_stream(stream, options = {}, &block)
122
129
  parse_io(stream, options, &block)
123
130
  end
@@ -137,16 +144,16 @@ module RequestLogAnalyzer::Source
137
144
  def parse_io(io, options = {}, &block) # :yields: request
138
145
  @current_lineno = 1
139
146
  while line = io.gets
140
- @progress_handler.call(:progress, io.pos) if @progress_handler && @current_lineno % 127 == 0
141
-
147
+ @progress_handler.call(:progress, io.pos) if @progress_handler && @current_lineno % 255 == 0
148
+
142
149
  if request_data = file_format.parse_line(line) { |wt, message| warn(wt, message) }
143
150
  @parsed_lines += 1
144
151
  update_current_request(request_data.merge(:source => @current_source, :lineno => @current_lineno), &block)
145
152
  end
146
-
153
+
147
154
  @current_lineno += 1
148
155
  end
149
-
156
+
150
157
  warn(:unfinished_request_on_eof, "End of file reached, but last request was not completed!") unless @current_request.nil?
151
158
  @current_lineno = nil
152
159
  end
@@ -162,7 +169,7 @@ module RequestLogAnalyzer::Source
162
169
  def warning=(proc)
163
170
  @warning_handler = proc
164
171
  end
165
-
172
+
166
173
  # Add a block to this method to install a source change handler while parsing,
167
174
  # <tt>proc</tt>:: The proc that will be called to handle source changes
168
175
  def source_changes=(proc)
@@ -170,9 +177,9 @@ module RequestLogAnalyzer::Source
170
177
  end
171
178
 
172
179
  # This method is called by the parser if it encounteres any parsing problems.
173
- # It will call the installed warning handler if any.
180
+ # It will call the installed warning handler if any.
174
181
  #
175
- # By default, RequestLogAnalyzer::Controller will install a warning handler
182
+ # By default, RequestLogAnalyzer::Controller will install a warning handler
176
183
  # that will pass the warnings to each aggregator so they can do something useful
177
184
  # with it.
178
185
  #
@@ -184,24 +191,24 @@ module RequestLogAnalyzer::Source
184
191
 
185
192
  protected
186
193
 
187
- # Combines the different lines of a request into a single Request object. It will start a
188
- # new request when a header line is encountered en will emit the request when a footer line
194
+ # Combines the different lines of a request into a single Request object. It will start a
195
+ # new request when a header line is encountered en will emit the request when a footer line
189
196
  # is encountered.
190
197
  #
191
198
  # Combining the lines is done using heuristics. Problems can occur in this process. The
192
199
  # current parse strategy defines how these cases are handled.
193
200
  #
194
201
  # When using the 'assume-correct' parse strategy (default):
195
- # - Every line that is parsed before a header line is ignored as it cannot be included in
202
+ # - Every line that is parsed before a header line is ignored as it cannot be included in
196
203
  # any request. It will emit a :no_current_request warning.
197
- # - If a header line is found before the previous requests was closed, the previous request
204
+ # - If a header line is found before the previous requests was closed, the previous request
198
205
  # will be yielded and a new request will be started.
199
206
  #
200
207
  # When using the 'cautious' parse strategy:
201
- # - Every line that is parsed before a header line is ignored as it cannot be included in
208
+ # - Every line that is parsed before a header line is ignored as it cannot be included in
202
209
  # any request. It will emit a :no_current_request warning.
203
210
  # - A header line that is parsed before a request is closed by a footer line, is a sign of
204
- # an unproperly ordered file. All data that is gathered for the request until then is
211
+ # an unproperly ordered file. All data that is gathered for the request until then is
205
212
  # discarded and the next request is ignored as well. An :unclosed_request warning is
206
213
  # emitted.
207
214
  #
@@ -228,7 +235,7 @@ module RequestLogAnalyzer::Source
228
235
  @current_request << request_data
229
236
  if footer_line?(request_data)
230
237
  handle_request(@current_request, &block) # yield @current_request
231
- @current_request = nil
238
+ @current_request = nil
232
239
  end
233
240
  else
234
241
  @skipped_lines += 1
@@ -236,8 +243,8 @@ module RequestLogAnalyzer::Source
236
243
  end
237
244
  end
238
245
  end
239
-
240
- # Handles the parsed request by sending it into the pipeline.
246
+
247
+ # Handles the parsed request by sending it into the pipeline.
241
248
  #
242
249
  # - It will call RequestLogAnalyzer::Request#validate on the request instance
243
250
  # - It will send the request into the pipeline, checking whether it was accepted by all the filters.
@@ -249,19 +256,19 @@ module RequestLogAnalyzer::Source
249
256
  request.validate
250
257
  accepted = block_given? ? yield(request) : true
251
258
  @skipped_requests += 1 unless accepted
252
- end
259
+ end
253
260
 
254
261
  # Checks whether a given line hash is a header line according to the current file format.
255
- # <tt>hash</tt>:: A hash of data that was parsed from the line.
262
+ # <tt>hash</tt>:: A hash of data that was parsed from the line.
256
263
  def header_line?(hash)
257
264
  hash[:line_definition].header
258
265
  end
259
266
 
260
- # Checks whether a given line hash is a footer line according to the current file format.
261
- # <tt>hash</tt>:: A hash of data that was parsed from the line.
267
+ # Checks whether a given line hash is a footer line according to the current file format.
268
+ # <tt>hash</tt>:: A hash of data that was parsed from the line.
262
269
  def footer_line?(hash)
263
270
  hash[:line_definition].footer
264
- end
271
+ end
265
272
  end
266
273
 
267
274
  end
@@ -1,7 +1,7 @@
1
1
  # The RequestLogAnalyzer::Source module contains all functionality that loads requests from a given source
2
2
  # and feed them to the pipeline for further processing. The requests (see RequestLogAnalyzer::Request) that
3
- # will be parsed from a source, will be piped throug filters (see RequestLogAnalyzer::Filter) and are then
4
- # fed to an aggregator (see RequestLogAnalyzer::Aggregator). The source instance is thus the beginning of
3
+ # will be parsed from a source, will be piped throug filters (see RequestLogAnalyzer::Filter) and are then
4
+ # fed to an aggregator (see RequestLogAnalyzer::Aggregator). The source instance is thus the beginning of
5
5
  # the RequestLogAnalyzer chain.
6
6
  #
7
7
  # - The base class for all sources is RequestLogAnalyzer::Source::Base. All source classes should inherit from this class.
@@ -14,15 +14,12 @@ module RequestLogAnalyzer::Source
14
14
  def self.const_missing(const)
15
15
  RequestLogAnalyzer::load_default_class_file(self, const)
16
16
  end
17
-
17
+
18
18
  # The base Source class. All other sources should inherit from this class.
19
19
  #
20
20
  # A source implememtation should at least implement the each_request method, which should yield
21
21
  # RequestLogAnalyzer::Request instances that will be fed through the pipleine.
22
22
  class Base
23
-
24
- # Make the Spurce instance aware of the current file format
25
- include RequestLogAnalyzer::FileFormat::Awareness
26
23
 
27
24
  # A hash of options
28
25
  attr_reader :options
@@ -42,23 +39,24 @@ module RequestLogAnalyzer::Source
42
39
  # The total number of skipped requests because of filters.
43
40
  attr_reader :skipped_requests
44
41
 
45
-
42
+ # The FileFormat instance that describes the format of this source.
43
+ attr_reader :file_format
46
44
 
47
45
  # Initializer, which will register the file format and save any options given as a hash.
48
46
  # <tt>format</tt>:: The file format instance
49
47
  # <tt>options</tt>:: A hash of options that can be used by a specific Source implementation
50
48
  def initialize(format, options = {})
51
- @options = options
52
- register_file_format(format)
49
+ @options = options
50
+ @file_format = format
53
51
  end
54
-
52
+
55
53
  # The prepare method is called before the RequestLogAnalyzer::Source::Base#each_request method is called.
56
54
  # Use this method to implement any initialization that should occur before this source can produce Request
57
55
  # instances.
58
56
  def prepare
59
57
  end
60
-
61
- # This function is called to actually produce the requests that will be send into the pipeline.
58
+
59
+ # This function is called to actually produce the requests that will be send into the pipeline.
62
60
  # The implementation should yield instances of RequestLogAnalyzer::Request.
63
61
  # <tt>options</tt>:: A Hash of options that can be used in the implementation.
64
62
  def each_request(options = {}, &block) # :yields: request
@@ -3,12 +3,11 @@ module RequestLogAnalyzer::Tracker
3
3
  # Analyze the duration of a specific attribute
4
4
  #
5
5
  # === Options
6
- # * <tt>:amount</tt> The amount of lines in the report
7
6
  # * <tt>:category</tt> Proc that handles request categorization for given fileformat (REQUEST_CATEGORIZER)
8
7
  # * <tt>:duration</tt> The field containing the duration in the request hash.
9
8
  # * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
10
9
  # * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
11
- # * <tt>:title</tt> Title do be displayed above the report
10
+ # * <tt>:title</tt> Title do be displayed above the report
12
11
  # * <tt>:unless</tt> Handle request if this proc is false for the handled request.
13
12
  #
14
13
  # The items in the update request hash are set during the creation of the Duration tracker.
@@ -21,141 +20,62 @@ module RequestLogAnalyzer::Tracker
21
20
  # EmployeeController#index.html [GET] | 5802 | 1477.32s | 0.25s
22
21
  # .............
23
22
  class Duration < Base
24
-
23
+
24
+ include RequestLogAnalyzer::Tracker::StatisticsTracking
25
+
25
26
  attr_reader :categories
26
27
 
27
28
  # Check if duration and catagory option have been received,
28
29
  def prepare
29
30
  raise "No duration field set up for category tracker #{self.inspect}" unless options[:duration]
30
- raise "No categorizer set up for duration tracker #{self.inspect}" unless options[:category]
31
+ raise "No categorizer set up for duration tracker #{self.inspect}" unless options[:category]
31
32
 
32
- @categorizer = options[:category].respond_to?(:call) ? options[:category] : lambda { |request| request[options[:category]] }
33
- @durationizer = options[:duration].respond_to?(:call) ? options[:duration] : lambda { |request| request[options[:duration]] }
34
- @categories = {}
33
+ unless options[:multiple]
34
+ @categorizer = create_lambda(options[:category])
35
+ @durationizer = create_lambda(options[:duration])
36
+ end
37
+
38
+ @categories = {}
35
39
  end
36
40
 
37
41
  # Get the duration information fron the request and store it in the different categories.
38
42
  # <tt>request</tt> The request.
39
43
  def update(request)
40
44
  if options[:multiple]
41
- categories = request.every(options[:category])
42
- durations = request.every(options[:duration])
43
-
44
- if categories.length == durations.length
45
- categories.each_with_index do |category, index|
46
- @categories[category] ||= {:hits => 0, :cumulative => 0.0}
47
- @categories[category][:hits] += 1
48
- @categories[category][:cumulative] += durations[index]
49
- end
50
- else
51
- raise "Capture mismatch for multiple values in a request"
52
- end
45
+ found_categories = request.every(options[:category])
46
+ found_durations = request.every(options[:duration])
47
+ raise "Capture mismatch for multiple values in a request" unless found_categories.length == found_durations.length
48
+ found_categories.each_with_index { |cat, index| update_statistics(cat, found_durations[index]) }
53
49
  else
54
- category = @categorizer.call(request) # options[:category].respond_to?(:call) ? options[:category].call(request) : request[options[:category]]
55
- duration = @durationizer.call(request) # options[:duration].respond_to?(:call) ? options[:duration].call(request) : request[options[:duration]]
56
-
57
- if duration.kind_of?(Float) && category.kind_of?(String)
58
- @categories[category] ||= {:hits => 0, :cumulative => 0.0, :min => duration, :max => duration }
59
- @categories[category][:hits] += 1
60
- @categories[category][:cumulative] += duration
61
- @categories[category][:min] = duration if duration < @categories[category][:min]
62
- @categories[category][:max] = duration if duration > @categories[category][:max]
63
- end
50
+ category = @categorizer.call(request)
51
+ duration = @durationizer.call(request)
52
+ update_statistics(category, duration) if duration.kind_of?(Numeric) && category
64
53
  end
65
54
  end
66
-
67
- # Get the number of hits of a specific category.
68
- # <tt>cat</tt> The category
69
- def hits(cat)
70
- categories[cat][:hits]
71
- end
72
-
73
- # Get the total duration of a specific category.
74
- # <tt>cat</tt> The category
75
- def cumulative_duration(cat)
76
- categories[cat][:cumulative]
77
- end
78
-
79
- # Get the minimal duration of a specific category.
80
- # <tt>cat</tt> The category
81
- def min_duration(cat)
82
- categories[cat][:min]
83
- end
84
-
85
- # Get the maximum duration of a specific category.
86
- # <tt>cat</tt> The category
87
- def max_duration(cat)
88
- categories[cat][:max]
89
- end
90
-
91
- # Get the average duration of a specific category.
92
- # <tt>cat</tt> The category
93
- def average_duration(cat)
94
- categories[cat][:cumulative] / categories[cat][:hits]
95
- end
96
-
97
- # Get the average duration of a all categories.
98
- def overall_average_duration
99
- overall_cumulative_duration / overall_hits
100
- end
101
-
102
- # Get the cumlative duration of a all categories.
103
- def overall_cumulative_duration
104
- categories.inject(0.0) { |sum, (name, cat)| sum + cat[:cumulative] }
105
- end
106
-
107
- # Get the total hits of a all categories.
108
- def overall_hits
109
- categories.inject(0) { |sum, (name, cat)| sum + cat[:hits] }
110
- end
111
55
 
112
- # Return categories sorted by hits.
113
- def sorted_by_hits
114
- sorted_by(:hits)
115
- end
116
-
117
- # Return categories sorted by cumulative duration.
118
- def sorted_by_cumulative
119
- sorted_by(:cumulative)
120
- end
121
-
122
- # Return categories sorted by cumulative duration.
123
- def sorted_by_average
124
- sorted_by { |cat| cat[:cumulative] / cat[:hits] }
125
- end
126
-
127
- # Return categories sorted by a given key.
128
- # <tt>by</tt> The key.
129
- def sorted_by(by = nil)
130
- if block_given?
131
- categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }
132
- else
133
- categories.sort { |a, b| b[1][by] <=> a[1][by] }
134
- end
135
- end
136
-
137
56
  # Block function to build a result table using a provided sorting function.
138
57
  # <tt>output</tt> The output object.
139
58
  # <tt>amount</tt> The number of rows in the report table (default 10).
140
59
  # === Options
141
60
  # * </tt>:title</tt> The title of the table
142
61
  # * </tt>:sort</tt> The key to sort on (:hits, :cumulative, :average, :min or :max)
143
- def report_table(output, amount = 10, options = {}, &block)
144
-
145
- output.title(options[:title])
146
-
147
- top_categories = @categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }.slice(0...amount)
148
- output.table({:title => 'Category', :width => :rest},
149
- {:title => 'Hits', :align => :right, :highlight => (options[:sort] == :hits), :min_width => 4},
150
- {:title => 'Cumulative', :align => :right, :highlight => (options[:sort] == :cumulative), :min_width => 10},
151
- {:title => 'Average', :align => :right, :highlight => (options[:sort] == :average), :min_width => 8},
152
- {:title => 'Min', :align => :right, :highlight => (options[:sort] == :min)},
153
- {:title => 'Max', :align => :right, :highlight => (options[:sort] == :max)}) do |rows|
154
-
155
- top_categories.each do |(cat, info)|
156
- rows << [cat, info[:hits], "%0.02fs" % info[:cumulative], "%0.02fs" % (info[:cumulative] / info[:hits]),
157
- "%0.02fs" % info[:min], "%0.02fs" % info[:max]]
158
- end
62
+ def report_table(output, sort, options = {}, &block)
63
+ output.puts
64
+ top_categories = output.slice_results(sorted_by(sort))
65
+ output.with_style(:top_line => true) do
66
+ output.table(*statistics_header(:title => options[:title], :highlight => sort)) do |rows|
67
+ top_categories.each { |(cat, info)| rows << statistics_row(cat) }
68
+ end
69
+ end
70
+ end
71
+
72
+ # Display a duration
73
+ def display_value(time)
74
+ case time
75
+ when nil then '-'
76
+ when 0...60 then "%0.02fs" % time
77
+ when 60...3600 then "%dm%02ds" % [time / 60, (time % 60).round]
78
+ else "%dh%02dm%02ds" % [time / 3600, (time % 3600) / 60, (time % 60).round]
159
79
  end
160
80
  end
161
81
 
@@ -164,33 +84,20 @@ module RequestLogAnalyzer::Tracker
164
84
  # Any options for the report should have been set during initialize.
165
85
  # <tt>output</tt> The output object
166
86
  def report(output)
167
-
168
- options[:title] ||= 'Request duration'
169
- options[:report] ||= [:cumulative, :average]
170
- options[:top] ||= 20
171
-
172
- options[:report].each do |report|
173
- case report
174
- when :average
175
- report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by average time", :sort => :average) { |cat| cat[:cumulative] / cat[:hits] }
176
- when :cumulative
177
- report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by cumulative time", :sort => :cumulative) { |cat| cat[:cumulative] }
178
- when :hits
179
- report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by hits", :sort => :hits) { |cat| cat[:hits] }
180
- else
181
- raise "Unknown duration report specified: #{report}!"
182
- end
87
+ sortings = output.options[:sort] || [:sum, :mean]
88
+ sortings.each do |sorting|
89
+ report_table(output, sorting, :title => "#{title} - by #{sorting}")
183
90
  end
184
91
  end
185
-
92
+
186
93
  # Returns the title of this tracker for reports
187
94
  def title
188
95
  options[:title] || 'Request duration'
189
96
  end
190
-
97
+
191
98
  # Returns all the categories and the tracked duration as a hash than can be exported to YAML
192
99
  def to_yaml_object
193
- return nil if @categories.empty?
100
+ return nil if @categories.empty?
194
101
  @categories
195
102
  end
196
103
  end
@@ -1,10 +1,9 @@
1
1
  module RequestLogAnalyzer::Tracker
2
-
2
+
3
3
  # Catagorize requests by frequency.
4
- # Count and analyze requests for a specific attribute
4
+ # Count and analyze requests for a specific attribute
5
5
  #
6
6
  # === Options
7
- # * <tt>:amount</tt> The amount of lines in the report
8
7
  # * <tt>:category</tt> Proc that handles the request categorization.
9
8
  # * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
10
9
  # * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
@@ -23,41 +22,42 @@ module RequestLogAnalyzer::Tracker
23
22
  # DELETE | 512 hits (1.1%) |
24
23
  class Frequency < Base
25
24
 
26
- attr_reader :frequencies
25
+ attr_reader :categories
27
26
 
28
27
  # Check if categories are set up
29
28
  def prepare
30
29
  raise "No categorizer set up for category tracker #{self.inspect}" unless options[:category]
31
- @frequencies = {}
32
- if options[:all_categories].kind_of?(Enumerable)
33
- options[:all_categories].each { |cat| @frequencies[cat] = 0 }
34
- end
30
+ @categorizer = create_lambda(options[:category])
31
+
32
+ # Initialize the categories. Use the list of category names to
33
+ @categories = {}
34
+ options[:all_categories].each { |cat| @categories[cat] = 0 } if options[:all_categories].kind_of?(Enumerable)
35
35
  end
36
-
37
- # Check HTTP method of a request and store that in the frequencies hash.
36
+
37
+ # Check HTTP method of a request and store that in the categories hash.
38
38
  # <tt>request</tt> The request.
39
39
  def update(request)
40
- cat = options[:category].respond_to?(:call) ? options[:category].call(request) : request[options[:category]]
41
- if !cat.nil? || options[:nils]
42
- @frequencies[cat] ||= 0
43
- @frequencies[cat] += 1
40
+ cat = @categorizer.call(request)
41
+ if cat || options[:nils]
42
+ @categories[cat] ||= 0
43
+ @categories[cat] += 1
44
44
  end
45
45
  end
46
46
 
47
47
  # Return the amount of times a HTTP method has been encountered
48
48
  # <tt>cat</tt> The HTTP method (:get, :put, :post or :delete)
49
49
  def frequency(cat)
50
- frequencies[cat] || 0
50
+ categories[cat] || 0
51
51
  end
52
-
52
+
53
53
  # Return the overall frequency
54
54
  def overall_frequency
55
- frequencies.inject(0) { |carry, item| carry + item[1] }
55
+ categories.inject(0) { |carry, item| carry + item[1] }
56
56
  end
57
-
57
+
58
58
  # Return the methods sorted by frequency
59
59
  def sorted_by_frequency
60
- @frequencies.sort { |a, b| b[1] <=> a[1] }
60
+ @categories.sort { |a, b| b[1] <=> a[1] }
61
61
  end
62
62
 
63
63
  # Generate a HTTP method frequency report to the given output object.
@@ -65,29 +65,28 @@ module RequestLogAnalyzer::Tracker
65
65
  # <tt>output</tt> The output object
66
66
  def report(output)
67
67
  output.title(options[:title]) if options[:title]
68
-
69
- if @frequencies.empty?
70
- output << "None found.\n"
68
+
69
+ if @categories.empty?
70
+ output << "None found.\n"
71
71
  else
72
- sorted_frequencies = @frequencies.sort { |a, b| b[1] <=> a[1] }
73
- total_hits = sorted_frequencies.inject(0) { |carry, item| carry + item[1] }
74
- sorted_frequencies = sorted_frequencies.slice(0...options[:amount]) if options[:amount]
72
+ sorted_categories = output.slice_results(sorted_by_frequency)
73
+ total_hits = overall_frequency
75
74
 
76
- output.table({:align => :left}, {:align => :right }, {:align => :right}, {:type => :ratio, :width => :rest}) do |rows|
77
- sorted_frequencies.each do |(cat, count)|
75
+ output.table({:align => :left}, {:align => :right }, {:align => :right}, {:type => :ratio, :width => :rest}) do |rows|
76
+ sorted_categories.each do |(cat, count)|
78
77
  rows << [cat, "#{count} hits", '%0.1f%%' % ((count.to_f / total_hits.to_f) * 100.0), (count.to_f / total_hits.to_f)]
79
78
  end
80
79
  end
81
80
 
82
81
  end
83
82
  end
84
-
85
- # Returns a hash with the frequencies of every category that can be exported to YAML
83
+
84
+ # Returns a hash with the categories of every category that can be exported to YAML
86
85
  def to_yaml_object
87
- return nil if @frequencies.empty?
88
- @frequencies
86
+ return nil if @categories.empty?
87
+ @categories
89
88
  end
90
-
89
+
91
90
  # Returns the title of this tracker for reports
92
91
  def title
93
92
  options[:title] || 'Request frequency'