wvanbergen-request-log-analyzer 1.0.0 → 1.0.1

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 (55) hide show
  1. data/DESIGN +14 -0
  2. data/HACKING +7 -0
  3. data/README.textile +9 -98
  4. data/Rakefile +2 -2
  5. data/bin/request-log-analyzer +1 -1
  6. data/lib/cli/bashcolorizer.rb +60 -0
  7. data/lib/cli/command_line_arguments.rb +301 -0
  8. data/lib/cli/progressbar.rb +236 -0
  9. data/lib/request_log_analyzer/aggregator/base.rb +51 -0
  10. data/lib/request_log_analyzer/aggregator/database.rb +97 -0
  11. data/lib/request_log_analyzer/aggregator/echo.rb +25 -0
  12. data/lib/request_log_analyzer/aggregator/summarizer.rb +116 -0
  13. data/lib/request_log_analyzer/controller.rb +206 -0
  14. data/lib/request_log_analyzer/file_format/merb.rb +33 -0
  15. data/lib/request_log_analyzer/file_format/rails.rb +119 -0
  16. data/lib/request_log_analyzer/file_format.rb +77 -0
  17. data/lib/request_log_analyzer/filter/base.rb +29 -0
  18. data/lib/request_log_analyzer/filter/field.rb +36 -0
  19. data/lib/request_log_analyzer/filter/timespan.rb +32 -0
  20. data/lib/request_log_analyzer/line_definition.rb +159 -0
  21. data/lib/request_log_analyzer/log_parser.rb +183 -0
  22. data/lib/request_log_analyzer/log_processor.rb +121 -0
  23. data/lib/request_log_analyzer/request.rb +115 -0
  24. data/lib/request_log_analyzer/source/base.rb +42 -0
  25. data/lib/request_log_analyzer/source/log_file.rb +180 -0
  26. data/lib/request_log_analyzer/tracker/base.rb +54 -0
  27. data/lib/request_log_analyzer/tracker/category.rb +71 -0
  28. data/lib/request_log_analyzer/tracker/duration.rb +81 -0
  29. data/lib/request_log_analyzer/tracker/hourly_spread.rb +80 -0
  30. data/lib/request_log_analyzer/tracker/timespan.rb +54 -0
  31. data/spec/file_format_spec.rb +78 -0
  32. data/spec/file_formats/spec_format.rb +26 -0
  33. data/spec/filter_spec.rb +137 -0
  34. data/spec/log_processor_spec.rb +57 -0
  35. data/tasks/rspec.rake +6 -0
  36. metadata +53 -55
  37. data/TODO +0 -58
  38. data/bin/request-log-database +0 -81
  39. data/lib/base/log_parser.rb +0 -78
  40. data/lib/base/record_inserter.rb +0 -139
  41. data/lib/command_line/arguments.rb +0 -129
  42. data/lib/command_line/flag.rb +0 -51
  43. data/lib/merb_analyzer/log_parser.rb +0 -26
  44. data/lib/rails_analyzer/log_parser.rb +0 -35
  45. data/lib/rails_analyzer/record_inserter.rb +0 -39
  46. data/tasks/test.rake +0 -8
  47. data/test/log_fragments/fragment_1.log +0 -59
  48. data/test/log_fragments/fragment_2.log +0 -5
  49. data/test/log_fragments/fragment_3.log +0 -12
  50. data/test/log_fragments/fragment_4.log +0 -10
  51. data/test/log_fragments/fragment_5.log +0 -24
  52. data/test/log_fragments/merb_1.log +0 -84
  53. data/test/merb_log_parser_test.rb +0 -39
  54. data/test/rails_log_parser_test.rb +0 -94
  55. data/test/record_inserter_test.rb +0 -45
