request-log-analyzer 1.0.2 → 1.6.3
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/.gitignore +10 -0
- data/DESIGN.rdoc +41 -0
- data/LICENSE +4 -4
- data/README.rdoc +38 -0
- data/Rakefile +6 -3
- data/bin/request-log-analyzer +70 -72
- data/lib/cli/command_line_arguments.rb +53 -53
- data/lib/cli/database_console.rb +26 -0
- data/lib/cli/database_console_init.rb +43 -0
- data/lib/cli/progressbar.rb +166 -189
- data/lib/cli/tools.rb +49 -0
- data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
- data/lib/request_log_analyzer/aggregator/echo.rb +17 -12
- data/lib/request_log_analyzer/aggregator/summarizer.rb +101 -63
- data/lib/request_log_analyzer/{aggregator/base.rb → aggregator.rb} +17 -13
- data/lib/request_log_analyzer/controller.rb +251 -98
- data/lib/request_log_analyzer/database/base.rb +114 -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/database.rb +102 -0
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +74 -0
- data/lib/request_log_analyzer/file_format/apache.rb +147 -0
- data/lib/request_log_analyzer/file_format/delayed_job.rb +55 -0
- data/lib/request_log_analyzer/file_format/merb.rb +65 -29
- data/lib/request_log_analyzer/file_format/mysql.rb +101 -0
- data/lib/request_log_analyzer/file_format/postgresql.rb +68 -0
- data/lib/request_log_analyzer/file_format/rack.rb +9 -0
- data/lib/request_log_analyzer/file_format/rails.rb +164 -78
- data/lib/request_log_analyzer/file_format/rails3.rb +86 -0
- data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
- data/lib/request_log_analyzer/file_format.rb +252 -58
- data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
- data/lib/request_log_analyzer/filter/field.rb +19 -13
- data/lib/request_log_analyzer/filter/timespan.rb +25 -12
- data/lib/request_log_analyzer/filter.rb +30 -0
- data/lib/request_log_analyzer/line_definition.rb +69 -96
- data/lib/request_log_analyzer/log_processor.rb +31 -53
- data/lib/request_log_analyzer/mailer.rb +65 -0
- data/lib/request_log_analyzer/output/fancy_html.rb +49 -0
- data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
- data/lib/request_log_analyzer/output/html.rb +187 -0
- data/lib/request_log_analyzer/output.rb +117 -0
- data/lib/request_log_analyzer/request.rb +125 -40
- data/lib/request_log_analyzer/source/database_loader.rb +87 -0
- data/lib/request_log_analyzer/source/log_parser.rb +297 -0
- data/lib/request_log_analyzer/source.rb +72 -0
- data/lib/request_log_analyzer/tracker/duration.rb +28 -64
- data/lib/request_log_analyzer/tracker/frequency.rb +108 -0
- data/lib/request_log_analyzer/tracker/hourly_spread.rb +76 -49
- data/lib/request_log_analyzer/tracker/numeric_value.rb +223 -0
- data/lib/request_log_analyzer/tracker/timespan.rb +54 -27
- data/lib/request_log_analyzer/tracker/traffic.rb +40 -0
- data/lib/request_log_analyzer/tracker.rb +101 -0
- data/lib/request_log_analyzer.rb +43 -13
- data/request-log-analyzer.gemspec +41 -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_prefixed.log +9 -0
- data/spec/fixtures/mysql_slow_query.log +110 -0
- data/spec/fixtures/postgresql.log +2980 -0
- data/spec/fixtures/rails.db +0 -0
- data/spec/fixtures/rails_22.log +1 -1
- data/spec/fixtures/sinatra.log +99 -0
- data/spec/integration/command_line_usage_spec.rb +84 -0
- data/spec/integration/mailer_spec.rb +179 -0
- data/spec/integration/munin_plugins_rails_spec.rb +58 -0
- data/spec/integration/scout_spec.rb +152 -0
- data/spec/lib/helpers.rb +72 -0
- data/spec/lib/macros.rb +18 -0
- data/spec/lib/matchers.rb +77 -0
- data/spec/lib/mocks.rb +77 -0
- data/spec/lib/testing_format.rb +46 -0
- data/spec/spec_helper.rb +16 -59
- 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 +67 -0
- data/spec/unit/file_format/apache_format_spec.rb +203 -0
- data/spec/unit/file_format/common_regular_expressions_spec.rb +53 -0
- data/spec/unit/file_format/delayed_job_format_spec.rb +83 -0
- data/spec/unit/file_format/file_format_api_spec.rb +69 -0
- data/spec/unit/file_format/format_autodetection_spec.rb +40 -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/mysql_format_spec.rb +154 -0
- data/spec/unit/file_format/postgresql_format_spec.rb +65 -0
- data/spec/unit/file_format/rack_format_spec.rb +50 -0
- data/spec/unit/file_format/rails3_format_spec.rb +120 -0
- data/spec/unit/file_format/rails_format_spec.rb +180 -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 +42 -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 +49 -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/numeric_value_tracker_spec.rb +166 -0
- data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
- data/spec/unit/tracker/tracker_api_spec.rb +125 -0
- data/spec/unit/tracker/traffic_tracker_spec.rb +28 -0
- data/tasks/github-gem.rake +290 -139
- data/tasks/request_log_analyzer.rake +23 -7
- metadata +221 -94
- data/DESIGN +0 -14
- data/HACKING +0 -7
- data/README.textile +0 -36
- data/lib/cli/bashcolorizer.rb +0 -60
- data/lib/request_log_analyzer/aggregator/database.rb +0 -148
- data/lib/request_log_analyzer/filter/base.rb +0 -29
- data/lib/request_log_analyzer/log_parser.rb +0 -173
- data/lib/request_log_analyzer/source/base.rb +0 -42
- data/lib/request_log_analyzer/source/log_file.rb +0 -170
- data/lib/request_log_analyzer/tracker/base.rb +0 -54
- data/lib/request_log_analyzer/tracker/category.rb +0 -71
- data/spec/controller_spec.rb +0 -40
- data/spec/database_inserter_spec.rb +0 -101
- data/spec/file_format_spec.rb +0 -78
- data/spec/file_formats/spec_format.rb +0 -26
- data/spec/filter_spec.rb +0 -137
- data/spec/line_definition_spec.rb +0 -124
- data/spec/log_parser_spec.rb +0 -68
- data/spec/log_processor_spec.rb +0 -57
- data/spec/merb_format_spec.rb +0 -38
- data/spec/rails_format_spec.rb +0 -76
- data/spec/request_spec.rb +0 -72
- data/spec/summarizer_spec.rb +0 -9
- data/tasks/rspec.rake +0 -6
|
@@ -2,80 +2,44 @@ module RequestLogAnalyzer::Tracker
|
|
|
2
2
|
|
|
3
3
|
# Analyze the duration of a specific attribute
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
# * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
|
|
7
|
-
# * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
|
|
8
|
-
# * <tt>:title</tt> Title do be displayed above the report
|
|
5
|
+
# === Options
|
|
9
6
|
# * <tt>:category</tt> Proc that handles request categorization for given fileformat (REQUEST_CATEGORIZER)
|
|
10
7
|
# * <tt>:duration</tt> The field containing the duration in the request hash.
|
|
11
|
-
# * <tt>:
|
|
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
12
|
#
|
|
13
13
|
# The items in the update request hash are set during the creation of the Duration tracker.
|
|
14
14
|
#
|
|
15
15
|
# Example output:
|
|
16
|
-
# Request duration - top 20 by cumulative time
|
|
17
|
-
#
|
|
18
|
-
# EmployeeController#show.html [GET]
|
|
19
|
-
# EmployeeController#update.html [POST]
|
|
20
|
-
# EmployeeController#index.html [GET]
|
|
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
21
|
# .............
|
|
22
|
-
class Duration <
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
class Duration < NumericValue
|
|
23
|
+
|
|
24
|
+
# Check if duration and catagory option have been received,
|
|
25
25
|
def prepare
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@categories = {}
|
|
26
|
+
options[:value] = options[:duration] if options[:duration]
|
|
27
|
+
super
|
|
30
28
|
end
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@categories[category][:total_duration] += duration
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def report_table(output = STDOUT, amount = 10, options = {}, &block)
|
|
44
|
-
|
|
45
|
-
top_categories = @categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }.slice(0...amount)
|
|
46
|
-
max_cat_length = top_categories.map { |a| a[0].length }.max || 0
|
|
47
|
-
space_left = [options[:report_width] - 33, [max_cat_length + 1, options[:title].length].max].min
|
|
48
|
-
|
|
49
|
-
output << "\n"
|
|
50
|
-
output << "%-#{space_left+1}s┃ Hits ┃ Sum. | Avg." % [options[:title][0...space_left]] + "\n"
|
|
51
|
-
output << green('━' * options[:report_width], options[:color]) + "\n"
|
|
52
|
-
|
|
53
|
-
top_categories.each do |(cat, info)|
|
|
54
|
-
hits = info[:count]
|
|
55
|
-
total = "%0.02f" % info[:total_duration]
|
|
56
|
-
avg = "%0.02f" % (info[:total_duration] / info[:count])
|
|
57
|
-
output << "%-#{space_left+1}s┃%8d ┃%9ss ┃%9ss" % [cat[0...space_left], hits, total, avg] + "\n"
|
|
29
|
+
|
|
30
|
+
# Display a duration
|
|
31
|
+
def display_value(time)
|
|
32
|
+
case time
|
|
33
|
+
when nil then '-'
|
|
34
|
+
when 0...60 then "%0.02fs" % time
|
|
35
|
+
when 60...3600 then "%dm%02ds" % [time / 60, (time % 60).round]
|
|
36
|
+
else "%dh%02dm%02ds" % [time / 3600, (time % 3600) / 60, (time % 60).round]
|
|
58
37
|
end
|
|
59
38
|
end
|
|
60
|
-
|
|
61
|
-
def report(output = STDOUT, report_width = 80, color = false)
|
|
62
39
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
options[:
|
|
66
|
-
|
|
67
|
-
options[:report].each do |report|
|
|
68
|
-
case report
|
|
69
|
-
when :average
|
|
70
|
-
report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by average time", :color => color, :report_width => report_width) { |request| request[:total_duration] / request[:count] }
|
|
71
|
-
when :total
|
|
72
|
-
report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by cumulative time", :color => color, :report_width => report_width) { |request| request[:total_duration] }
|
|
73
|
-
when :hits
|
|
74
|
-
report_table(output, options[:top], :title => "#{options[:title]} - top #{options[:top]} by hits", :color => color, :report_width => report_width) { |request| request[:count] }
|
|
75
|
-
else
|
|
76
|
-
output << "Unknown duration report specified\n"
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
40
|
+
# Returns the title of this tracker for reports
|
|
41
|
+
def title
|
|
42
|
+
options[:title] || 'Request duration'
|
|
43
|
+
end
|
|
80
44
|
end
|
|
81
|
-
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
options[:category] = options[:value] if options[:value] && !options[:category]
|
|
30
|
+
raise "No categorizer set up for category tracker #{self.inspect}" unless options[:category]
|
|
31
|
+
|
|
32
|
+
@categorizer = create_lambda(options[:category]) unless options[:multiple]
|
|
33
|
+
|
|
34
|
+
# Initialize the categories. Use the list of category names to
|
|
35
|
+
@categories = {}
|
|
36
|
+
options[:all_categories].each { |cat| @categories[cat] = 0 } if options[:all_categories].kind_of?(Enumerable)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check HTTP method of a request and store that in the categories hash.
|
|
40
|
+
# <tt>request</tt> The request.
|
|
41
|
+
def update(request)
|
|
42
|
+
if options[:multiple]
|
|
43
|
+
cats = request.every(options[:category])
|
|
44
|
+
cats.each do |cat|
|
|
45
|
+
if cat || options[:nils]
|
|
46
|
+
@categories[cat] ||= 0
|
|
47
|
+
@categories[cat] += 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
else
|
|
52
|
+
cat = @categorizer.call(request)
|
|
53
|
+
if cat || options[:nils]
|
|
54
|
+
@categories[cat] ||= 0
|
|
55
|
+
@categories[cat] += 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Return the amount of times a HTTP method has been encountered
|
|
61
|
+
# <tt>cat</tt> The HTTP method (:get, :put, :post or :delete)
|
|
62
|
+
def frequency(cat)
|
|
63
|
+
categories[cat] || 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Return the overall frequency
|
|
67
|
+
def overall_frequency
|
|
68
|
+
categories.inject(0) { |carry, item| carry + item[1] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Return the methods sorted by frequency
|
|
72
|
+
def sorted_by_frequency
|
|
73
|
+
@categories.sort { |a, b| b[1] <=> a[1] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Generate a HTTP method frequency report to the given output object.
|
|
77
|
+
# Any options for the report should have been set during initialize.
|
|
78
|
+
# <tt>output</tt> The output object
|
|
79
|
+
def report(output)
|
|
80
|
+
output.title(options[:title]) if options[:title]
|
|
81
|
+
|
|
82
|
+
if @categories.empty?
|
|
83
|
+
output << "None found.\n"
|
|
84
|
+
else
|
|
85
|
+
sorted_categories = output.slice_results(sorted_by_frequency)
|
|
86
|
+
total_hits = overall_frequency
|
|
87
|
+
|
|
88
|
+
output.table({:align => :left}, {:align => :right }, {:align => :right}, {:type => :ratio, :width => :rest}) do |rows|
|
|
89
|
+
sorted_categories.each do |(cat, count)|
|
|
90
|
+
rows << [cat, "#{count} hits", '%0.1f%%' % ((count.to_f / total_hits.to_f) * 100.0), (count.to_f / total_hits.to_f)]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns a hash with the categories of every category that can be exported to YAML
|
|
98
|
+
def to_yaml_object
|
|
99
|
+
return nil if @categories.empty?
|
|
100
|
+
@categories
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns the title of this tracker for reports
|
|
104
|
+
def title
|
|
105
|
+
options[:title] || 'Request frequency'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -4,77 +4,104 @@ module RequestLogAnalyzer::Tracker
|
|
|
4
4
|
# This spread is shown in a graph form.
|
|
5
5
|
#
|
|
6
6
|
# Accepts the following options:
|
|
7
|
-
# * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
|
|
8
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.
|
|
9
11
|
#
|
|
10
12
|
# Expects the following items in the update request hash
|
|
11
13
|
# * <tt>:timestamp</tt> in YYYYMMDDHHMMSS format.
|
|
12
14
|
#
|
|
13
15
|
# Example output:
|
|
14
16
|
# Requests graph - average per day per hour
|
|
15
|
-
#
|
|
16
|
-
# 7:00 - 330 hits :
|
|
17
|
-
# 8:00 - 704 hits :
|
|
18
|
-
# 9:00 - 830 hits :
|
|
19
|
-
# 10:00 - 822 hits :
|
|
20
|
-
# 11:00 - 823 hits :
|
|
21
|
-
# 12:00 - 729 hits :
|
|
22
|
-
# 13:00 - 614 hits :
|
|
23
|
-
# 14:00 - 690 hits :
|
|
24
|
-
# 15:00 - 492 hits :
|
|
25
|
-
# 16:00 - 355 hits :
|
|
26
|
-
# 17:00 - 213 hits :
|
|
27
|
-
# 18:00 - 107 hits :
|
|
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 : ==
|
|
28
30
|
# ................
|
|
29
|
-
class HourlySpread <
|
|
31
|
+
class HourlySpread < Base
|
|
32
|
+
|
|
33
|
+
attr_reader :hour_frequencies, :first, :last
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
# Check if timestamp field is set in the options and prepare the result time graph.
|
|
33
36
|
def prepare
|
|
34
37
|
options[:field] ||= :timestamp
|
|
35
|
-
@
|
|
38
|
+
@hour_frequencies = (0...24).map { 0 }
|
|
39
|
+
@first, @last = 99999999999999, 0
|
|
36
40
|
end
|
|
37
|
-
|
|
41
|
+
|
|
42
|
+
# Check if the timestamp in the request and store it.
|
|
43
|
+
# <tt>request</tt> The request.
|
|
38
44
|
def update(request)
|
|
39
|
-
|
|
40
|
-
timestamp
|
|
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
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@
|
|
51
|
+
# Total amount of requests tracked
|
|
52
|
+
def total_requests
|
|
53
|
+
@hour_frequencies.inject(0) { |sum, value| sum + value }
|
|
45
54
|
end
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
53
79
|
output << "None found.\n"
|
|
54
80
|
return
|
|
55
81
|
end
|
|
56
82
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@request_time_graph.each_with_index do |requests, index|
|
|
65
|
-
display_chars = requests / deviation
|
|
66
|
-
request_today = requests / days
|
|
67
|
-
|
|
68
|
-
if display_chars >= color_cutoff
|
|
69
|
-
display_chars_string = green(('░' * color_cutoff), color) + red(('░' * (display_chars - color_cutoff)), color)
|
|
70
|
-
else
|
|
71
|
-
display_chars_string = green(('░' * display_chars), color)
|
|
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]
|
|
72
89
|
end
|
|
73
|
-
|
|
74
|
-
output << "#{index.to_s.rjust(3)}:00 - #{(request_today.to_s + ' hits').ljust(15)} : #{display_chars_string}\n"
|
|
75
90
|
end
|
|
76
|
-
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the title of this tracker for reports
|
|
94
|
+
def title
|
|
95
|
+
options[:title] || "Request distribution per hour"
|
|
96
|
+
end
|
|
77
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
|
|
78
105
|
end
|
|
79
106
|
end
|
|
80
107
|
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
module RequestLogAnalyzer::Tracker
|
|
2
|
+
|
|
3
|
+
class NumericValue < Base
|
|
4
|
+
|
|
5
|
+
attr_reader :categories
|
|
6
|
+
|
|
7
|
+
# Sets up the numeric value tracker. It will check whether the value and category
|
|
8
|
+
# options are set that are used to extract and categorize the values during
|
|
9
|
+
# parsing. Two lambda procedures are created for these tasks
|
|
10
|
+
def prepare
|
|
11
|
+
|
|
12
|
+
raise "No value field set up for numeric tracker #{self.inspect}" unless options[:value]
|
|
13
|
+
raise "No categorizer set up for numeric tracker #{self.inspect}" unless options[:category]
|
|
14
|
+
|
|
15
|
+
unless options[:multiple]
|
|
16
|
+
@categorizer = create_lambda(options[:category])
|
|
17
|
+
@valueizer = create_lambda(options[:value])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@categories = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get the value information from the request and store it in the respective categories.
|
|
24
|
+
#
|
|
25
|
+
# If a request can contain multiple usable values for this tracker, the :multiple option
|
|
26
|
+
# should be set to true. In this case, all the values and respective categories will be
|
|
27
|
+
# read from the request using the #every method from the fields given in the :value and
|
|
28
|
+
# :category option.
|
|
29
|
+
#
|
|
30
|
+
# If the request contains only one suitable value and the :multiple is not set, it will
|
|
31
|
+
# read the single value and category from the fields provided in the :value and :category
|
|
32
|
+
# option, or calculate it with any lambda procedure that is assigned to these options. The
|
|
33
|
+
# request will be passed to procedure as input for the calculation.
|
|
34
|
+
#
|
|
35
|
+
# @param [RequestLogAnalyzer::Request] request The request to get the information from.
|
|
36
|
+
def update(request)
|
|
37
|
+
if options[:multiple]
|
|
38
|
+
found_categories = request.every(options[:category])
|
|
39
|
+
found_values = request.every(options[:value])
|
|
40
|
+
raise "Capture mismatch for multiple values in a request" unless found_categories.length == found_values.length
|
|
41
|
+
|
|
42
|
+
found_categories.each_with_index do |cat, index|
|
|
43
|
+
update_statistics(cat, found_values[index]) if cat && found_values[index].kind_of?(Numeric)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
else
|
|
47
|
+
category = @categorizer.call(request)
|
|
48
|
+
value = @valueizer.call(request)
|
|
49
|
+
update_statistics(category, value) if value.kind_of?(Numeric) && category
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Block function to build a result table using a provided sorting function.
|
|
54
|
+
# <tt>output</tt> The output object.
|
|
55
|
+
# <tt>amount</tt> The number of rows in the report table (default 10).
|
|
56
|
+
# === Options
|
|
57
|
+
# * </tt>:title</tt> The title of the table
|
|
58
|
+
# * </tt>:sort</tt> The key to sort on (:hits, :cumulative, :average, :min or :max)
|
|
59
|
+
def report_table(output, sort, options = {}, &block)
|
|
60
|
+
output.puts
|
|
61
|
+
top_categories = output.slice_results(sorted_by(sort))
|
|
62
|
+
output.with_style(:top_line => true) do
|
|
63
|
+
output.table(*statistics_header(:title => options[:title], :highlight => sort)) do |rows|
|
|
64
|
+
top_categories.each { |(cat, info)| rows << statistics_row(cat) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Display a value
|
|
70
|
+
def display_value(value)
|
|
71
|
+
return "- " if value.nil?
|
|
72
|
+
return "0 " if value.zero?
|
|
73
|
+
|
|
74
|
+
case [Math.log10(value.abs).floor, 0].max
|
|
75
|
+
when 0...4 then '%d ' % value
|
|
76
|
+
when 4...7 then '%dk' % (value / 1000)
|
|
77
|
+
when 7...10 then '%dM' % (value / 1000_000)
|
|
78
|
+
when 10...13 then '%dG' % (value / 1000_000_000)
|
|
79
|
+
when 13...16 then '%dT' % (value / 1000_000_000_000)
|
|
80
|
+
else '%dP' % (value / 1000_000_000_000_000)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Generate a request report to the given output object
|
|
85
|
+
# By default colulative and average duration are generated.
|
|
86
|
+
# Any options for the report should have been set during initialize.
|
|
87
|
+
# <tt>output</tt> The output object
|
|
88
|
+
def report(output)
|
|
89
|
+
sortings = output.options[:sort] || [:sum, :mean]
|
|
90
|
+
sortings.each do |sorting|
|
|
91
|
+
report_table(output, sorting, :title => "#{title} - by #{sorting}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if options[:total]
|
|
95
|
+
output.puts
|
|
96
|
+
output.puts "#{output.colorize(title, :white, :bold)} - total: " + output.colorize(display_value(sum_overall), :brown, :bold)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the title of this tracker for reports
|
|
101
|
+
def title
|
|
102
|
+
@title ||= begin
|
|
103
|
+
if options[:title]
|
|
104
|
+
options[:title]
|
|
105
|
+
else
|
|
106
|
+
title_builder = ""
|
|
107
|
+
title_builder << "#{options[:value]} " if options[:value].kind_of?(Symbol)
|
|
108
|
+
title_builder << (options[:category].kind_of?(Symbol) ? "per #{options[:category]}" : "per request")
|
|
109
|
+
title_builder
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns all the categories and the tracked duration as a hash than can be exported to YAML
|
|
115
|
+
def to_yaml_object
|
|
116
|
+
return nil if @categories.empty?
|
|
117
|
+
@categories
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Update sthe running calculation of statistics with the newly found numeric value.
|
|
122
|
+
# <tt>category</tt>:: The category for which to update the running statistics calculations
|
|
123
|
+
# <tt>number</tt>:: The numeric value to update the calculations with.
|
|
124
|
+
def update_statistics(category, number)
|
|
125
|
+
@categories[category] ||= {:hits => 0, :sum => 0, :mean => 0.0, :sum_of_squares => 0.0, :min => number, :max => number }
|
|
126
|
+
delta = number - @categories[category][:mean]
|
|
127
|
+
|
|
128
|
+
@categories[category][:hits] += 1
|
|
129
|
+
@categories[category][:mean] += (delta / @categories[category][:hits])
|
|
130
|
+
@categories[category][:sum_of_squares] += delta * (number - @categories[category][:mean])
|
|
131
|
+
@categories[category][:sum] += number
|
|
132
|
+
@categories[category][:min] = number if number < @categories[category][:min]
|
|
133
|
+
@categories[category][:max] = number if number > @categories[category][:max]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get the number of hits of a specific category.
|
|
137
|
+
# <tt>cat</tt> The category
|
|
138
|
+
def hits(cat)
|
|
139
|
+
@categories[cat][:hits]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get the total duration of a specific category.
|
|
143
|
+
# <tt>cat</tt> The category
|
|
144
|
+
def sum(cat)
|
|
145
|
+
@categories[cat][:sum]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Get the minimal duration of a specific category.
|
|
149
|
+
# <tt>cat</tt> The category
|
|
150
|
+
def min(cat)
|
|
151
|
+
@categories[cat][:min]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get the maximum duration of a specific category.
|
|
155
|
+
# <tt>cat</tt> The category
|
|
156
|
+
def max(cat)
|
|
157
|
+
@categories[cat][:max]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get the average duration of a specific category.
|
|
161
|
+
# <tt>cat</tt> The category
|
|
162
|
+
def mean(cat)
|
|
163
|
+
@categories[cat][:mean]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Get the standard deviation of the duration of a specific category.
|
|
167
|
+
# <tt>cat</tt> The category
|
|
168
|
+
def stddev(cat)
|
|
169
|
+
Math.sqrt(variance(cat))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get the variance of the duration of a specific category.
|
|
173
|
+
# <tt>cat</tt> The category
|
|
174
|
+
def variance(cat)
|
|
175
|
+
return 0.0 if @categories[cat][:hits] <= 1
|
|
176
|
+
(@categories[cat][:sum_of_squares] / (@categories[cat][:hits] - 1))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Get the average duration of a all categories.
|
|
180
|
+
def mean_overall
|
|
181
|
+
sum_overall / hits_overall
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Get the cumlative duration of a all categories.
|
|
185
|
+
def sum_overall
|
|
186
|
+
@categories.inject(0.0) { |sum, (name, cat)| sum + cat[:sum] }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get the total hits of a all categories.
|
|
190
|
+
def hits_overall
|
|
191
|
+
@categories.inject(0) { |sum, (name, cat)| sum + cat[:hits] }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Return categories sorted by a given key.
|
|
195
|
+
# <tt>by</tt> The key to sort on. This parameter can be omitted if a sorting block is provided instead
|
|
196
|
+
def sorted_by(by = nil)
|
|
197
|
+
if block_given?
|
|
198
|
+
categories.sort { |a, b| yield(b[1]) <=> yield(a[1]) }
|
|
199
|
+
else
|
|
200
|
+
categories.sort { |a, b| send(by, b[0]) <=> send(by, a[0]) }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Returns the column header for a statistics table to report on the statistics result
|
|
205
|
+
def statistics_header(options)
|
|
206
|
+
[
|
|
207
|
+
{:title => options[:title], :width => :rest},
|
|
208
|
+
{:title => 'Hits', :align => :right, :highlight => (options[:highlight] == :hits), :min_width => 4},
|
|
209
|
+
{:title => 'Sum', :align => :right, :highlight => (options[:highlight] == :sum), :min_width => 6},
|
|
210
|
+
{:title => 'Mean', :align => :right, :highlight => (options[:highlight] == :mean), :min_width => 6},
|
|
211
|
+
{:title => 'StdDev', :align => :right, :highlight => (options[:highlight] == :stddev), :min_width => 6},
|
|
212
|
+
{:title => 'Min', :align => :right, :highlight => (options[:highlight] == :min), :min_width => 6},
|
|
213
|
+
{:title => 'Max', :align => :right, :highlight => (options[:highlight] == :max), :min_width => 6}
|
|
214
|
+
]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Returns a row of statistics information for a report table, given a category
|
|
218
|
+
def statistics_row(cat)
|
|
219
|
+
[cat, hits(cat), display_value(sum(cat)), display_value(mean(cat)), display_value(stddev(cat)),
|
|
220
|
+
display_value(min(cat)), display_value(max(cat))]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|