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
@@ -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,13 @@ 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
+ value_hash[capture[:name]] ||= converted
82
+ if converted.kind_of?(Hash) && capture[:provides].kind_of?(Hash)
83
+ capture[:provides].each do |name, type|
84
+ value_hash[name] ||= request.convert_value(converted[name], { :type => type })
85
+ end
86
+ end
77
87
  end
78
88
  return value_hash
79
89
  end
@@ -1,5 +1,8 @@
1
+ # coding: utf-8
1
2
  module RequestLogAnalyzer::Output
2
3
 
4
+ # Fixed Width output class.
5
+ # Outputs a fixed width ASCII or UF8 report.
3
6
  class FixedWidth < Base
4
7
 
5
8
  module Monochrome
@@ -126,8 +129,12 @@ module RequestLogAnalyzer::Output
126
129
  end
127
130
 
128
131
  if column_widths.include?(nil)
132
+ fill_column = columns[column_widths.index(nil)]
129
133
  width_left = options[:width] - ((columns.length - 1) * (style[:cell_separator] ? 3 : 1)) - column_widths.compact.inject(0) { |sum, col| sum + col}
130
- column_widths[column_widths.index(nil)] = width_left
134
+ column_widths[column_widths.index(nil)] = case fill_column[:type]
135
+ when :ratio; width_left # max out
136
+ else; [width_left, fill_column[:actual_width]].min
137
+ end
131
138
  end
132
139
 
133
140
  # Print table header
@@ -156,14 +163,18 @@ module RequestLogAnalyzer::Output
156
163
  bar << colorize(characters[:block] * (width.to_f * (row[index].to_f - column[:treshold])).round, :red)
157
164
  row_values.push(bar)
158
165
  else
166
+ # Create a bar by combining block characters
159
167
  row_values.push(characters[:block] * (width.to_f * row[index].to_f).round)
160
168
  end
161
169
  else
170
+ # Too few characters for a ratio bar. Display nothing
162
171
  row_values.push('')
163
172
  end
164
173
  else
165
- alignment = (columns[index][:align] == :right ? '' : '-')
166
- row_values.push("%#{alignment}#{width}s" % row[index].to_s[0...width])
174
+ alignment = (columns[index][:align] == :right ? '' : '-')
175
+ cell_value = "%#{alignment}#{width}s" % row[index].to_s[0...width]
176
+ cell_value = colorize(cell_value, :bold, :brown) if columns[index][:highlight]
177
+ row_values.push(cell_value)
167
178
  end
168
179
  end
169
180
  puts row_values.join(style[:cell_separator] ? " #{characters[:vertical_line]} " : ' ')
@@ -1,5 +1,6 @@
1
1
  module RequestLogAnalyzer::Output
2
2
 
3
+ # HTML Output class. Generated a HTML-formatted report, including CSS.
3
4
  class HTML < Base
4
5
 
5
6
  # def initialize(io, options = {})
@@ -30,6 +30,12 @@ module RequestLogAnalyzer
30
30
  end
31
31
  end
32
32
 
33
+ def convert_eval(value, capture_definition)
34
+ eval(value).inject({}) { |h, (k, v)| h[k.to_sym] = v; h}
35
+ rescue SyntaxError
36
+ nil
37
+ end
38
+
33
39
  # Slow default method to parse timestamps
34
40
  def convert_timestamp(value, capture_definition)
35
41
  DateTime.parse(value).strftime('%Y%m%d%H%M%S').to_i unless value.nil?
