ngmoco-request-log-analyzer 1.4.2

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 (112) hide show
  1. data/.gitignore +10 -0
  2. data/DESIGN.rdoc +41 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +8 -0
  6. data/bin/request-log-analyzer +114 -0
  7. data/lib/cli/command_line_arguments.rb +301 -0
  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 +213 -0
  11. data/lib/cli/tools.rb +46 -0
  12. data/lib/request_log_analyzer.rb +44 -0
  13. data/lib/request_log_analyzer/aggregator.rb +49 -0
  14. data/lib/request_log_analyzer/aggregator/database_inserter.rb +83 -0
  15. data/lib/request_log_analyzer/aggregator/echo.rb +29 -0
  16. data/lib/request_log_analyzer/aggregator/summarizer.rb +175 -0
  17. data/lib/request_log_analyzer/controller.rb +332 -0
  18. data/lib/request_log_analyzer/database.rb +102 -0
  19. data/lib/request_log_analyzer/database/base.rb +115 -0
  20. data/lib/request_log_analyzer/database/connection.rb +38 -0
  21. data/lib/request_log_analyzer/database/request.rb +22 -0
  22. data/lib/request_log_analyzer/database/source.rb +13 -0
  23. data/lib/request_log_analyzer/database/warning.rb +14 -0
  24. data/lib/request_log_analyzer/file_format.rb +160 -0
  25. data/lib/request_log_analyzer/file_format/amazon_s3.rb +71 -0
  26. data/lib/request_log_analyzer/file_format/apache.rb +141 -0
  27. data/lib/request_log_analyzer/file_format/merb.rb +67 -0
  28. data/lib/request_log_analyzer/file_format/rack.rb +11 -0
  29. data/lib/request_log_analyzer/file_format/rails.rb +176 -0
  30. data/lib/request_log_analyzer/file_format/rails_development.rb +12 -0
  31. data/lib/request_log_analyzer/filter.rb +30 -0
  32. data/lib/request_log_analyzer/filter/anonymize.rb +39 -0
  33. data/lib/request_log_analyzer/filter/field.rb +42 -0
  34. data/lib/request_log_analyzer/filter/timespan.rb +45 -0
  35. data/lib/request_log_analyzer/line_definition.rb +111 -0
  36. data/lib/request_log_analyzer/log_processor.rb +99 -0
  37. data/lib/request_log_analyzer/mailer.rb +62 -0
  38. data/lib/request_log_analyzer/output.rb +113 -0
  39. data/lib/request_log_analyzer/output/fixed_width.rb +220 -0
  40. data/lib/request_log_analyzer/output/html.rb +184 -0
  41. data/lib/request_log_analyzer/request.rb +175 -0
  42. data/lib/request_log_analyzer/source.rb +72 -0
  43. data/lib/request_log_analyzer/source/database_loader.rb +87 -0
  44. data/lib/request_log_analyzer/source/log_parser.rb +274 -0
  45. data/lib/request_log_analyzer/tracker.rb +206 -0
  46. data/lib/request_log_analyzer/tracker/duration.rb +104 -0
  47. data/lib/request_log_analyzer/tracker/frequency.rb +95 -0
  48. data/lib/request_log_analyzer/tracker/hourly_spread.rb +107 -0
  49. data/lib/request_log_analyzer/tracker/timespan.rb +81 -0
  50. data/lib/request_log_analyzer/tracker/traffic.rb +106 -0
  51. data/request-log-analyzer.gemspec +40 -0
  52. data/spec/database.yml +23 -0
  53. data/spec/fixtures/apache_combined.log +5 -0
  54. data/spec/fixtures/apache_common.log +10 -0
  55. data/spec/fixtures/decompression.log +12 -0
  56. data/spec/fixtures/decompression.log.bz2 +0 -0
  57. data/spec/fixtures/decompression.log.gz +0 -0
  58. data/spec/fixtures/decompression.log.zip +0 -0
  59. data/spec/fixtures/decompression.tar.gz +0 -0
  60. data/spec/fixtures/decompression.tgz +0 -0
  61. data/spec/fixtures/header_and_footer.log +6 -0
  62. data/spec/fixtures/merb.log +84 -0
  63. data/spec/fixtures/merb_prefixed.log +9 -0
  64. data/spec/fixtures/multiple_files_1.log +5 -0
  65. data/spec/fixtures/multiple_files_2.log +2 -0
  66. data/spec/fixtures/rails.db +0 -0
  67. data/spec/fixtures/rails_1x.log +59 -0
  68. data/spec/fixtures/rails_22.log +12 -0
  69. data/spec/fixtures/rails_22_cached.log +10 -0
  70. data/spec/fixtures/rails_unordered.log +24 -0
  71. data/spec/fixtures/syslog_1x.log +5 -0
  72. data/spec/fixtures/test_file_format.log +13 -0
  73. data/spec/fixtures/test_language_combined.log +14 -0
  74. data/spec/fixtures/test_order.log +16 -0
  75. data/spec/integration/command_line_usage_spec.rb +84 -0
  76. data/spec/integration/munin_plugins_rails_spec.rb +58 -0
  77. data/spec/integration/scout_spec.rb +151 -0
  78. data/spec/lib/helpers.rb +52 -0
  79. data/spec/lib/macros.rb +18 -0
  80. data/spec/lib/matchers.rb +77 -0
  81. data/spec/lib/mocks.rb +76 -0
  82. data/spec/lib/testing_format.rb +46 -0
  83. data/spec/spec_helper.rb +24 -0
  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 +49 -0
  92. data/spec/unit/file_format/apache_format_spec.rb +203 -0
  93. data/spec/unit/file_format/file_format_api_spec.rb +69 -0
  94. data/spec/unit/file_format/line_definition_spec.rb +75 -0
  95. data/spec/unit/file_format/merb_format_spec.rb +52 -0
  96. data/spec/unit/file_format/rails_format_spec.rb +164 -0
  97. data/spec/unit/filter/anonymize_filter_spec.rb +21 -0
  98. data/spec/unit/filter/field_filter_spec.rb +66 -0
  99. data/spec/unit/filter/filter_spec.rb +17 -0
  100. data/spec/unit/filter/timespan_filter_spec.rb +58 -0
  101. data/spec/unit/mailer_spec.rb +30 -0
  102. data/spec/unit/request_spec.rb +111 -0
  103. data/spec/unit/source/log_parser_spec.rb +119 -0
  104. data/spec/unit/tracker/duration_tracker_spec.rb +130 -0
  105. data/spec/unit/tracker/frequency_tracker_spec.rb +88 -0
  106. data/spec/unit/tracker/hourly_spread_spec.rb +79 -0
  107. data/spec/unit/tracker/timespan_tracker_spec.rb +73 -0
  108. data/spec/unit/tracker/tracker_api_spec.rb +124 -0
  109. data/spec/unit/tracker/traffic_tracker_spec.rb +107 -0
  110. data/tasks/github-gem.rake +323 -0
  111. data/tasks/request_log_analyzer.rake +26 -0
  112. 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