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.
Files changed (144) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +4 -4
  4. data/README.rdoc +38 -0
  5. data/Rakefile +6 -3
  6. data/bin/request-log-analyzer +70 -72
  7. data/lib/cli/command_line_arguments.rb +53 -53
  8. data/lib/cli/database_console.rb +26 -0
  9. data/lib/cli/database_console_init.rb +43 -0
  10. data/lib/cli/progressbar.rb +166 -189
  11. data/lib/cli/tools.rb +49 -0
  12. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  13. data/lib/request_log_analyzer/aggregator/echo.rb +17 -12
  14. data/lib/request_log_analyzer/aggregator/summarizer.rb +101 -63
  15. data/lib/request_log_analyzer/{aggregator/base.rb → aggregator.rb} +17 -13
  16. data/lib/request_log_analyzer/controller.rb +251 -98
  17. data/lib/request_log_analyzer/database/base.rb +114 -0
  18. data/lib/request_log_analyzer/database/connection.rb +38 -0
  19. data/lib/request_log_analyzer/database/request.rb +22 -0
  20. data/lib/request_log_analyzer/database/source.rb +13 -0
  21. data/lib/request_log_analyzer/database/warning.rb +14 -0
  22. data/lib/request_log_analyzer/database.rb +102 -0
  23. data/lib/request_log_analyzer/file_format/amazon_s3.rb +74 -0
  24. data/lib/request_log_analyzer/file_format/apache.rb +147 -0
  25. data/lib/request_log_analyzer/file_format/delayed_job.rb +55 -0
  26. data/lib/request_log_analyzer/file_format/merb.rb +65 -29
  27. data/lib/request_log_analyzer/file_format/mysql.rb +101 -0
  28. data/lib/request_log_analyzer/file_format/postgresql.rb +68 -0
  29. data/lib/request_log_analyzer/file_format/rack.rb +9 -0
  30. data/lib/request_log_analyzer/file_format/rails.rb +164 -78
  31. data/lib/request_log_analyzer/file_format/rails3.rb +86 -0
  32. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  33. data/lib/request_log_analyzer/file_format.rb +252 -58
  34. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  35. data/lib/request_log_analyzer/filter/field.rb +19 -13
  36. data/lib/request_log_analyzer/filter/timespan.rb +25 -12
  37. data/lib/request_log_analyzer/filter.rb +30 -0
  38. data/lib/request_log_analyzer/line_definition.rb +69 -96
  39. data/lib/request_log_analyzer/log_processor.rb +31 -53
  40. data/lib/request_log_analyzer/mailer.rb +65 -0
  41. data/lib/request_log_analyzer/output/fancy_html.rb +49 -0
  42. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  43. data/lib/request_log_analyzer/output/html.rb +187 -0
  44. data/lib/request_log_analyzer/output.rb +117 -0
  45. data/lib/request_log_analyzer/request.rb +125 -40
  46. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  47. data/lib/request_log_analyzer/source/log_parser.rb +297 -0
  48. data/lib/request_log_analyzer/source.rb +72 -0
  49. data/lib/request_log_analyzer/tracker/duration.rb +28 -64
  50. data/lib/request_log_analyzer/tracker/frequency.rb +108 -0
  51. data/lib/request_log_analyzer/tracker/hourly_spread.rb +76 -49
  52. data/lib/request_log_analyzer/tracker/numeric_value.rb +223 -0
  53. data/lib/request_log_analyzer/tracker/timespan.rb +54 -27
  54. data/lib/request_log_analyzer/tracker/traffic.rb +40 -0
  55. data/lib/request_log_analyzer/tracker.rb +101 -0
  56. data/lib/request_log_analyzer.rb +43 -13
  57. data/request-log-analyzer.gemspec +41 -0
  58. data/spec/database.yml +23 -0
  59. data/spec/fixtures/apache_combined.log +5 -0
  60. data/spec/fixtures/apache_common.log +10 -0
  61. data/spec/fixtures/decompression.log +12 -0
  62. data/spec/fixtures/decompression.log.bz2 +0 -0
  63. data/spec/fixtures/decompression.log.gz +0 -0
  64. data/spec/fixtures/decompression.log.zip +0 -0
  65. data/spec/fixtures/decompression.tar.gz +0 -0
  66. data/spec/fixtures/decompression.tgz +0 -0
  67. data/spec/fixtures/header_and_footer.log +6 -0
  68. data/spec/fixtures/merb_prefixed.log +9 -0
  69. data/spec/fixtures/mysql_slow_query.log +110 -0
  70. data/spec/fixtures/postgresql.log +2980 -0
  71. data/spec/fixtures/rails.db +0 -0
  72. data/spec/fixtures/rails_22.log +1 -1
  73. data/spec/fixtures/sinatra.log +99 -0
  74. data/spec/integration/command_line_usage_spec.rb +84 -0
  75. data/spec/integration/mailer_spec.rb +179 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +152 -0
  78. data/spec/lib/helpers.rb +72 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +77 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +16 -59
  84. data/spec/unit/aggregator/database_inserter_spec.rb +93 -0
  85. data/spec/unit/aggregator/summarizer_spec.rb +26 -0
  86. data/spec/unit/controller/controller_spec.rb +41 -0
  87. data/spec/unit/controller/log_processor_spec.rb +18 -0
  88. data/spec/unit/database/base_class_spec.rb +183 -0
  89. data/spec/unit/database/connection_spec.rb +34 -0
  90. data/spec/unit/database/database_spec.rb +133 -0
  91. data/spec/unit/file_format/amazon_s3_format_spec.rb +67 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/common_regular_expressions_spec.rb +53 -0
  94. data/spec/unit/file_format/delayed_job_format_spec.rb +83 -0
  95. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  96. data/spec/unit/file_format/format_autodetection_spec.rb +40 -0
  97. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  98. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  99. data/spec/unit/file_format/mysql_format_spec.rb +154 -0
  100. data/spec/unit/file_format/postgresql_format_spec.rb +65 -0
  101. data/spec/unit/file_format/rack_format_spec.rb +50 -0
  102. data/spec/unit/file_format/rails3_format_spec.rb +120 -0
  103. data/spec/unit/file_format/rails_format_spec.rb +180 -0
  104. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  105. data/spec/unit/filter/field_filter_spec.rb +66 -0
  106. data/spec/unit/filter/filter_spec.rb +17 -0
  107. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  108. data/spec/unit/mailer_spec.rb +42 -0
  109. data/spec/unit/request_spec.rb +111 -0
  110. data/spec/unit/source/log_parser_spec.rb +119 -0
  111. data/spec/unit/tracker/duration_tracker_spec.rb +49 -0
  112. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  113. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  114. data/spec/unit/tracker/numeric_value_tracker_spec.rb +166 -0
  115. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  116. data/spec/unit/tracker/tracker_api_spec.rb +125 -0
  117. data/spec/unit/tracker/traffic_tracker_spec.rb +28 -0
  118. data/tasks/github-gem.rake +290 -139
  119. data/tasks/request_log_analyzer.rake +23 -7
  120. metadata +221 -94
  121. data/DESIGN +0 -14
  122. data/HACKING +0 -7
  123. data/README.textile +0 -36
  124. data/lib/cli/bashcolorizer.rb +0 -60
  125. data/lib/request_log_analyzer/aggregator/database.rb +0 -148
  126. data/lib/request_log_analyzer/filter/base.rb +0 -29
  127. data/lib/request_log_analyzer/log_parser.rb +0 -173
  128. data/lib/request_log_analyzer/source/base.rb +0 -42
  129. data/lib/request_log_analyzer/source/log_file.rb +0 -170
  130. data/lib/request_log_analyzer/tracker/base.rb +0 -54
  131. data/lib/request_log_analyzer/tracker/category.rb +0 -71
  132. data/spec/controller_spec.rb +0 -40
  133. data/spec/database_inserter_spec.rb +0 -101
  134. data/spec/file_format_spec.rb +0 -78
  135. data/spec/file_formats/spec_format.rb +0 -26
  136. data/spec/filter_spec.rb +0 -137
  137. data/spec/line_definition_spec.rb +0 -124
  138. data/spec/log_parser_spec.rb +0 -68
  139. data/spec/log_processor_spec.rb +0 -57
  140. data/spec/merb_format_spec.rb +0 -38
  141. data/spec/rails_format_spec.rb +0 -76
  142. data/spec/request_spec.rb +0 -72
  143. data/spec/summarizer_spec.rb +0 -9
  144. 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