@@ -0,0 +1,76 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+
4
+ module RequestLogAnalyzer::Source
5
+
6
+ # Active Resource hook
7
+ class CompletedLine < ActiveRecord::Base
8
+ def convert(file_format)
9
+ RequestLogAnalyzer::Request.create(file_format, self.attributes)
10
+ end
11
+ end
12
+
13
+ # The Database class gets log data from the database.
14
+ class Database < Base
15
+
16
+ attr_reader :source_files
17
+ attr_reader :requests
18
+
19
+ # Initializes the log file parser instance.
20
+ # It will apply the language specific FileFormat module to this instance. It will use the line
21
+ # definitions in this module to parse any input that it is given (see parse_io).
22
+ #
23
+ # <tt>format</tt>:: The current file format instance
24
+ # <tt>options</tt>:: A hash of options that are used by the parser
25
+ def initialize(format, options = {})
26
+ @line_definitions = {}
27
+ @options = options
28
+ @source_files = options[:source_files]
29
+ @parsed_requests = 0
30
+ @requests = []
31
+
32
+ self.register_file_format(format)
33
+ end
34
+
35
+ # Reads the input, which can either be a file, sequence of files or STDIN to parse
36
+ # lines specified in the FileFormat. This lines will be combined into Request instances,
37
+ # that will be yielded. The actual parsing occurs in the parse_io method.
38
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
39
+ def each_request(options = {}, &block) # :yields: request
40
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => @source_files)
41
+
42
+ @progress_handler.call(:started, @source_files) if @progress_handler
43
+ RequestLogAnalyzer::Source::CompletedLine.find(:all).each do |request|
44
+ @parsed_requests += 1
45
+ yield(request.convert(self.file_format))
46
+ end
47
+
48
+ @progress_handler.call(:finished, @source_files) if @progress_handler
49
+ end
50
+
51
+ # Add a block to this method to install a progress handler while parsing.
52
+ # <tt>proc</tt>:: The proc that will be called to handle progress update messages
53
+ def progress=(proc)
54
+ @progress_handler = proc
55
+ end
56
+
57
+ # Add a block to this method to install a warning handler while parsing,
58
+ # <tt>proc</tt>:: The proc that will be called to handle parse warning messages
59
+ def warning=(proc)
60
+ @warning_handler = proc
61
+ end
62
+
63
+ # This method is called by the parser if it encounteres any parsing problems.
64
+ # It will call the installed warning handler if any.
65
+ #
66
+ # By default, RequestLogAnalyzer::Controller will install a warning handler
67
+ # that will pass the warnings to each aggregator so they can do something useful
68
+ # with it.
69
+ #
70
+ # <tt>type</tt>:: The warning type (a Symbol)
71
+ # <tt>message</tt>:: A message explaining the warning
72
+ def warn(type, message)
73
+ @warning_handler.call(type, message, @current_io.lineno) if @warning_handler
74
+ end
75
+ end
76
+ end
@@ -7,15 +7,24 @@ module RequestLogAnalyzer::Source
7
7
  # De order in which lines occur is used to combine lines to a single request. If these lines
8
8
  # are mixed, requests cannot be combined properly. This can be the case if data is written to
9
9
  # the log file simultaneously by different mongrel processes. This problem is detected by the
10
- # parser, but the requests that are mixed up cannot be parsed. It will emit warnings when this
11
- # occurs.
10
+ # parser. It will emit warnings when this occurs. LogParser supports multiple parse strategies
11
+ # that deal differently with this problem.
12
12
  class LogParser < Base
13
13
 
14
+ # The default parse strategy that will be used to parse the input.
15
+ DEFAULT_PARSE_STRATEGY = 'assume-correct'
16
+
17
+ # All available parse strategies.
18
+ PARSE_STRATEGIES = ['cautious', 'assume-correct']
19
+
14
20
  attr_reader :source_files
15
21
 
16
- # Initializes the parser instance.
22
+ # Initializes the log file parser instance.
17
23
  # It will apply the language specific FileFormat module to this instance. It will use the line
18
- # definitions in this module to parse any input.
24
+ # definitions in this module to parse any input that it is given (see parse_io).
25
+ #
26
+ # <tt>format</tt>:: The current file format instance
27
+ # <tt>options</tt>:: A hash of options that are used by the parser
19
28
  def initialize(format, options = {})
20
29
  @line_definitions = {}
21
30
  @options = options
@@ -25,13 +34,18 @@ module RequestLogAnalyzer::Source
25
34
  @skipped_requests = 0
