ngmoco-request-log-analyzer 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
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