- # Accepts the following options:
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>:amount</tt> The amount of lines in the report
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 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
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 < RequestLogAnalyzer::Tracker::Base
23
- attr_reader :categories
24
-
22
+ class Duration < NumericValue
23
+
24
+ # Check if duration and catagory option have been received,
25
25
  def prepare
26
- raise "No duration field set up for category tracker #{self.inspect}" unless options[:duration]
27
- raise "No categorizer set up for duration tracker #{self.inspect}" unless options[:category]
28
-
29
- @categories = {}
26
+ options[:value] = options[:duration] if options[:duration]
27
+ super
30
28
  end
31
-
32
- def update(request)
33
- category = options[:category].respond_to?(:call) ? options[:category].call(request) : request[options[:category]]
34
- duration = options[:duration].respond_to?(:call) ? options[:duration].call(request) : request[options[:duration]]
35
-
36
- if !duration.nil? && !category.nil?
37
- @categories[category] ||= {:count => 0, :total_duration => 0.0}
38
- @categories[category][:count] += 1
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
- options[:title] ||= 'Request duration'
64
- options[:report] ||= [:total, :average]
65
- options[:top] ||= 20
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 < RequestLogAnalyzer::Tracker::Base
31
+ class HourlySpread < Base
32
+
33
+ attr_reader :hour_frequencies, :first, :last
30
34
 
31
- attr_reader :first, :last, :request_time_graph
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
- @request_time_graph = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
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
- request = request.attributes
40
- timestamp = request[options[:field]]
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
- @request_time_graph[timestamp.to_s[8..9].to_i] +=1
43
- @first = timestamp if @first.nil? || timestamp < @first
44
- @last = timestamp if @last.nil? || timestamp > @last
51
+ # Total amount of requests tracked
52
+ def total_requests
53
+ @hour_frequencies.inject(0) { |sum, value| sum + value }
45
54
  end
46
-
47
- def report(output = STDOUT, report_width = 80, color = false)
48
- output << "\n"
49
- output << "Requests graph - average per day per hour\n"
50
- output << green("━" * report_width, color) + "\n"
51
-
52
- if @request_time_graph == [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
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
- first_date = DateTime.parse(@first.to_s, '%Y%m%d%H%M%S')
58
- last_date = DateTime.parse(@last.to_s, '%Y%m%d%H%M%S')
59
- days = (@last && @first) ? (last_date - first_date).ceil : 1
60
- deviation = @request_time_graph.max / 20
61
- deviation = 1 if deviation == 0
62
- color_cutoff = 15
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
- output << "\n"
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