26
35
  @current_io = nil
27
36
  @source_files = options[:source_files]
28
-
29
- # install the file format module (see RequestLogAnalyzer::FileFormat)
30
- # and register all the line definitions to the parser
37
+
38
+ @options[:parse_strategy] ||= DEFAULT_PARSE_STRATEGY
39
+ raise "Unknown parse strategy" unless PARSE_STRATEGIES.include?(@options[:parse_strategy])
40
+
31
41
  self.register_file_format(format)
32
42
  end
33
43
 
34
- def each_request(options = {}, &block)
44
+ # Reads the input, which can either be a file, sequence of files or STDIN to parse
45
+ # lines specified in the FileFormat. This lines will be combined into Request instances,
46
+ # that will be yielded. The actual parsing occurs in the parse_io method.
47
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
48
+ def each_request(options = {}, &block) # :yields: request
35
49
 
36
50
  case @source_files
37
51
  when IO;
@@ -46,35 +60,46 @@ module RequestLogAnalyzer::Source
46
60
  end
47
61
  end
48
62
 
49
- # Parses a list of consequent files of the same format
50
- def parse_files(files, options = {}, &block)
63
+ # Parses a list of subsequent files of the same format, by calling parse_file for every
64
+ # file in the array.
65
+ # <tt>files</tt>:: The Array of files that should be parsed
66
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
67
+ def parse_files(files, options = {}, &block) # :yields: request
51
68
  files.each { |file| parse_file(file, options, &block) }
52
69
  end
53
70
 
54
- # Parses a file.
55
- # Creates an IO stream for the provided file, and sends it to parse_io for further handling
71
+ # Parses a log file. Creates an IO stream for the provided file, and sends it to parse_io for
72
+ # further handling. This method supports progress updates that can be used to display a progressbar
73
+ # <tt>file</tt>:: The file that should be parsed.
74
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
56
75
  def parse_file(file, options = {}, &block)
57
76
  @progress_handler.call(:started, file) if @progress_handler
58
77
  File.open(file, 'r') { |f| parse_io(f, options, &block) }
59
78
  @progress_handler.call(:finished, file) if @progress_handler
60
79
  end
61
80
 
81
+
82
+ # Parses an IO stream. It will simply call parse_io. This function does not support progress updates
83
+ # because the length of a stream is not known.
84
+ # <tt>stream</tt>:: The IO stream that should be parsed.
85
+ # <tt>options</tt>:: A Hash of options that will be pased to parse_io.
62
86
  def parse_stream(stream, options = {}, &block)
63
87
  parse_io(stream, options, &block)
64
88
  end
65
89
 
66
- # Finds a log line and then parses the information in the line.
67
- # Yields a hash containing the information found.
68
- # <tt>*line_types</tt> The log line types to look for (defaults to LOG_LINES.keys).
69
- # Yeilds a Hash when it encounters a chunk of information.
70
- def parse_io(io, options = {}, &block)
71
-
72
- # parse every line type by default
73
- line_types = options[:line_types] || file_format.line_definitions.keys
74
-
75
- # check whether all provided line types are valid
76
- unknown = line_types.reject { |line_type| file_format.line_definitions.has_key?(line_type) }
77
- raise "Unknown line types: #{unknown.join(', ')}" unless unknown.empty?
90
+ # This method loops over each line of the input stream. It will try to parse this line as any of
91
+ # the lines that are defined by the current file format (see RequestLogAnalyazer::FileFormat).
92
+ # It will then combine these parsed line into requests using heuristics. These requests (see
93
+ # RequestLogAnalyzer::Request) will then be yielded for further processing in the pipeline.
94
+ #
95
+ # - RequestLogAnalyzer::LineDefinition#matches is called to test if a line matches a line definition of the file format.
96
+ # - update_current_request is used to combine parsed lines into requests using heuristics.
97
+ # - The method will yield progress updates if a progress handler is installed using progress=
98
+ # - The method will yield parse warnings if a warning handler is installed using warning=
99
+ #
100
+ # <tt>io</tt>:: The IO instance to use as source
101
+ # <tt>options</tt>:: A hash of options that can be used by the parser.
102
+ def parse_io(io, options = {}, &block) # :yields: request
78
103
 
