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.
- data/LICENSE +3 -3
- data/README.rdoc +1 -1
- data/bin/request-log-analyzer +17 -14
- data/lib/cli/command_line_arguments.rb +51 -51
- data/lib/cli/database_console.rb +3 -3
- data/lib/cli/database_console_init.rb +4 -3
- data/lib/cli/progressbar.rb +10 -10
- data/lib/cli/tools.rb +3 -3
- data/lib/request_log_analyzer/aggregator/database_inserter.rb +19 -17
- data/lib/request_log_analyzer/aggregator/echo.rb +14 -9
- data/lib/request_log_analyzer/aggregator/summarizer.rb +26 -26
- data/lib/request_log_analyzer/aggregator.rb +11 -15
- data/lib/request_log_analyzer/controller.rb +162 -89
- data/lib/request_log_analyzer/database/base.rb +22 -21
- data/lib/request_log_analyzer/database/connection.rb +3 -3
- data/lib/request_log_analyzer/database/request.rb +22 -0
- data/lib/request_log_analyzer/database/source.rb +13 -0
- data/lib/request_log_analyzer/database/warning.rb +14 -0
- data/lib/request_log_analyzer/database.rb +28 -103
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +20 -20
- data/lib/request_log_analyzer/file_format/apache.rb +37 -30
- data/lib/request_log_analyzer/file_format/merb.rb +30 -13
- data/lib/request_log_analyzer/file_format/rack.rb +11 -0
- data/lib/request_log_analyzer/file_format/rails.rb +143 -73
- data/lib/request_log_analyzer/file_format/rails_development.rb +4 -49
- data/lib/request_log_analyzer/file_format.rb +16 -28
- data/lib/request_log_analyzer/filter/anonymize.rb +8 -7
- data/lib/request_log_analyzer/filter/field.rb +9 -9
- data/lib/request_log_analyzer/filter/timespan.rb +12 -10
- data/lib/request_log_analyzer/filter.rb +12 -16
- data/lib/request_log_analyzer/line_definition.rb +15 -14
- data/lib/request_log_analyzer/log_processor.rb +27 -29
- data/lib/request_log_analyzer/mailer.rb +15 -9
- data/lib/request_log_analyzer/output/fixed_width.rb +48 -48
- data/lib/request_log_analyzer/output/html.rb +20 -20
- data/lib/request_log_analyzer/output.rb +53 -12
- data/lib/request_log_analyzer/request.rb +88 -69
- data/lib/request_log_analyzer/source/database_loader.rb +10 -14
- data/lib/request_log_analyzer/source/log_parser.rb +57 -50
- data/lib/request_log_analyzer/source.rb +10 -12
- data/lib/request_log_analyzer/tracker/duration.rb +41 -134
- data/lib/request_log_analyzer/tracker/frequency.rb +31 -32
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +21 -21
- data/lib/request_log_analyzer/tracker/timespan.rb +17 -17
- data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
- data/lib/request_log_analyzer/tracker.rb +139 -32
- data/lib/request_log_analyzer.rb +5 -5
- data/request-log-analyzer.gemspec +19 -15
- data/spec/database.yml +6 -0
- data/spec/fixtures/rails_22.log +1 -1
- data/spec/integration/command_line_usage_spec.rb +7 -1
- data/spec/lib/helpers.rb +7 -7
- data/spec/lib/macros.rb +3 -3
- data/spec/lib/matchers.rb +41 -27
- data/spec/lib/mocks.rb +19 -15
- data/spec/lib/testing_format.rb +9 -9
- data/spec/spec_helper.rb +6 -6
- data/spec/unit/aggregator/database_inserter_spec.rb +16 -16
- data/spec/unit/aggregator/summarizer_spec.rb +4 -4
- data/spec/unit/controller/controller_spec.rb +2 -2
- data/spec/unit/controller/log_processor_spec.rb +1 -1
- data/spec/unit/database/base_class_spec.rb +26 -33
- data/spec/unit/database/connection_spec.rb +3 -3
- data/spec/unit/database/database_spec.rb +33 -38
- data/spec/unit/file_format/amazon_s3_format_spec.rb +5 -5
- data/spec/unit/file_format/apache_format_spec.rb +48 -11
- data/spec/unit/file_format/file_format_api_spec.rb +13 -13
- data/spec/unit/file_format/line_definition_spec.rb +24 -17
- data/spec/unit/file_format/merb_format_spec.rb +41 -45
- data/spec/unit/file_format/rails_format_spec.rb +157 -117
- data/spec/unit/filter/anonymize_filter_spec.rb +2 -2
- data/spec/unit/filter/field_filter_spec.rb +13 -13
- data/spec/unit/filter/filter_spec.rb +1 -1
- data/spec/unit/filter/timespan_filter_spec.rb +15 -15
- data/spec/unit/mailer_spec.rb +30 -0
- data/spec/unit/{source/request_spec.rb → request_spec.rb} +30 -30
- data/spec/unit/source/log_parser_spec.rb +27 -27
- data/spec/unit/tracker/duration_tracker_spec.rb +115 -78
- data/spec/unit/tracker/frequency_tracker_spec.rb +74 -63
- data/spec/unit/tracker/hourly_spread_spec.rb +28 -20
- data/spec/unit/tracker/timespan_tracker_spec.rb +25 -13
- data/spec/unit/tracker/tracker_api_spec.rb +117 -42
- data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
- data/tasks/github-gem.rake +125 -75
- data/tasks/request_log_analyzer.rake +2 -2
- 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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
119
|
+
|
|
113
120
|
@current_source = nil
|
|
114
|
-
|
|
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 %
|
|
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
|
|
52
|
-
|
|
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}"
|
|
31
|
+
raise "No categorizer set up for duration tracker #{self.inspect}" unless options[:category]
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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)
|
|
55
|
-
duration = @durationizer.call(request)
|
|
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,
|
|
144
|
-
|
|
145
|
-
output.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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 :
|
|
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
|
-
@
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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 =
|
|
41
|
-
if
|
|
42
|
-
@
|
|
43
|
-
@
|
|
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
|
-
|
|
50
|
+
categories[cat] || 0
|
|
51
51
|
end
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
# Return the overall frequency
|
|
54
54
|
def overall_frequency
|
|
55
|
-
|
|
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
|
-
@
|
|
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 @
|
|
70
|
-
output << "None found.\n"
|
|
68
|
+
|
|
69
|
+
if @categories.empty?
|
|
70
|
+
output << "None found.\n"
|
|
71
71
|
else
|
|
72
|
-
|
|
73
|
-
total_hits
|
|
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
|
-
|
|
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
|
|
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 @
|
|
88
|
-
@
|
|
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'
|