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.
- data/DESIGN +24 -10
- data/README.rdoc +1 -1
- data/Rakefile +1 -2
- data/bin/request-log-analyzer +4 -27
- data/lib/cli/progressbar.rb +2 -19
- data/lib/cli/tools.rb +46 -0
- data/lib/request_log_analyzer/aggregator/database.rb +33 -8
- data/lib/request_log_analyzer/aggregator/echo.rb +1 -0
- data/lib/request_log_analyzer/aggregator/summarizer.rb +15 -13
- data/lib/request_log_analyzer/controller.rb +8 -4
- data/lib/request_log_analyzer/file_format/merb.rb +17 -4
- data/lib/request_log_analyzer/file_format/rails.rb +2 -2
- data/lib/request_log_analyzer/file_format/rails_development.rb +30 -92
- data/lib/request_log_analyzer/file_format.rb +8 -4
- data/lib/request_log_analyzer/filter/anonymize.rb +0 -3
- data/lib/request_log_analyzer/filter/field.rb +6 -1
- data/lib/request_log_analyzer/filter/timespan.rb +7 -1
- data/lib/request_log_analyzer/filter.rb +0 -4
- data/lib/request_log_analyzer/line_definition.rb +12 -2
- data/lib/request_log_analyzer/output/fixed_width.rb +14 -3
- data/lib/request_log_analyzer/output/html.rb +1 -0
- data/lib/request_log_analyzer/request.rb +6 -0
- data/lib/request_log_analyzer/source/database.rb +76 -0
- data/lib/request_log_analyzer/source/log_parser.rb +101 -48
- data/lib/request_log_analyzer/source.rb +28 -6
- data/lib/request_log_analyzer/tracker/duration.rb +90 -15
- data/lib/request_log_analyzer/tracker/frequency.rb +22 -10
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +20 -9
- data/lib/request_log_analyzer/tracker/timespan.rb +15 -8
- data/lib/request_log_analyzer.rb +29 -16
- data/spec/integration/command_line_usage_spec.rb +71 -0
- data/spec/lib/helper.rb +33 -0
- data/spec/lib/mocks.rb +47 -0
- data/spec/{file_formats/spec_format.rb → lib/testing_format.rb} +6 -1
- data/spec/spec_helper.rb +5 -41
- data/spec/{database_inserter_spec.rb → unit/aggregator/database_inserter_spec.rb} +40 -37
- data/spec/unit/aggregator/summarizer_spec.rb +28 -0
- data/spec/unit/controller/controller_spec.rb +43 -0
- data/spec/{log_processor_spec.rb → unit/controller/log_processor_spec.rb} +4 -3
- data/spec/{file_format_spec.rb → unit/file_format/file_format_api_spec.rb} +16 -4
- data/spec/{line_definition_spec.rb → unit/file_format/line_definition_spec.rb} +13 -6
- data/spec/{merb_format_spec.rb → unit/file_format/merb_format_spec.rb} +2 -2
- data/spec/{rails_format_spec.rb → unit/file_format/rails_format_spec.rb} +19 -11
- data/spec/unit/filter/anonymize_filter_spec.rb +22 -0
- data/spec/unit/filter/field_filter_spec.rb +69 -0
- data/spec/unit/filter/timespan_filter_spec.rb +61 -0
- data/spec/{log_parser_spec.rb → unit/source/log_parser_spec.rb} +7 -7
- data/spec/{request_spec.rb → unit/source/request_spec.rb} +5 -5
- data/spec/unit/tracker/duration_tracker_spec.rb +99 -0
- data/spec/unit/tracker/frequency_tracker_spec.rb +83 -0
- data/spec/unit/tracker/hourly_spread_spec.rb +75 -0
- data/spec/unit/tracker/timespan_tracker_spec.rb +65 -0
- data/spec/unit/tracker/tracker_api_test.rb +45 -0
- data/tasks/rspec.rake +12 -0
- metadata +54 -26
- data/spec/controller_spec.rb +0 -64
- data/spec/filter_spec.rb +0 -157
- 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
|
|
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
|
|
@@ -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
|
-
|
|
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)] =
|
|
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
|
-
|
|
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]} " : ' ')
|
|
@@ -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
|
|
11
|
-
#
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
50
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
break if request_data
|
|
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
|
|
113
|
-
#
|
|
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
|
|
128
|
-
# discarded
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
@current_request
|
|
136
|
-
|
|
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.
|
|
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
|
|
159
|
-
#
|
|
160
|
-
|
|
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
|
-
#
|
|
27
|
-
|
|
28
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 => '
|
|
50
|
-
{:title => '
|
|
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[:
|
|
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] ||= [:
|
|
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") { |
|
|
69
|
-
when :
|
|
70
|
-
report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by cumulative time") { |
|
|
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") { |
|
|
147
|
+
report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by hits", :sort => :hits) { |cat| cat[:hits] }
|
|
73
148
|
else
|
|
74
|
-
|
|
149
|
+
raise "Unknown duration report specified: #{report}!"
|
|
75
150
|
end
|
|
76
151
|
end
|
|
77
152
|
end
|