79
104
  @current_io = io
80
105
  @current_io.each_line do |line|
@@ -82,11 +107,11 @@ module RequestLogAnalyzer::Source
82
107
  @progress_handler.call(:progress, @current_io.pos) if @progress_handler && @current_io.kind_of?(File)
83
108
 
84
109
  request_data = nil
85
- line_types.each do |line_type|
86
- line_type_definition = file_format.line_definitions[line_type]
87
- break if request_data = line_type_definition.matches(line, @current_io.lineno, self)
110
+ file_format.line_definitions.each do |line_type, definition|
111
+ request_data = definition.matches(line, @current_io.lineno, self)
112
+ break if request_data
88
113
  end
89
-
114
+
90
115
  if request_data
91
116
  @parsed_lines += 1
92
117
  update_current_request(request_data, &block)
@@ -98,19 +123,27 @@ module RequestLogAnalyzer::Source
98
123
  @current_io = nil
99
124
  end
100
125
 
101
- # Add a block to this method to install a progress handler while parsing
126
+ # Add a block to this method to install a progress handler while parsing.
127
+ # <tt>proc</tt>:: The proc that will be called to handle progress update messages
102
128
  def progress=(proc)
103
129
  @progress_handler = proc
104
130
  end
105
131
 
106
- # Add a block to this method to install a warning handler while parsing
132
+ # Add a block to this method to install a warning handler while parsing,
133
+ # <tt>proc</tt>:: The proc that will be called to handle parse warning messages
107
134
  def warning=(proc)
108
135
  @warning_handler = proc
109
136
  end
110
137
 
111
- # This method is called by the parser if it encounteres any problems.
112
- # It will call the warning handler. The default controller will pass all warnings to every
113
- # aggregator that is registered and running
138
+ # This method is called by the parser if it encounteres any parsing problems.
139
+ # It will call the installed warning handler if any.
140
+ #
141
+ # By default, RequestLogAnalyzer::Controller will install a warning handler
142
+ # that will pass the warnings to each aggregator so they can do something useful
143
+ # with it.
144
+ #
145
+ # <tt>type</tt>:: The warning type (a Symbol)
146
+ # <tt>message</tt>:: A message explaining the warning
114
147
  def warn(type, message)
115
148
  @warning_handler.call(type, message, @current_io.lineno) if @warning_handler
116
149
  end
@@ -121,25 +154,38 @@ module RequestLogAnalyzer::Source
121
154
  # new request when a header line is encountered en will emit the request when a footer line
122
155
  # is encountered.
123
156
  #
157
+ # Combining the lines is done using heuristics. Problems can occur in this process. The
158
+ # current parse strategy defines how these cases are handled.
159
+ #
160
+ # When using the 'assume-correct' parse strategy (default):
161
+ # - Every line that is parsed before a header line is ignored as it cannot be included in
162
+ # any request. It will emit a :no_current_request warning.
163
+ # - If a header line is found before the previous requests was closed, the previous request
164
+ # will be yielded and a new request will be started.
165
+ #
166
+ # When using the 'cautious' parse strategy:
124
167
  # - Every line that is parsed before a header line is ignored as it cannot be included in
125
168
  # any request. It will emit a :no_current_request warning.
126
169
  # - A header line that is parsed before a request is closed by a footer line, is a sign of
127
- # an unprpertly ordered file. All data that is gathered for the request until then is
128
- # discarded, the next request is ignored as well and a :unclosed_request warning is
170
+ # an unproperly ordered file. All data that is gathered for the request until then is
171
+ # discarded and the next request is ignored as well. An :unclosed_request warning is
129
172
  # emitted.
130
- def update_current_request(request_data, &block)
173
+ #
174
+ # <tt>request_data</tt>:: A hash of data that was parsed from the last line.
175
+ def update_current_request(request_data, &block) # :yields: request
131
176
  if header_line?(request_data)
