ngmoco-request-log-analyzer 1.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/DESIGN.rdoc +41 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +8 -0
- data/bin/request-log-analyzer +114 -0
- data/lib/cli/command_line_arguments.rb +301 -0
- data/lib/cli/database_console.rb +26 -0
- data/lib/cli/database_console_init.rb +43 -0
- data/lib/cli/progressbar.rb +213 -0
- data/lib/cli/tools.rb +46 -0
- data/lib/request_log_analyzer.rb +44 -0
- data/lib/request_log_analyzer/aggregator.rb +49 -0
- data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
- data/lib/request_log_analyzer/aggregator/echo.rb +29 -0
- data/lib/request_log_analyzer/aggregator/summarizer.rb +175 -0
- data/lib/request_log_analyzer/controller.rb +332 -0
- data/lib/request_log_analyzer/database.rb +102 -0
- data/lib/request_log_analyzer/database/base.rb +115 -0
- data/lib/request_log_analyzer/database/connection.rb +38 -0
- 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/file_format.rb +160 -0
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +71 -0
- data/lib/request_log_analyzer/file_format/apache.rb +141 -0
- data/lib/request_log_analyzer/file_format/merb.rb +67 -0
- data/lib/request_log_analyzer/file_format/rack.rb +11 -0
- data/lib/request_log_analyzer/file_format/rails.rb +176 -0
- data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
- data/lib/request_log_analyzer/filter.rb +30 -0
- data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
- data/lib/request_log_analyzer/filter/field.rb +42 -0
- data/lib/request_log_analyzer/filter/timespan.rb +45 -0
- data/lib/request_log_analyzer/line_definition.rb +111 -0
- data/lib/request_log_analyzer/log_processor.rb +99 -0
- data/lib/request_log_analyzer/mailer.rb +62 -0
- data/lib/request_log_analyzer/output.rb +113 -0
- data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
- data/lib/request_log_analyzer/output/html.rb +184 -0
- data/lib/request_log_analyzer/request.rb +175 -0
- data/lib/request_log_analyzer/source.rb +72 -0
- data/lib/request_log_analyzer/source/database_loader.rb +87 -0
- data/lib/request_log_analyzer/source/log_parser.rb +274 -0
- data/lib/request_log_analyzer/tracker.rb +206 -0
- data/lib/request_log_analyzer/tracker/duration.rb +104 -0
- data/lib/request_log_analyzer/tracker/frequency.rb +95 -0
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +107 -0
- data/lib/request_log_analyzer/tracker/timespan.rb +81 -0
- data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
- data/request-log-analyzer.gemspec +40 -0
- data/spec/database.yml +23 -0
- data/spec/fixtures/apache_combined.log +5 -0
- data/spec/fixtures/apache_common.log +10 -0
- data/spec/fixtures/decompression.log +12 -0
- data/spec/fixtures/decompression.log.bz2 +0 -0
- data/spec/fixtures/decompression.log.gz +0 -0
- data/spec/fixtures/decompression.log.zip +0 -0
- data/spec/fixtures/decompression.tar.gz +0 -0
- data/spec/fixtures/decompression.tgz +0 -0
- data/spec/fixtures/header_and_footer.log +6 -0
- data/spec/fixtures/merb.log +84 -0
- data/spec/fixtures/merb_prefixed.log +9 -0
- data/spec/fixtures/multiple_files_1.log +5 -0
- data/spec/fixtures/multiple_files_2.log +2 -0
- data/spec/fixtures/rails.db +0 -0
- data/spec/fixtures/rails_1x.log +59 -0
- data/spec/fixtures/rails_22.log +12 -0
- data/spec/fixtures/rails_22_cached.log +10 -0
- data/spec/fixtures/rails_unordered.log +24 -0
- data/spec/fixtures/syslog_1x.log +5 -0
- data/spec/fixtures/test_file_format.log +13 -0
- data/spec/fixtures/test_language_combined.log +14 -0
- data/spec/fixtures/test_order.log +16 -0
- data/spec/integration/command_line_usage_spec.rb +84 -0
- data/spec/integration/munin_plugins_rails_spec.rb +58 -0
- data/spec/integration/scout_spec.rb +151 -0
- data/spec/lib/helpers.rb +52 -0
- data/spec/lib/macros.rb +18 -0
- data/spec/lib/matchers.rb +77 -0
- data/spec/lib/mocks.rb +76 -0
- data/spec/lib/testing_format.rb +46 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
- data/spec/unit/aggregator/summarizer_spec.rb +26 -0
- data/spec/unit/controller/controller_spec.rb +41 -0
- data/spec/unit/controller/log_processor_spec.rb +18 -0
- data/spec/unit/database/base_class_spec.rb +183 -0
- data/spec/unit/database/connection_spec.rb +34 -0
- data/spec/unit/database/database_spec.rb +133 -0
- data/spec/unit/file_format/amazon_s3_format_spec.rb +49 -0
- data/spec/unit/file_format/apache_format_spec.rb +203 -0
- data/spec/unit/file_format/file_format_api_spec.rb +69 -0
- data/spec/unit/file_format/line_definition_spec.rb +75 -0
- data/spec/unit/file_format/merb_format_spec.rb +52 -0
- data/spec/unit/file_format/rails_format_spec.rb +164 -0
- data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
- data/spec/unit/filter/field_filter_spec.rb +66 -0
- data/spec/unit/filter/filter_spec.rb +17 -0
- data/spec/unit/filter/timespan_filter_spec.rb +58 -0
- data/spec/unit/mailer_spec.rb +30 -0
- data/spec/unit/request_spec.rb +111 -0
- data/spec/unit/source/log_parser_spec.rb +119 -0
- data/spec/unit/tracker/duration_tracker_spec.rb +130 -0
- data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
- data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
- data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
- data/spec/unit/tracker/tracker_api_spec.rb +124 -0
- data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
- data/tasks/github-gem.rake +323 -0
- data/tasks/request_log_analyzer.rake +26 -0
- metadata +220 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
module RequestLogAnalyzer::Tracker
|
2
|
+
|
3
|
+
# const_missing: this function is used to load subclasses in the RequestLogAnalyzer::Track namespace.
|
4
|
+
# It will automatically load the required file based on the class name
|
5
|
+
def self.const_missing(const)
|
6
|
+
RequestLogAnalyzer::load_default_class_file(self, const)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Base Tracker class. All other trackers inherit from this class
|
10
|
+
#
|
11
|
+
# Accepts the following options:
|
12
|
+
# * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
|
13
|
+
# * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
|
14
|
+
# * <tt>:output</tt> Direct output here (defaults to STDOUT)
|
15
|
+
# * <tt>:unless</tt> Proc that has to return nil for a request to be passed to the tracker.
|
16
|
+
#
|
17
|
+
# For example :if => lambda { |request| request[:duration] && request[:duration] > 1.0 }
|
18
|
+
class Base
|
19
|
+
|
20
|
+
attr_reader :options
|
21
|
+
|
22
|
+
# Initialize the class
|
23
|
+
# Note that the options are only applicable if should_update? is not overwritten
|
24
|
+
# by the inheriting class.
|
25
|
+
#
|
26
|
+
# === Options
|
27
|
+
# * <tt>:if</tt> Handle request if this proc is true for the handled request.
|
28
|
+
# * <tt>:unless</tt> Handle request if this proc is false for the handled request.
|
29
|
+
# * <tt>:line_type</tt> Line type this tracker will accept.
|
30
|
+
def initialize(options ={})
|
31
|
+
@options = options
|
32
|
+
setup_should_update_checks!
|
33
|
+
end
|
34
|
+
|
35
|
+
# Sets up the tracker's should_update? checks.
|
36
|
+
def setup_should_update_checks!
|
37
|
+
@should_update_checks = []
|
38
|
+
@should_update_checks.push( lambda { |request| request.has_line_type?(options[:line_type]) } ) if options[:line_type]
|
39
|
+
@should_update_checks.push(options[:if]) if options[:if].respond_to?(:call)
|
40
|
+
@should_update_checks.push( lambda { |request| request[options[:if]] }) if options[:if].kind_of?(Symbol)
|
41
|
+
@should_update_checks.push( lambda { |request| !options[:unless].call(request) }) if options[:unless].respond_to?(:call)
|
42
|
+
@should_update_checks.push( lambda { |request| !request[options[:unless]] }) if options[:unless].kind_of?(Symbol)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Creates a lambda expression to return a static field from a request. If the
|
46
|
+
# argument already is a lambda exprssion, it will simply return the argument.
|
47
|
+
def create_lambda(arg)
|
48
|
+
case arg
|
49
|
+
when Proc then arg
|
50
|
+
when Symbol then lambda { |request| request[arg] }
|
51
|
+
else raise "Canot create a lambda expression from this argument: #{arg.inspect}!"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Hook things that need to be done before running here.
|
56
|
+
def prepare
|
57
|
+
end
|
58
|
+
|
59
|
+
# Will be called with each request.
|
60
|
+
# <tt>request</tt> The request to track data in.
|
61
|
+
def update(request)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Hook things that need to be done after running here.
|
65
|
+
def finalize
|
66
|
+
end
|
67
|
+
|
68
|
+
# Determine if we should run the update function at all.
|
69
|
+
# Usually the update function will be heavy, so a light check is done here
|
70
|
+
# determining if we need to call update at all.
|
71
|
+
#
|
72
|
+
# Default this checks if defined:
|
73
|
+
# * :line_type is also in the request hash.
|
74
|
+
# * :if is true for this request.
|
75
|
+
# * :unless if false for this request
|
76
|
+
#
|
77
|
+
# <tt>request</tt> The request object.
|
78
|
+
def should_update?(request)
|
79
|
+
@should_update_checks.all? { |c| c.call(request) }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Hook report generation here.
|
83
|
+
# Defaults to self.inspect
|
84
|
+
# <tt>output</tt> The output object the report will be passed to.
|
85
|
+
def report(output)
|
86
|
+
output << self.inspect
|
87
|
+
output << "\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
# The title of this tracker. Used for reporting.
|
91
|
+
def title
|
92
|
+
self.class.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
# This method is called by RequestLogAnalyzer::Aggregator:Summarizer to retrieve an
|
96
|
+
# object with all the results of this tracker, that can be dumped to YAML format.
|
97
|
+
def to_yaml_object
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
module StatisticsTracking
|
103
|
+
|
104
|
+
# Update sthe running calculation of statistics with the newly found numeric value.
|
105
|
+
# <tt>category</tt>:: The category for which to update the running statistics calculations
|
106
|
+
# <tt>number</tt>:: The numeric value to update the calculations with.
|
107
|
+
def update_statistics(category, number)
|
108
|
+
@categories[category] ||= {:hits => 0, :sum => 0, :mean => 0.0, :sum_of_squares => 0.0, :min => number, :max => number }
|
109
|
+
delta = number - @categories[category][:mean]
|
110
|
+
|
111
|
+
@categories[category][:hits] += 1
|
112
|
+
@categories[category][:mean] += (delta / @categories[category][:hits])
|
113
|
+
@categories[category][:sum_of_squares] += delta * (number - @categories[category][:mean])
|
114
|
+
@categories[category][:sum] += number
|
115
|
+
@categories[category][:min] = number if number < @categories[category][:min]
|
116
|
+
@categories[category][:max] = number if number > @categories[category][:max]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Get the number of hits of a specific category.
|
120
|
+
# <tt>cat</tt> The category
|
121
|
+
def hits(cat)
|
122
|
+
@categories[cat][:hits]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get the total duration of a specific category.
|
126
|
+
# <tt>cat</tt> The category
|
127
|
+
def sum(cat)
|
128
|
+
@categories[cat][:sum]
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get the minimal duration of a specific category.
|
132
|
+
# <tt>cat</tt> The category
|
133
|
+
def min(cat)
|
134
|
+
@categories[cat][:min]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Get the maximum duration of a specific category.
|
138
|
+
# <tt>cat</tt> The category
|
139
|
+
def max(cat)
|
140
|
+
@categories[cat][:max]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Get the average duration of a specific category.
|
144
|
+
# <tt>cat</tt> The category
|
145
|
+
def mean(cat)
|
146
|
+
@categories[cat][:mean]
|
147
|
+
end
|
148
|
+
|
149
|
+
# Get the standard deviation of the duration of a specific category.
|
150
|
+
# <tt>cat</tt> The category
|
151
|
+
def stddev(cat)
|
152
|
+
Math.sqrt(variance(cat))
|
153
|
+
end
|
154
|
+
|
155
|
+
# Get the variance of the duration of a specific category.
|
156
|
+
# <tt>cat</tt> The category
|
157
|
+
def variance(cat)
|
158
|
+
return 0.0 if @categories[cat][:hits] <= 1
|
159
|
+
(@categories[cat][:sum_of_squares] / (@categories[cat][:hits] - 1))
|
160
|
+
end
|
161
|
+
|
162
|
+
# Get the average duration of a all categories.
|
163
|
+
def mean_overall
|
164
|
+
sum_overall / hits_overall
|
165
|
+
end
|
166
|
+
|
167
|
+
# Get the cumlative duration of a all categories.
|
168
|
+
def sum_overall
|
169
|
+
@categories.inject(0.0) { |sum, (name, cat)| sum + cat[:sum] }
|
170
|
+
end
|
171
|
+
|
172
|
+
# Get the total hits of a all categories.
|
173
|
+
def hits_overall
|
174
|
+
@categories.inject(0) { |sum, (name, cat)| sum + cat[:hits] }
|
175
|
+
end
|
176
|
+
|
177
|
+
# Return categories sorted by a given key.
|
178
|
+
# <tt>by</tt> The key to sort on. This parameter can be omitted if a sorting block is provided instead
|
179
|
+
def sorted_by(by = nil)
|
180
|
+
if block_given?
|
181
|
+
categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }
|
182
|
+
else
|
183
|
+
categories.sort { |a, b| send(by, b[0]) <=> send(by, a[0]) }
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns the column header for a statistics table to report on the statistics result
|
188
|
+
def statistics_header(options)
|
189
|
+
[
|
190
|
+
{:title => options[:title], :width => :rest},
|
191
|
+
{:title => 'Hits', :align => :right, :highlight => (options[:highlight] == :hits), :min_width => 4},
|
192
|
+
{:title => 'Sum', :align => :right, :highlight => (options[:highlight] == :sum), :min_width => 6},
|
193
|
+
{:title => 'Mean', :align => :right, :highlight => (options[:highlight] == :mean), :min_width => 6},
|
194
|
+
{:title => 'StdDev', :align => :right, :highlight => (options[:highlight] == :stddev), :min_width => 6},
|
195
|
+
{:title => 'Min', :align => :right, :highlight => (options[:highlight] == :min), :min_width => 6},
|
196
|
+
{:title => 'Max', :align => :right, :highlight => (options[:highlight] == :max), :min_width => 6}
|
197
|
+
]
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns a row of statistics information for a report table, given a category
|
201
|
+
def statistics_row(cat)
|
202
|
+
[cat, hits(cat), display_value(sum(cat)), display_value(mean(cat)), display_value(stddev(cat)),
|
203
|
+
display_value(min(cat)), display_value(max(cat))]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module RequestLogAnalyzer::Tracker
|
2
|
+
|
3
|
+
# Analyze the duration of a specific attribute
|
4
|
+
#
|
5
|
+
# === Options
|
6
|
+
# * <tt>:category</tt> Proc that handles request categorization for given fileformat (REQUEST_CATEGORIZER)
|
7
|
+
# * <tt>:duration</tt> The field containing the duration in the request hash.
|
8
|
+
# * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
|
9
|
+
# * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
|
10
|
+
# * <tt>:title</tt> Title do be displayed above the report
|
11
|
+
# * <tt>:unless</tt> Handle request if this proc is false for the handled request.
|
12
|
+
#
|
13
|
+
# The items in the update request hash are set during the creation of the Duration tracker.
|
14
|
+
#
|
15
|
+
# Example output:
|
16
|
+
# Request duration - top 20 by cumulative time | Hits | Sum. | Avg.
|
17
|
+
# ---------------------------------------------------------------------------------
|
18
|
+
# EmployeeController#show.html [GET] | 4742 | 4922.56s | 1.04s
|
19
|
+
# EmployeeController#update.html [POST] | 4647 | 2731.23s | 0.59s
|
20
|
+
# EmployeeController#index.html [GET] | 5802 | 1477.32s | 0.25s
|
21
|
+
# .............
|
22
|
+
class Duration < Base
|
23
|
+
|
24
|
+
include RequestLogAnalyzer::Tracker::StatisticsTracking
|
25
|
+
|
26
|
+
attr_reader :categories
|
27
|
+
|
28
|
+
# Check if duration and catagory option have been received,
|
29
|
+
def prepare
|
30
|
+
raise "No duration field set up for category tracker #{self.inspect}" unless options[:duration]
|
31
|
+
raise "No categorizer set up for duration tracker #{self.inspect}" unless options[:category]
|
32
|
+
|
33
|
+
unless options[:multiple]
|
34
|
+
@categorizer = create_lambda(options[:category])
|
35
|
+
@durationizer = create_lambda(options[:duration])
|
36
|
+
end
|
37
|
+
|
38
|
+
@categories = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get the duration information fron the request and store it in the different categories.
|
42
|
+
# <tt>request</tt> The request.
|
43
|
+
def update(request)
|
44
|
+
if options[:multiple]
|
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]) }
|
49
|
+
else
|
50
|
+
category = @categorizer.call(request)
|
51
|
+
duration = @durationizer.call(request)
|
52
|
+
update_statistics(category, duration) if duration.kind_of?(Numeric) && category
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Block function to build a result table using a provided sorting function.
|
57
|
+
# <tt>output</tt> The output object.
|
58
|
+
# <tt>amount</tt> The number of rows in the report table (default 10).
|
59
|
+
# === Options
|
60
|
+
# * </tt>:title</tt> The title of the table
|
61
|
+
# * </tt>:sort</tt> The key to sort on (:hits, :cumulative, :average, :min or :max)
|
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]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Generate a request duration report to the given output object
|
83
|
+
# By default colulative and average duration are generated.
|
84
|
+
# Any options for the report should have been set during initialize.
|
85
|
+
# <tt>output</tt> The output object
|
86
|
+
def report(output)
|
87
|
+
sortings = output.options[:sort] || [:sum, :mean]
|
88
|
+
sortings.each do |sorting|
|
89
|
+
report_table(output, sorting, :title => "#{title} - by #{sorting}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the title of this tracker for reports
|
94
|
+
def title
|
95
|
+
options[:title] || 'Request duration'
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns all the categories and the tracked duration as a hash than can be exported to YAML
|
99
|
+
def to_yaml_object
|
100
|
+
return nil if @categories.empty?
|
101
|
+
@categories
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module RequestLogAnalyzer::Tracker
|
2
|
+
|
3
|
+
# Catagorize requests by frequency.
|
4
|
+
# Count and analyze requests for a specific attribute
|
5
|
+
#
|
6
|
+
# === Options
|
7
|
+
# * <tt>:category</tt> Proc that handles the request categorization.
|
8
|
+
# * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
|
9
|
+
# * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
|
10
|
+
# * <tt>:nils</tt> Track undetermined methods.
|
11
|
+
# * <tt>:title</tt> Title do be displayed above the report.
|
12
|
+
# * <tt>:unless</tt> Proc that has to return nil for a request to be passed to the tracker.
|
13
|
+
#
|
14
|
+
# The items in the update request hash are set during the creation of the Duration tracker.
|
15
|
+
#
|
16
|
+
# Example output:
|
17
|
+
# HTTP methods
|
18
|
+
# ----------------------------------------------------------------------
|
19
|
+
# GET | 22248 hits (46.2%) |=================
|
20
|
+
# PUT | 13685 hits (28.4%) |===========
|
21
|
+
# POST | 11662 hits (24.2%) |=========
|
22
|
+
# DELETE | 512 hits (1.1%) |
|
23
|
+
class Frequency < Base
|
24
|
+
|
25
|
+
attr_reader :categories
|
26
|
+
|
27
|
+
# Check if categories are set up
|
28
|
+
def prepare
|
29
|
+
raise "No categorizer set up for category tracker #{self.inspect}" unless options[:category]
|
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
|
+
end
|
36
|
+
|
37
|
+
# Check HTTP method of a request and store that in the categories hash.
|
38
|
+
# <tt>request</tt> The request.
|
39
|
+
def update(request)
|
40
|
+
cat = @categorizer.call(request)
|
41
|
+
if cat || options[:nils]
|
42
|
+
@categories[cat] ||= 0
|
43
|
+
@categories[cat] += 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return the amount of times a HTTP method has been encountered
|
48
|
+
# <tt>cat</tt> The HTTP method (:get, :put, :post or :delete)
|
49
|
+
def frequency(cat)
|
50
|
+
categories[cat] || 0
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the overall frequency
|
54
|
+
def overall_frequency
|
55
|
+
categories.inject(0) { |carry, item| carry + item[1] }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return the methods sorted by frequency
|
59
|
+
def sorted_by_frequency
|
60
|
+
@categories.sort { |a, b| b[1] <=> a[1] }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Generate a HTTP method frequency report to the given output object.
|
64
|
+
# Any options for the report should have been set during initialize.
|
65
|
+
# <tt>output</tt> The output object
|
66
|
+
def report(output)
|
67
|
+
output.title(options[:title]) if options[:title]
|
68
|
+
|
69
|
+
if @categories.empty?
|
70
|
+
output << "None found.\n"
|
71
|
+
else
|
72
|
+
sorted_categories = output.slice_results(sorted_by_frequency)
|
73
|
+
total_hits = overall_frequency
|
74
|
+
|
75
|
+
output.table({:align => :left}, {:align => :right }, {:align => :right}, {:type => :ratio, :width => :rest}) do |rows|
|
76
|
+
sorted_categories.each do |(cat, count)|
|
77
|
+
rows << [cat, "#{count} hits", '%0.1f%%' % ((count.to_f / total_hits.to_f) * 100.0), (count.to_f / total_hits.to_f)]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns a hash with the categories of every category that can be exported to YAML
|
85
|
+
def to_yaml_object
|
86
|
+
return nil if @categories.empty?
|
87
|
+
@categories
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns the title of this tracker for reports
|
91
|
+
def title
|
92
|
+
options[:title] || 'Request frequency'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module RequestLogAnalyzer::Tracker
|
2
|
+
|
3
|
+
# Determines the average hourly spread of the parsed requests.
|
4
|
+
# This spread is shown in a graph form.
|
5
|
+
#
|
6
|
+
# Accepts the following options:
|
7
|
+
# * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
|
8
|
+
# * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
|
9
|
+
# * <tt>:output</tt> Direct output here (defaults to STDOUT)
|
10
|
+
# * <tt>:unless</tt> Proc that has to return nil for a request to be passed to the tracker.
|
11
|
+
#
|
12
|
+
# Expects the following items in the update request hash
|
13
|
+
# * <tt>:timestamp</tt> in YYYYMMDDHHMMSS format.
|
14
|
+
#
|
15
|
+
# Example output:
|
16
|
+
# Requests graph - average per day per hour
|
17
|
+
# --------------------------------------------------
|
18
|
+
# 7:00 - 330 hits : =======
|
19
|
+
# 8:00 - 704 hits : =================
|
20
|
+
# 9:00 - 830 hits : ====================
|
21
|
+
# 10:00 - 822 hits : ===================
|
22
|
+
# 11:00 - 823 hits : ===================
|
23
|
+
# 12:00 - 729 hits : =================
|
24
|
+
# 13:00 - 614 hits : ==============
|
25
|
+
# 14:00 - 690 hits : ================
|
26
|
+
# 15:00 - 492 hits : ===========
|
27
|
+
# 16:00 - 355 hits : ========
|
28
|
+
# 17:00 - 213 hits : =====
|
29
|
+
# 18:00 - 107 hits : ==
|
30
|
+
# ................
|
31
|
+
class HourlySpread < Base
|
32
|
+
|
33
|
+
attr_reader :hour_frequencies, :first, :last
|
34
|
+
|
35
|
+
# Check if timestamp field is set in the options and prepare the result time graph.
|
36
|
+
def prepare
|
37
|
+
options[:field] ||= :timestamp
|
38
|
+
@hour_frequencies = (0...24).map { 0 }
|
39
|
+
@first, @last = 99999999999999, 0
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if the timestamp in the request and store it.
|
43
|
+
# <tt>request</tt> The request.
|
44
|
+
def update(request)
|
45
|
+
timestamp = request.first(options[:field])
|
46
|
+
@hour_frequencies[timestamp.to_s[8..9].to_i] +=1
|
47
|
+
@first = timestamp if timestamp < @first
|
48
|
+
@last = timestamp if timestamp > @last
|
49
|
+
end
|
50
|
+
|
51
|
+
# Total amount of requests tracked
|
52
|
+
def total_requests
|
53
|
+
@hour_frequencies.inject(0) { |sum, value| sum + value }
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# First timestamp encountered
|
58
|
+
def first_timestamp
|
59
|
+
DateTime.parse(@first.to_s, '%Y%m%d%H%M%S') rescue nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# Last timestamp encountered
|
63
|
+
def last_timestamp
|
64
|
+
DateTime.parse(@last.to_s, '%Y%m%d%H%M%S') rescue nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# Difference between last and first timestamp.
|
68
|
+
def timespan
|
69
|
+
last_timestamp - first_timestamp
|
70
|
+
end
|
71
|
+
|
72
|
+
# Generate an hourly spread report to the given output object.
|
73
|
+
# Any options for the report should have been set during initialize.
|
74
|
+
# <tt>output</tt> The output object
|
75
|
+
def report(output)
|
76
|
+
output.title(title)
|
77
|
+
|
78
|
+
if total_requests == 0
|
79
|
+
output << "None found.\n"
|
80
|
+
return
|
81
|
+
end
|
82
|
+
|
83
|
+
days = [1, timespan].max
|
84
|
+
output.table({}, {:align => :right}, {:type => :ratio, :width => :rest, :treshold => 0.15}) do |rows|
|
85
|
+
@hour_frequencies.each_with_index do |requests, index|
|
86
|
+
ratio = requests.to_f / total_requests.to_f
|
87
|
+
requests_per_day = (requests / days).ceil
|
88
|
+
rows << ["#{index.to_s.rjust(3)}:00", "%d hits/day" % requests_per_day, ratio]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the title of this tracker for reports
|
94
|
+
def title
|
95
|
+
options[:title] || "Request distribution per hour"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns the found frequencies per hour as a hash for YAML exporting
|
99
|
+
def to_yaml_object
|
100
|
+
yaml_object = {}
|
101
|
+
@hour_frequencies.each_with_index do |freq, hour|
|
102
|
+
yaml_object["#{hour}:00 - #{hour+1}:00"] = freq
|
103
|
+
end
|
104
|
+
yaml_object
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|