@@ -0,0 +1,180 @@
1
+ module RequestLogAnalyzer::Source
2
+ # The LogParser class reads log data from a given source and uses a file format definition
3
+ # to parse all relevent information about requests from the file.
4
+ #
5
+ # A FileFormat module should be provided that contains the definitions of the lines that
6
+ # occur in the log data. The log parser can run in two modes:
7
+ # - In single line mode, it will emit every detected line as a separate request
8
+ # - In combined requests mode, it will combine the different lines from the line defintions
9
+ # into one request, that will then be emitted.
10
+ #
11
+ # The combined requests mode gives better information, but can be problematic if the log
12
+ # file is unordered. This can be the case if data is written to the log file simultaneously
13
+ # by different mongrel processes. This problem is detected by the parser, but the requests
14
+ # that are mixed up cannot be parsed. It will emit warnings when this occurs.
15
+ class LogFile < RequestLogAnalyzer::Source::Base
16
+
17
+ attr_reader :source_files
18
+
19
+ # Initializes the parser instance.
20
+ # It will apply the language specific FileFormat module to this instance. It will use the line
21
+ # definitions in this module to parse any input.
22
+ def initialize(format, options = {})
23
+ @line_definitions = {}
24
+ @options = options
25
+ @parsed_lines = 0
26
+ @parsed_requests = 0
27
+ @skipped_requests = 0
28
+ @current_io = nil
29
+ @source_files = options[:source_files]
30
+
31
+ # install the file format module (see RequestLogAnalyzer::FileFormat)
32
+ # and register all the line definitions to the parser
33
+ self.register_file_format(format)
34
+ end
35
+
36
+ def requests(options = {}, &block)
37
+
38
+ case @source_files
39
+ when IO;
40
+ puts "Parsing from the standard input. Press CTRL+C to finish."
41
+ parse_stream(@source_files, options, &block)
42
+ when String
43
+ parse_file(@source_files, options, &block)
44
+ when Array
45
+ parse_files(@source_files, options, &block)
46
+ else
47
+ raise "Unknown source provided"
48
+ end
49
+ end
50
+
51
+ # Parses a list of consequent files of the same format
52
+ def parse_files(files, options = {}, &block)
53
+ files.each { |file| parse_file(file, options, &block) }
54
+ end
55
+
56
+ # Parses a file.
57
+ # Creates an IO stream for the provided file, and sends it to parse_io for further handling
58
+ def parse_file(file, options = {}, &block)
59
+ @progress_handler.call(:started, file) if @progress_handler
60
+ File.open(file, 'r') { |f| parse_io(f, options, &block) }
61
+ @progress_handler.call(:finished, file) if @progress_handler
62
+ end
63
+
64
+ def parse_stream(stream, options = {}, &block)
65
+ parse_io(stream, options, &block)
66
+ end
67
+
68
+ # Finds a log line and then parses the information in the line.
69
+ # Yields a hash containing the information found.
70
+ # <tt>*line_types</tt> The log line types to look for (defaults to LOG_LINES.keys).
71
+ # Yeilds a Hash when it encounters a chunk of information.
72
+ def parse_io(io, options = {}, &block)
73
+
74
+ # parse every line type by default
75
+ line_types = options[:line_types] || file_format.line_definitions.keys
76
+
77
+ # check whether all provided line types are valid
78
+ unknown = line_types.reject { |line_type| file_format.line_definitions.has_key?(line_type) }
79
+ raise "Unknown line types: #{unknown.join(', ')}" unless unknown.empty?
80
+
81
+ puts "Parsing mode: " + (options[:combined_requests] ? 'combined requests' : 'single lines') if options[:debug]
82
+
83
+ @current_io = io
84
+ @current_io.each_line do |line|
85
+
86
+ @progress_handler.call(:progress, @current_io.pos) if @progress_handler && @current_io.kind_of?(File)
87
+
88
+ request_data = nil
89
+ line_types.each do |line_type|
90
+ line_type_definition = file_format.line_definitions[line_type]
91
+ break if request_data = line_type_definition.matches(line, @current_io.lineno, self)
92
+ end
93
+
94
+ if request_data
95
+ @parsed_lines += 1
96
+ if @options[:combined_requests]
97
+ update_current_request(request_data, &block)
98
+ else
99
+ @parsed_requests +=1
100
+ yield RequestLogAnalyzer::Request.create(@file_format, request_data)
101
+ end
102
+ end
103
+ end
104
+
105
+ warn(:unfinished_request_on_eof, "End of file reached, but last request was not completed!") unless @current_request.nil?
106
+
107
+ @current_io = nil
108
+ end
109
+
110
+ # Add a block to this method to install a progress handler while parsing
111
+ def progress=(proc)
112
+ @progress_handler = proc
113
+ end
114
+
115
+ # Add a block to this method to install a warning handler while parsing
116
+ def warning=(proc)
117
+ @warning_handler = proc
118
+ end
119
+
120
+ # This method is called by the parser if it encounteres any problems.
121
+ # It will call the warning handler. The default controller will pass all warnings to every
122
+ # aggregator that is registered and running
123
+ def warn(type, message)
124
+ @warning_handler.call(type, message, @current_io.lineno) if @warning_handler
125
+ end
126
+
127
+ protected
128
+
129
+ # Combines the different lines of a request into a single Request object.
130
+ # This function is only called in combined requests mode. It will start a new request when
131
+ # a header line is encountered en will emit the request when a footer line is encountered.
132
+ #
133
+ # - Every line that is parsed before a header line is ignored as it cannot be included in
134
+ # any request. It will emit a :no_current_request warning.
135
+ # - A header line that is parsed before a request is closed by a footer line, is a sign of
136
+ # an unprpertly ordered file. All data that is gathered for the request until then is
137
+ # discarded, the next request is ignored as well and a :unclosed_request warning is
138
+ # emitted.
139
+ def update_current_request(request_data, &block)
140
+ if header_line?(request_data)
141
+ unless @current_request.nil?
142
+ if options[:assume_correct_order]
143
+ @parsed_requests += 1
144
+ yield @current_request
145
+ @current_request = RequestLogAnalyzer::Request.create(@file_format, request_data)
146
+ else
147
+ @skipped_requests += 1
148
+ warn(:unclosed_request, "Encountered header line, but previous request was not closed!")
149
+ @current_request = nil # remove all data that was parsed, skip next request as well.
150
+ end
151
+ else
152
+ @current_request = RequestLogAnalyzer::Request.create(@file_format, request_data)
153
+ end
154
+ else
155
+ unless @current_request.nil?
156
+ @current_request << request_data
157
+ if footer_line?(request_data)
158
+ @parsed_requests += 1
159
+ yield @current_request
160
+ @current_request = nil
161
+ end
162
+ else
163
+ @skipped_requests += 1
164
+ warn(:no_current_request, "Parsebale line found outside of a request!")
165
+ end
166
+ end
167
+ end
168
+
169
+ # Checks whether a given line hash is a header line.
170
+ def header_line?(hash)
171
+ file_format.line_definitions[hash[:line_type]].header
172
+ end
173
+
174
+ # Checks whether a given line hash is a footer line.
175
+ def footer_line?(hash)
176
+ file_format.line_definitions[hash[:line_type]].footer
177
+ end
178
+ end
179
+
180
+ end
@@ -0,0 +1,54 @@
1
+ module RequestLogAnalyzer
2
+ module Tracker
3
+
4
+ # Base tracker. All other trackers inherit from this class
5
+ #
6
+ # Accepts the following options:
7
+ # * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
8
+ # * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
9
+ # * <tt>:output</tt> Direct output here (defaults to STDOUT)
10
+ #
11
+ # For example :if => lambda { |request| request[:duration] && request[:duration] > 1.0 }
12
+ class Base
13
+
14
+ attr_reader :options
15
+
16
+ def initialize(options ={})
17
+ @options = options
18
+ end
19
+
20
+ def prepare
21
+ end
22
+
23
+ def update(request)
24
+ end
25
+
26
+ def finalize
27
+ end
28
+
29
+ def should_update?(request)
30
+ return false if options[:line_type] && !request.has_line_type?(options[:line_type])
31
+
32
+ if options[:if].kind_of?(Symbol)
33
+ return false unless request[options[:if]]
34
+ elsif options[:if].respond_to?(:call)
35
+ return false unless options[:if].call(request)
36
+ end
37
+
38
+ if options[:unless].kind_of?(Symbol)
39
+ return false if request[options[:unless]]
40
+ elsif options[:unless].respond_to?(:call)
41
+ return false if options[:unless].call(request)
42
+ end
43
+
44
+ return true
45
+ end
46
+
47
+ def report(output=STDOUT, report_width = 80, color = false)
48
+ output << self.inspect
49
+ output << "\n"
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,71 @@
1
+ module RequestLogAnalyzer::Tracker
2
+
3
+ # Catagorize requests.
4
+ # Count and analyze requests for a specific attribute
5
+ #
6
+ # Accepts the following options:
7
+ # * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
8
+ # * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
9
+ # * <tt>:title</tt> Title do be displayed above the report.
10
+ # * <tt>:category</tt> Proc that handles the request categorization.
11
+ # * <tt>:amount</tt> The amount of lines in the report
12
+ #
13
+ # The items in the update request hash are set during the creation of the Duration tracker.
14
+ #
15
+ # Example output:
16
+ # HTTP methods
17
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
+ # GET ┃ 22248 hits (46.2%) ┃░░░░░░░░░░░░░░░░░
19
+ # PUT ┃ 13685 hits (28.4%) ┃░░░░░░░░░░░
20
+ # POST ┃ 11662 hits (24.2%) ┃░░░░░░░░░
21
+ # DELETE ┃ 512 hits (1.1%) ┃
22
+ class Category < RequestLogAnalyzer::Tracker::Base
23
+
24
+ attr_reader :categories
25
+
26
+ def prepare
27
+ raise "No categorizer set up for category tracker #{self.inspect}" unless options[:category]
28
+ @categories = {}
29
+ if options[:all_categories].kind_of?(Enumerable)
30
+ options[:all_categories].each { |cat| @categories[cat] = 0 }
31
+ end
32
+ end
33
+
34
+ def update(request)
35
+ cat = options[:category].respond_to?(:call) ? options[:category].call(request) : request[options[:category]]
36
+ if !cat.nil? || options[:nils]
37
+ @categories[cat] ||= 0
38
+ @categories[cat] += 1
39
+ end
40
+ end
41
+
42
+ def report(output = STDOUT, report_width = 80, color = false)
43
+ if options[:title]
44
+ output << "\n#{options[:title]}\n"
45
+ output << green(('━' * report_width), color) + "\n"
46
+ end
47
+
48
+ if @categories.empty?
49
+ output << "None found.\n"
50
+ else
51
+ sorted_categories = @categories.sort { |a, b| b[1] <=> a[1] }
52
+ total_hits = sorted_categories.inject(0) { |carry, item| carry + item[1] }
53
+ sorted_categories = sorted_categories.slice(0...options[:amount]) if options[:amount]
54
+
55
+ adjuster = color ? 33 : 24 # justifcation calcultaion is slight different when color codes are inserterted
56
+ max_cat_length = [sorted_categories.map { |c| c[0].to_s.length }.max, report_width - adjuster].min
57
+ sorted_categories.each do |(cat, count)|
58
+ text = "%-#{max_cat_length+1}s┃%7d hits %s" % [cat.to_s[0..max_cat_length], count, (green("(%0.01f%%)", color) % [(count.to_f / total_hits) * 100])]
59
+ space_left = report_width - (max_cat_length + adjuster + 3)
60
+ if space_left > 3
61
+ bar_chars = (space_left * (count.to_f / total_hits)).round
62
+ output << "%-#{max_cat_length + adjuster}s %s%s" % [text, '┃', '░' * bar_chars] + "\n"
63
+ else
64
+ output << text + "\n"
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,81 @@
1
+ module RequestLogAnalyzer::Tracker
2
+
3
+ # Analyze the duration of a specific attribute
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
9
+ # * <tt>:category</tt> Proc that handles request categorization for given fileformat (REQUEST_CATEGORIZER)
10
+ # * <tt>:duration</tt> The field containing the duration in the request hash.
11
+ # * <tt>:amount</tt> The amount of lines in the report
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 < RequestLogAnalyzer::Tracker::Base
23
+ attr_reader :categories
24
+
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 = {}
30
+ 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"
58
+ end
59
+ end
60
+
61
+ def report(output = STDOUT, report_width = 80, color = false)
62
+
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
80
+ end
81
+ end
@@ -0,0 +1,80 @@
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>:line_type</tt> The line type that contains the duration field (determined by the category proc).
8
+ # * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
9
+ #
10
+ # Expects the following items in the update request hash
11
+ # * <tt>:timestamp</tt> in YYYYMMDDHHMMSS format.
12
+ #
13
+ # Example output:
14
+ # 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 : ░░
28
+ # ................
29
+ class HourlySpread < RequestLogAnalyzer::Tracker::Base
30
+
31
+ attr_reader :first, :last, :request_time_graph
32
+
33
+ def prepare
34
+ 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]
36
+ end
37
+
38
+ def update(request)
39
+ request = request.attributes
40
+ timestamp = request[options[:field]]
41
+
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
45
+ 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]
53
+ output << "None found.\n"
54
+ return
55
+ end
56
+
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)
72
+ end
73
+
74
+ output << "#{index.to_s.rjust(3)}:00 - #{(request_today.to_s + ' hits').ljust(15)} : #{display_chars_string}\n"
75
+ end
76
+ output << "\n"
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,54 @@
1
+ module RequestLogAnalyzer::Tracker
2
+
3
+ # Determines the datetime of the first request and the last request
4
+ # Also determines the amount of days inbetween these.
5
+ #
6
+ # Accepts the following options:
7
+ # * <tt>:line_type</tt> The line type that contains the duration field (determined by the category proc).
8
+ # * <tt>:if</tt> Proc that has to return !nil for a request to be passed to the tracker.
9
+ # * <tt>:field</tt> The timestamp field that is looked at. Defaults to :timestamp.
10
+ # * <tt>:title</tt> Title do be displayed above the report.
11
+ #
12
+ # Expects the following items in the update request hash
13
+ # * <tt>:timestamp</tt> in YYYYMMDDHHMMSS format.
14
+ #
15
+ # Example output:
16
+ # First request: 2008-07-13 06:25:06
17
+ # Last request: 2008-07-20 06:18:06
18
+ # Total time analyzed: 7 days
19
+ class Timespan < RequestLogAnalyzer::Tracker::Base
20
+
21
+ attr_reader :first, :last, :request_time_graph
22
+
23
+ def prepare
24
+ options[:field] ||= :timestamp
25
+ end
26
+
27
+ def update(request)
28
+ timestamp = request[options[:field]]
29
+
30
+ @first = timestamp if @first.nil? || timestamp < @first
31
+ @last = timestamp if @last.nil? || timestamp > @last
32
+ end
33
+
34
+ def report(output = STDOUT, report_width = 80, color = false)
35
+ if options[:title]
36
+ output << "\n#{options[:title]}\n"
37
+ output << green('━' * options[:title].length, color) + "\n"
38
+ end
39
+
40
+ first_date = DateTime.parse(@first.to_s, '%Y%m%d%H%M%S') rescue nil
41
+ last_date = DateTime.parse(@last.to_s, '%Y%m%d%H%M%S') rescue nil
42
+
43
+ if @last && @first
44
+ days = (@last && @first) ? (last_date - first_date).ceil : 1
45
+
46
+ output << "First request: #{first_date.strftime('%Y-%m-%d %H:%M:%I')}\n"
47
+ output << "Last request: #{last_date.strftime('%Y-%m-%d %H:%M:%I')}\n"
48
+ output << "Total time analyzed: #{days} days\n"
49
+ end
50
+ output << "\n"
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,78 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe RequestLogAnalyzer::FileFormat, :format_definition do
4
+
5
+ before(:each) do
6
+ @first_file_format = Class.new(RequestLogAnalyzer::FileFormat)
7
+ @second_file_format = Class.new(RequestLogAnalyzer::FileFormat)
8
+ end
9
+
10
+ it "should specify lines with a hash" do
11
+
12
+ @first_file_format.new.should have(0).line_definitions
13
+
14
+ @first_file_format.format_definition do |line|
15
+ line.hash_test :regexp => /test/, :captures => []
16
+ end
17
+
18
+ @format_instance = @first_file_format.new
19
+ @format_instance.should have(1).line_definitions
20
+ @format_instance.line_definitions[:hash_test].should_not be_nil
21
+ end
22
+
23
+ it "should specift lines directly" do
24
+ @first_file_format.new.should have(0).line_definitions
25
+
26
+ @first_file_format.format_definition.direct_test do |line|
27
+ line.regexp = /test/
28
+ end
29
+
30
+ @first_file_format.new.line_definitions[:direct_test].should_not be_nil
31
+ end
32
+
33
+ it "specify lines with a block" do
34
+
35
+ @first_file_format.new.should have(0).line_definitions
36
+
37
+ @first_file_format.format_definition do |format|
38
+ format.block_test do |line|
39
+ line.regexp = /test/
40
+ line.captures = []
41
+ end
42
+ end
43
+
44
+ @first_file_format.new.line_definitions[:block_test].should_not be_nil
45
+ end
46
+
47
+ it "should define lines only for itself" do
48
+
49
+ @first_file_format.format_definition do |line|
50
+ line.first_test :regexp => /test/, :captures => []
51
+ end
52
+
53
+ @second_file_format.format_definition do |line|
54
+ line.second_test :regexp => /test/, :captures => []
55
+ end
56
+
57
+ @first_file_format.new.should have(1).line_definitions
58
+ @first_file_format.new.line_definitions[:first_test].should_not be_nil
59
+ @second_file_format.new.should have(1).line_definitions
60
+ @second_file_format.new.line_definitions[:second_test].should_not be_nil
61
+ end
62
+ end
63
+
64
+ describe RequestLogAnalyzer::FileFormat, :load do
65
+
66
+ include RequestLogAnalyzerSpecHelper
67
+
68
+ it "should load a predefined file format from the /file_format dir" do
69
+ @file_format = RequestLogAnalyzer::FileFormat.load(:rails)
70
+ @file_format.should be_kind_of(RequestLogAnalyzer::FileFormat::Rails)
71
+ end
72
+
73
+ it "should load a provided format file" do
74
+ @file_format = RequestLogAnalyzer::FileFormat.load(format_file(:spec_format))
75
+ @file_format.should be_kind_of(SpecFormat)
76
+ end
77
+
78
+ end
@@ -0,0 +1,26 @@
1
+ class SpecFormat < RequestLogAnalyzer::FileFormat
2
+
3
+ format_definition.first do |line|
4
+ line.header = true
5
+ line.teaser = /processing /
6
+ line.regexp = /processing request (\d+)/
7
+ line.captures = [{ :name => :request_no, :type => :integer, :anonymize => :slightly }]
8
+ end
9
+
10
+ format_definition.test do |line|
11
+ line.teaser = /testing /
12
+ line.regexp = /testing is (\w+)/
13
+ line.captures = [{ :name => :test_capture, :type => :string, :anonymize => true}]
14
+ end
15
+
16
+ format_definition.last do |line|
17
+ line.footer = true
18
+ line.teaser = /finishing /
19
+ line.regexp = /finishing request (\d+)/
20
+ line.captures = [{ :name => :request_no, :type => :integer}]
21
+ end
22
+
23
+ report do |analyze|
24
+ analyze.category :test_capture, :title => 'What is testing exactly?'
25
+ end
26
+ end