132
177
  unless @current_request.nil?
133
- if options[:assume_correct_order]
134
- handle_request(@current_request, &block) #yield @current_request
135
- @current_request = @file_format.create_request(request_data)
136
- else
178
+ case options[:parse_strategy]
179
+ when 'assume-correct'
180
+ handle_request(@current_request, &block)
181
+ @current_request = @file_format.request(request_data)
182
+ when 'cautious'
137
183
  @skipped_lines += 1
138
- warn(:unclosed_request, "Encountered header line, but previous request was not closed!")
184
+ warn(:unclosed_request, "Encountered header line (#{request_data[:line_definition].name.inspect}), but previous request was not closed!")
139
185
  @current_request = nil # remove all data that was parsed, skip next request as well.
140
186
  end
141
187
  else
142
- @current_request = @file_format.create_request(request_data)
188
+ @current_request = @file_format.request(request_data)
143
189
  end
144
190
  else
145
191
  unless @current_request.nil?
@@ -150,26 +196,33 @@ module RequestLogAnalyzer::Source
150
196
  end
151
197
  else
152
198
  @skipped_lines += 1
153
- warn(:no_current_request, "Parsebale line found outside of a request!")
199
+ warn(:no_current_request, "Parsebale line (#{request_data[:line_definition].name.inspect}) found outside of a request!")
154
200
  end
155
201
  end
156
202
  end
157
203
 
158
- # Handles the parsed request by calling the request handler.
159
- # The default controller will send the request to every running aggegator.
160
- def handle_request(request, &block)
204
+ # Handles the parsed request by sending it into the pipeline.
205
+ #
206
+ # - It will call RequestLogAnalyzer::Request#validate on the request instance
207
+ # - It will send the request into the pipeline, checking whether it was accepted by all the filters.
208
+ # - It will update the parsed_requests and skipped_requests variables accordingly
209
+ #
210
+ # <tt>request</tt>:: The parsed request instance (RequestLogAnalyzer::Request)
211
+ def handle_request(request, &block) # :yields: request
161
212
  @parsed_requests += 1
162
213
  request.validate
163
214
  accepted = block_given? ? yield(request) : true
164
215
  @skipped_requests += 1 if not accepted
165
216
  end
166
217
 
167
- # Checks whether a given line hash is a header line.
218
+ # Checks whether a given line hash is a header line according to the current file format.
219
+ # <tt>hash</tt>:: A hash of data that was parsed from the line.
168
220
  def header_line?(hash)
169
221
  hash[:line_definition].header
170
222
  end
171
223
 
172
- # Checks whether a given line hash is a footer line.
224
+ # Checks whether a given line hash is a footer line according to the current file format.
225
+ # <tt>hash</tt>:: A hash of data that was parsed from the line.
173
226
  def footer_line?(hash)
174
227
  hash[:line_definition].footer
175
228
  end
@@ -1,11 +1,27 @@
1
+ # The RequestLogAnalyzer::Source module contains all functionality that loads requests from a given source
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
5
+ # the RequestLogAnalyzer chain.
6
+ #
7
+ # - The base class for all sources is RequestLogAnalyzer::Source::Base. All source classes should inherit from this class.
8
+ # - Currently, RequestLogAnalyzer::Source::LogParser is the only implemented source.
1
9
  module RequestLogAnalyzer::Source
2
10
 
11
+ # Loads constants that reside in the RequestLogAnalyzer::Source namespace. This function uses
12
+ # RequestLogAnalyzer::load_default_class_file to load the file in which the constant is declared.
13
+ # <tt>const</tt>:: The constant to load in the RequestLogAnalyzer::Source namespace.
3
14
  def self.const_missing(const)
4
15
  RequestLogAnalyzer::load_default_class_file(self, const)
5
16
  end
6
17
 
18
+ # The base Source class. All other sources should inherit from this class.
19
+ #
20
+ # A source implememtation should at least implement the each_request method, which should yield
21
+ # RequestLogAnalyzer::Request instances that will be fed through the pipleine.
7
22
  class Base
8
23
 
24
+ # Make the Spurce instance aware of the current file format
9
25
  include RequestLogAnalyzer::FileFormat::Awareness
10
26
 
11
27
  # A hash of options
@@ -23,23 +39,29 @@ module RequestLogAnalyzer::Source
23
39
  # The number of skipped lines because of warnings
24
40
  attr_reader :skipped_lines
25
41
 
26
- # Base source class used to filter input requests.
27
-
28
- # Initializer
29
- # <tt>format</tt> The file format
30
- # <tt>options</tt> Are passed to the filters.
42
+ # Initializer, which will register the file format and save any options given as a hash.
43
+ # <tt>format</tt>:: The file format instance
44
+ # <tt>options</tt>:: A hash of options that can be used by a specific Source implementation
31
45
  def initialize(format, options = {})
32
46
  @options = options
33
47
  register_file_format(format)
34
48
  end
35
49
 
50
+ # The prepare method is called before the RequestLogAnalyzer::Source::Base#each_request method is called.
51
+ # Use this method to implement any initialization that should occur before this source can produce Request
52
+ # instances.
36
53
  def prepare
37
54
  end
38
55
 
39
- def requests(&block)
56
+ # This function is called to actually produce the requests that will be send into the pipeline.
57
+ # The implementation should yield instances of RequestLogAnalyzer::Request.
58
+ # <tt>options</tt>:: A Hash of options that can be used in the implementation.
59
+ def each_request(options = {}, &block) # :yields: request
40
60
  return true
41
61
  end
42
62
 
63
+ # This function is called after RequestLogAnalyzer::Source::Base#each_request finished. Any code to
64
+ # wrap up, free resources, etc. can be put in this method.
43
65
  def finalize
44
66
  end
45
67
 
@@ -31,26 +31,101 @@ module RequestLogAnalyzer::Tracker
31
31
  end
32
32
 
33
33
  def update(request)
34
- category = options[:category].respond_to?(:call) ? options[:category].call(request) : request[options[:category]]
35
- duration = options[:duration].respond_to?(:call) ? options[:duration].call(request) : request[options[:duration]]
34
+ if options[:multiple]
35
+ categories = request.every(options[:category])
36
+ durations = request.every(options[:duration])
37
+
38
+ if categories.length == durations.length
39
+ categories.each_with_index do |category, index|
40
+ @categories[category] ||= {:hits => 0, :cumulative => 0.0}
41
+ @categories[category][:hits] += 1
42
+ @categories[category][:cumulative] += durations[index]
43
+ end
44
+ else
45
+ raise "Capture mismatch for multiple values in a request"
46
+ end
47
+ else
48
+ category = options[:category].respond_to?(:call) ? options[:category].call(request) : request[options[:category]]
49
+ duration = options[:duration].respond_to?(:call) ? options[:duration].call(request) : request[options[:duration]]
36
50
 
37
- if !duration.nil? && !category.nil?
38
- @categories[category] ||= {:count => 0, :total_duration => 0.0}
39
- @categories[category][:count] += 1
40
- @categories[category][:total_duration] += duration
51
+ if duration.kind_of?(Float) && category.kind_of?(String)
52
+ @categories[category] ||= {:hits => 0, :cumulative => 0.0, :min => duration, :max => duration }
53
+ @categories[category][:hits] += 1
54
+ @categories[category][:cumulative] += duration
55
+ @categories[category][:min] = duration if duration < @categories[category][:min]
56
+ @categories[category][:max] = duration if duration > @categories[category][:max]
57
+ end
41
58
  end
42
59
  end
60
+
61
+ def hits(cat)
62
+ categories[cat][:hits]
63
+ end
64
+
65
+ def cumulative_duration(cat)
66
+ categories[cat][:cumulative]
67
+ end
43
68
 
69
+ def min_duration(cat)
70
+ categories[cat][:min]
71
+ end
72
+
73
+ def max_duration(cat)
74
+ categories[cat][:max]
75
+ end
76
+
77
+ def average_duration(cat)
78
+ categories[cat][:cumulative] / categories[cat][:hits]
79
+ end
80
+
81
+ def overall_average_duration
82
+ overall_cumulative_duration / overall_hits
83
+ end
84
+
85
+ def overall_cumulative_duration
86
+ categories.inject(0.0) { |sum, (name, cat)| sum + cat[:cumulative] }
87
+ end
88
+
89
+ def overall_hits
90
+ categories.inject(0) { |sum, (name, cat)| sum + cat[:hits] }
91
+ end
92
+
93
+ def sorted_by_hits
94
+ sorted_by(:hits)
95
+ end
96
+
97
+ def sorted_by_cumulative
98
+ sorted_by(:cumulative)
99
+ end
100
+
101
+ def sorted_by_average
102
+ sorted_by { |cat| cat[:cumulative] / cat[:hits] }
103
+ end
104
+
105
+ def sorted_by(by = nil)
106
+ if block_given?
107
+ categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }
108
+ else
109
+ categories.sort { |a, b| b[1][by] <=> a[1][by] }
110
+ end
111
+ end
112
+
113
+ # Builds a result table using a provided sorting function
44
114
  def report_table(output, amount = 10, options = {}, &block)
45
115
 
46
116
  output.title(options[:title])
47
117
 
48
118
  top_categories = @categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }.slice(0...amount)
49
- output.table({:title => 'Action'}, {:title => 'Hits', :align => :right, :min_width => 4},
50
- {:title => 'Cumulative', :align => :right, :min_width => 10}, {:title => 'Average', :align => :right, :min_width => 8}) do |rows|
119
+ output.table({:title => 'Category', :width => :rest},
120
+ {:title => 'Hits', :align => :right, :highlight => (options[:sort] == :hits), :min_width => 4},
121
+ {:title => 'Cumulative', :align => :right, :highlight => (options[:sort] == :cumulative), :min_width => 10},
122
+ {:title => 'Average', :align => :right, :highlight => (options[:sort] == :average), :min_width => 8},
123
+ {:title => 'Min', :align => :right, :highlight => (options[:sort] == :min)},
124
+ {:title => 'Max', :align => :right, :highlight => (options[:sort] == :max)}) do |rows|
51
125
 
52
126
  top_categories.each do |(cat, info)|
53
- rows << [cat, info[:count], "%0.02fs" % info[:total_duration], "%0.02fs" % (info[:total_duration] / info[:count])]
127
+ rows << [cat, info[:hits], "%0.02fs" % info[:cumulative], "%0.02fs" % (info[:cumulative] / info[:hits]),
128
+ "%0.02fs" % info[:min], "%0.02fs" % info[:max]]
54
129
  end
55
130
  end
56
131
 
@@ -59,19 +134,19 @@ module RequestLogAnalyzer::Tracker
59
134
  def report(output)
60
135
 
61
136
  options[:title] ||= 'Request duration'
62
- options[:report] ||= [:total, :average]
137
+ options[:report] ||= [:cumulative, :average]
63
138
  options[:top] ||= 20
64
139
 
65
140
  options[:report].each do |report|
66
141
  case report
67
142
  when :average
68
- report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by average time") { |request| request[:total_duration] / request[:count] }
69
- when :total
70
- report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by cumulative time") { |request| request[:total_duration] }
143
+ report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by average time", :sort => :average) { |cat| cat[:cumulative] / cat[:hits] }
144
+ when :cumulative
145
+ report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by cumulative time", :sort => :cumulative) { |cat| cat[:cumulative] }
71
146
  when :hits
72
- report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by hits") { |request| request[:count] }
147
+ report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by hits", :sort => :hits) { |cat| cat[:hits] }
73
148
  else
74
- output << "Unknown duration report specified\n"
149
+ raise "Unknown duration report specified: #{report}!"
75
150
  end
76
151
  end
77
152
  end