request-log-analyzer 1.5.2 → 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. data/bin/request-log-analyzer +9 -5
  2. data/lib/request_log_analyzer.rb +1 -1
  3. data/lib/request_log_analyzer/aggregator/echo.rb +2 -1
  4. data/lib/request_log_analyzer/aggregator/summarizer.rb +5 -37
  5. data/lib/request_log_analyzer/controller.rb +4 -3
  6. data/lib/request_log_analyzer/file_format.rb +68 -1
  7. data/lib/request_log_analyzer/file_format/amazon_s3.rb +3 -1
  8. data/lib/request_log_analyzer/file_format/apache.rb +7 -2
  9. data/lib/request_log_analyzer/file_format/merb.rb +3 -1
  10. data/lib/request_log_analyzer/file_format/mysql.rb +10 -5
  11. data/lib/request_log_analyzer/file_format/rack.rb +1 -3
  12. data/lib/request_log_analyzer/file_format/rails.rb +5 -5
  13. data/lib/request_log_analyzer/output.rb +4 -0
  14. data/lib/request_log_analyzer/output/fancy_html.rb +31 -0
  15. data/lib/request_log_analyzer/tracker.rb +0 -105
  16. data/lib/request_log_analyzer/tracker/duration.rb +3 -62
  17. data/lib/request_log_analyzer/tracker/frequency.rb +18 -5
  18. data/lib/request_log_analyzer/tracker/numeric_value.rb +223 -0
  19. data/lib/request_log_analyzer/tracker/traffic.rb +6 -72
  20. data/request-log-analyzer.gemspec +4 -4
  21. data/spec/lib/mocks.rb +1 -0
  22. data/spec/unit/file_format/common_regular_expressions_spec.rb +53 -0
  23. data/spec/unit/file_format/mysql_format_spec.rb +1 -2
  24. data/spec/unit/file_format/rails_format_spec.rb +9 -4
  25. data/spec/unit/tracker/duration_tracker_spec.rb +2 -83
  26. data/spec/unit/tracker/{count_tracker_spec.rb → numeric_value_tracker_spec.rb} +50 -7
  27. data/spec/unit/tracker/tracker_api_spec.rb +3 -2
  28. data/spec/unit/tracker/traffic_tracker_spec.rb +0 -79
  29. metadata +8 -5
  30. data/lib/request_log_analyzer/tracker/count.rb +0 -93
@@ -70,11 +70,11 @@ rescue CommandLine::Error => e
70
70
  puts "Usage: request-log-analyzer [LOGFILES*] <OPTIONS>"
71
71
  puts
72
72
  puts "Input options:"
73
- puts " --format <format>, -f: Log file format. amazon_s3, apache, merb, mysql, rack or rails (default)."
74
73
  puts " --after <date> Only consider requests from <date> or later."
75
74
  puts " --before <date> Only consider requests before <date>."
76
- puts " --select <field> <value> Only consider requests where <field> matches <value>."
75
+ puts " --format <format>, -f: Log file format. amazon_s3, apache, merb, mysql, rack or rails."
77
76
  puts " --reject <field> <value> Only consider requests where <field> does not match <value>."
77
+ puts " --select <field> <value> Only consider requests where <field> matches <value>."
78
78
  puts
79
79
  puts "Output options:"
80
80
  puts " --boring, -b Output reports without ASCII colors."
@@ -83,13 +83,17 @@ rescue CommandLine::Error => e
83
83
  puts " --file <filename> Redirect output to file."
84
84
  puts " --mail <emailaddress> Send report to an email address."
85
85
  puts " --mailhost <server> Use the given server as the SMTP server for sending email."
86
- puts " --output <format> Output format. Supports 'html' and 'fixed_width' (default)."
86
+ puts " --no-progress Hide the progress bar."
87
+ puts " --output <format> Output format. Supports 'html' and 'fixed_width'."
88
+ puts " --report-width <amount> Width of ASCII report. Defaults to terminal width."
89
+ puts " --report-amount <amount> Maximum numer of results per report."
87
90
  puts " --yaml <filename> Dump the results in YAML format in the given file."
88
91
  puts
89
92
  puts "Examples:"
90
- puts " request-log-analyzer development.log"
91
- puts " request-log-analyzer -b mongrel.0.log mongrel.1.log mongrel.2.log "
93
+ puts " request-log-analyzer production.log"
94
+ puts " request-log-analyzer mongrel.0.log mongrel.1.log --output HTML --mail root@localhost"
92
95
  puts " request-log-analyzer --format merb -d requests.db production.log"
96
+ puts " request-log-analyzer mysql_slow_query.log --reject query /SQL_NO_CACHE/"
93
97
  puts
94
98
  puts "To install rake tasks in your Rails application, "
95
99
  puts "run the following command in your application's root directory:"
@@ -11,7 +11,7 @@ module RequestLogAnalyzer
11
11
 
12
12
  # The current version of request-log-analyzer.
13
13
  # Do not change the value by hand; it will be updated automatically by the gem release script.
14
- VERSION = "1.5.2"
14
+ VERSION = "1.5.3"
15
15
 
16
16
  # Loads constants in the RequestLogAnalyzer namespace using self.load_default_class_file(base, const)
17
17
  # <tt>const</tt>:: The constant that is not yet loaded in the RequestLogAnalyzer namespace. This should be passed as a string or symbol.
@@ -11,7 +11,8 @@ module RequestLogAnalyzer::Aggregator
11
11
 
12
12
  # Display every parsed line immediately to the terminal
13
13
  def aggregate(request)
14
- puts "\nRequest: " + request.lines.inspect
14
+ puts "\nRequest: \n" + request.lines.map { |l|
15
+ "\t#{l[:lineno]}:#{l[:line_type]}: #{l.reject { |(k,v)| [:lineno, :line_type].include?(k) }.inspect}" }.join("\n")
15
16
  end
16
17
 
17
18
  # Capture all warnings during parsing
@@ -26,44 +26,12 @@ module RequestLogAnalyzer::Aggregator
26
26
  def method_missing(tracker_method, *args)
27
27
  track(tracker_method, *args)
28
28
  end
29
-
30
- # Track the frequency of a specific category
31
- # <tt>category_field</tt> Field to track
32
- # <tt>options</tt> options are passed to new frequency tracker
33
- def frequency(category_field, options = {})
34
- if category_field.kind_of?(Symbol)
35
- track(:frequency, options.merge(:category => category_field))
36
- elsif category_field.kind_of?(Hash)
37
- track(:frequency, category_field.merge(options))
38
- end
39
- end
40
-
41
- # Track the duration of a specific category
42
- # <tt>duration_field</tt> Field to track
43
- # <tt>options</tt> options are passed to new frequency tracker
44
- def duration(duration_field, options = {})
45
- if duration_field.kind_of?(Symbol)
46
- track(:duration, options.merge(:duration => duration_field))
47
- elsif duration_field.kind_of?(Hash)
48
- track(:duration, duration_field.merge(options))
49
- end
50
- end
51
-
52
- # Track the frequency of a specific category
53
- # <tt>category_field</tt> Field to track
54
- # <tt>options</tt> options are passed to new frequency tracker
55
- def count(category_field, options = {})
56
- if category_field.kind_of?(Symbol)
57
- track(:count, options.merge(:category => category_field))
58
- elsif category_field.kind_of?(Hash)
59
- track(:count, category_field.merge(options))
60
- end
61
- end
62
-
29
+
63
30
  # Helper function to initialize a tracker and add it to the tracker array.
64
31
  # <tt>tracker_class</tt> The class to include
65
32
  # <tt>optiont</tt> The options to pass to the trackers.
66
- def track(tracker_klass, options = {})
33
+ def track(tracker_klass, value_field = {}, other_options = {})
34
+ options = value_field.kind_of?(Symbol) ? other_options.merge(:value => value_field) : value_field.merge(other_options)
67
35
  tracker_klass = RequestLogAnalyzer::Tracker.const_get(RequestLogAnalyzer::to_camelcase(tracker_klass)) if tracker_klass.kind_of?(Symbol)
68
36
  @trackers << tracker_klass.new(options)
69
37
  end
@@ -125,7 +93,7 @@ module RequestLogAnalyzer::Aggregator
125
93
  def report(output)
126
94
  report_header(output)
127
95
  if source.parsed_requests > 0
128
- @trackers.each { |tracker| tracker.report(output) }
96
+ @trackers.each { |tracker| output.report_tracker(tracker) }
129
97
  else
130
98
  output.puts
131
99
  output.puts('There were no requests analyzed.')
@@ -156,7 +124,7 @@ module RequestLogAnalyzer::Aggregator
156
124
  if has_log_ordering_warnings?
157
125
  output.title("Parse warnings")
158
126
 
159
- output.puts "Parseable lines were encountered without a header line before it. It"
127
+ output.puts "Parsable lines were encountered without a header line before it. It"
160
128
  output.puts "could be that logging is not setup correctly for your application."
161
129
  output.puts "Visit this website for logging configuration tips:"
162
130
  output.puts output.link("http://github.com/wvanbergen/request-log-analyzer/wikis/configure-logging")
@@ -53,8 +53,9 @@ module RequestLogAnalyzer
53
53
 
54
54
  # Handle output format casing
55
55
  if options[:output].class == String
56
- options[:output] = 'HTML' if options[:output] == 'html'
57
- options[:output] = 'FixedWidth' if options[:output] == 'fixedwidth' || options[:output] == 'fixed_width'
56
+ options[:output] = 'FancyHTML' if options[:output] =~ /^fancy_?html$/i
57
+ options[:output] = 'HTML' if options[:output] =~ /^html$/i
58
+ options[:output] = 'FixedWidth' if options[:output] =~ /^fixed_?width$/i
58
59
  end
59
60
 
60
61
  # Register sources
@@ -142,7 +143,7 @@ module RequestLogAnalyzer
142
143
  # Set the output class
143
144
  output_args = {}
144
145
  output_object = nil
145
- if options[:output].is_a? Class
146
+ if options[:output].is_a?(Class)
146
147
  output_class = options[:output]
147
148
  else
148
149
  output_class = RequestLogAnalyzer::Output::const_get(options[:output])
@@ -88,10 +88,77 @@ module RequestLogAnalyzer::FileFormat
88
88
 
89
89
  # As Apache matches several simular formats, subtracting 1 will make a specific matcher have a higher score
90
90
  score -= 1 if parser.file_format.class == RequestLogAnalyzer::FileFormat::Apache
91
-
91
+
92
92
  score
93
93
  end
94
94
 
95
+ # This module contains some methods to construct regular expressions for log fragments
96
+ # that are commonly used, like IP addresses and timestamp.
97
+ #
98
+ # You need to extend (or include in an unlikely case) this module in your file format
99
+ # to use these regular expression constructors.
100
+ module CommonRegularExpressions
101
+
102
+ TIMESTAMP_PARTS = {
103
+ 'a' => '(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)',
104
+ 'b' => '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)',
105
+ 'y' => '\d{2}', 'Y' => '\d{4}', 'm' => '\d{2}', 'd' => '\d{2}',
106
+ 'H' => '\d{2}', 'M' => '\d{2}', 'S' => '\d{2}', 'k' => '(?:\d| )\d',
107
+ 'z' => '(?:[+-]\d{4}|[A-Z]{3,4})',
108
+ 'Z' => '(?:[+-]\d{4}|[A-Z]{3,4})',
109
+ '%' => '%'
110
+ }
111
+
112
+ # Create a regular expression for a timestamp, generated by a strftime call.
113
+ # Provide the format string to construct a matching regular expression.
114
+ # Set blank to true to allow and empty string, or set blank to a string to set
115
+ # a substitute for the nil value.
116
+ def timestamp(format_string, blank = false)
117
+ regexp = ''
118
+ format_string.scan(/([^%]*)(?:%([A-Za-z%]))?/) do |literal, variable|
119
+ regexp << Regexp.quote(literal)
120
+ if variable
121
+ if TIMESTAMP_PARTS.has_key?(variable)
122
+ regexp << TIMESTAMP_PARTS[variable]
123
+ else
124
+ raise "Unknown variable: %#{variable}"
125
+ end
126
+ end
127
+ end
128
+
129
+ regexp = Regexp.new(regexp)
130
+ return case blank
131
+ when String then Regexp.union(regexp, Regexp.new(Regexp.quote(blank)))
132
+ when true then Regexp.union(regexp, //)
133
+ else regexp
134
+ end
135
+ end
136
+
137
+ # Construct a regular expression to parse IPv4 and IPv6 addresses.
138
+ #
139
+ # Allow nil values if the blank option is given. This can be true to
140
+ # allow an empty string or to a string substitute for the nil value.
141
+ def ip_address(blank = false)
142
+
143
+ # IP address regexp copied from Resolv::IPv4 and Resolv::IPv6,
144
+ # but adjusted to work for the purpose of request-log-analyzer.
145
+ ipv4_regexp = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
146
+ ipv6_regex_8_hex = /(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}/
147
+ ipv6_regex_compressed_hex = /(?:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::(?:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)/
148
+ ipv6_regex_6_hex_4_dec = /(?:(?:[0-9A-Fa-f]{1,4}:){6})#{ipv4_regexp}/
149
+ ipv6_regex_compressed_hex_4_dec = /(?:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::(?:(?:[0-9A-Fa-f]{1,4}:)*)#{ipv4_regexp}/
150
+ ipv6_regexp = Regexp.union(ipv6_regex_8_hex, ipv6_regex_compressed_hex, ipv6_regex_6_hex_4_dec, ipv6_regex_compressed_hex_4_dec)
151
+
152
+ # Allow the field to be blank if this option is given. This can be true to
153
+ # allow an empty string or a string alternative for the nil value.
154
+ ip_regexp = case blank
155
+ when String then Regexp.union(ipv4_regexp, ipv6_regexp, Regexp.new(Regexp.quote(blank)))
156
+ when true then Regexp.union(ipv4_regexp, ipv6_regexp, //)
157
+ else Regexp.union(ipv4_regexp, ipv6_regexp)
158
+ end
159
+ end
160
+ end
161
+
95
162
  # Base class for all log file format definitions. This class provides functions for subclasses to
96
163
  # define their LineDefinitions and to define a summary report.
97
164
  #
@@ -5,11 +5,13 @@ module RequestLogAnalyzer::FileFormat
5
5
  # Access logs are disabled by default on Amazon S3. To enable logging, see
6
6
  # http://docs.amazonwebservices.com/AmazonS3/latest/index.html?ServerLogs.html
7
7
  class AmazonS3 < Base
8
+
9
+ extend CommonRegularExpressions
8
10
 
9
11
  line_definition :access do |line|
10
12
  line.header = true
11
13
  line.footer = true
12
- line.regexp = /^([^\ ]+) ([^\ ]+) \[(\d{2}\/[A-Za-z]{3}\/\d{4}.\d{2}:\d{2}:\d{2})(?: .\d{4})?\] ([\d\.:]+) ([^\ ]+) ([^\ ]+) (\w+(?:\.\w+)*) ([^\ ]+) "([^"]+)" (\d+) ([^\ ]+) (\d+) (\d+) (\d+) (\d+) "([^"]+)" "([^"]+)"/
14
+ line.regexp = /^([^\ ]+) ([^\ ]+) \[(#{timestamp('%d/%b/%Y:%H:%M:%S %z')})?\] (#{ip_address}) ([^\ ]+) ([^\ ]+) (\w+(?:\.\w+)*) ([^\ ]+) "([^"]+)" (\d+) ([^\ ]+) (\d+) (\d+) (\d+) (\d+) "([^"]+)" "([^"]+)"/
13
15
  line.captures << { :name => :bucket_owner, :type => :string } <<
14
16
  { :name => :bucket, :type => :string } <<
15
17
  { :name => :timestamp, :type => :timestamp } <<
@@ -14,6 +14,8 @@ module RequestLogAnalyzer::FileFormat
14
14
  # command line option.
15
15
  class Apache < Base
16
16
 
17
+ extend CommonRegularExpressions
18
+
17
19
  # A hash of predefined Apache log formats
18
20
  LOG_FORMAT_DEFAULTS = {
19
21
  :common => '%h %l %u %t "%r" %>s %b',
@@ -23,17 +25,20 @@ module RequestLogAnalyzer::FileFormat
23
25
  :agent => '%{User-agent}i'
24
26
  }
25
27
 
28
+ # I have encountered two timestamp types, with timezone and without. Parse both.
29
+ APACHE_TIMESTAMP = Regexp.union(timestamp('%d/%b/%Y:%H:%M:%S %z'), timestamp('%d/%b/%Y %H:%M:%S'))
30
+
26
31
  # A hash that defines how the log format directives should be parsed.
27
32
  LOG_DIRECTIVES = {
28
33
  '%' => { :regexp => '%', :captures => [] },
29
34
  'h' => { :regexp => '([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)', :captures => [{:name => :remote_host, :type => :string}] },
30
- 'a' => { :regexp => '([\d\.:]+)', :captures => [{:name => :remote_ip, :type => :string}] },
35
+ 'a' => { :regexp => "(#{ip_address})", :captures => [{:name => :remote_ip, :type => :string}] },
31
36
  'b' => { :regexp => '(\d+|-)', :captures => [{:name => :bytes_sent, :type => :traffic}] },
32
37
  'c' => { :regexp => '(\+|\-|\X)', :captures => [{:name => :connection_status, :type => :integer}] },
33
38
  'D' => { :regexp => '(\d+|-)', :captures => [{:name => :duration, :type => :duration, :unit => :musec}] },
34
39
  'l' => { :regexp => '([\w-]+)', :captures => [{:name => :remote_logname, :type => :nillable_string}] },
35
40
  'T' => { :regexp => '((?:\d+(?:\.\d+))|-)', :captures => [{:name => :duration, :type => :duration, :unit => :sec}] },
36
- 't' => { :regexp => '\[(\d{2}\/[A-Za-z]{3}\/\d{4}.\d{2}:\d{2}:\d{2})(?: .\d{4})?\]', :captures => [{:name => :timestamp, :type => :timestamp}] },
41
+ 't' => { :regexp => "\\[(#{APACHE_TIMESTAMP})?\\]", :captures => [{:name => :timestamp, :type => :timestamp}] },
37
42
  's' => { :regexp => '(\d{3})', :captures => [{:name => :http_status, :type => :integer}] },
38
43
  'u' => { :regexp => '(\w+|-)', :captures => [{:name => :user, :type => :nillable_string}] },
39
44
  'U' => { :regexp => '(\/\S*)', :captures => [{:name => :path, :type => :string}] },
@@ -5,11 +5,13 @@ module RequestLogAnalyzer::FileFormat
5
5
  # the different request durations that can be used for analysis.
6
6
  class Merb < Base
7
7
 
8
+ extend CommonRegularExpressions
9
+
8
10
  # ~ Started request handling: Fri Aug 29 11:10:23 +0200 2008
9
11
  line_definition :started do |line|
10
12
  line.header = true
11
13
  line.teaser = /Started request handling\:/
12
- line.regexp = /Started request handling\:\ (.+)/
14
+ line.regexp = /Started request handling\:\ (#{timestamp('%a %b %d %H:%M:%S %z %Y')})/
13
15
  line.captures << { :name => :timestamp, :type => :timestamp }
14
16
  end
15
17
 
@@ -2,17 +2,19 @@ module RequestLogAnalyzer::FileFormat
2
2
 
3
3
  class Mysql < Base
4
4
 
5
+ extend CommonRegularExpressions
6
+
5
7
  line_definition :time do |line|
6
8
  line.header = :alternative
7
9
  line.teaser = /\# Time: /
8
- line.regexp = /\# Time: (\d{6}\s+\d{1,2}:\d{2}:\d{2})/
10
+ line.regexp = /\# Time: (#{timestamp('%y%m%d %k:%M:%S')})/
9
11
  line.captures << { :name => :timestamp, :type => :timestamp }
10
12
  end
11
13
 
12
14
  line_definition :user_host do |line|
13
15
  line.header = :alternative
14
16
  line.teaser = /\# User\@Host\: /
15
- line.regexp = /\# User\@Host\: ([\w-]+)\[[\w-]+\] \@ ([\w\.-]*) \[([\d\.]*)\]/
17
+ line.regexp = /\# User\@Host\: ([\w-]+)\[[\w-]+\] \@ ([\w\.-]*) \[(#{ip_address(true)})\]/
16
18
  line.captures << { :name => :user, :type => :string } <<
17
19
  { :name => :host, :type => :string } <<
18
20
  { :name => :ip, :type => :string }
@@ -53,9 +55,12 @@ module RequestLogAnalyzer::FileFormat
53
55
  analyze.frequency :user, :title => "Users with most queries"
54
56
  analyze.duration :query_time, :category => PER_USER, :title => 'Query time per user'
55
57
  analyze.duration :query_time, :category => PER_USER_QUERY, :title => 'Query time'
56
- # => analyze.duration :lock_time, :category => PER_USER_QUERY, :title => 'Lock time'
57
- analyze.count :category => PER_USER_QUERY, :title => "Rows examined", :field => :rows_examined
58
- analyze.count :category => PER_USER_QUERY, :title => "Rows sent", :field => :rows_sent
58
+
59
+ analyze.duration :lock_time, :category => PER_USER_QUERY, :title => 'Lock time',
60
+ :if => lambda { |request| request[:lock_time] > 0.0 }
61
+
62
+ analyze.numeric_value :rows_examined, :category => PER_USER_QUERY, :title => "Rows examined"
63
+ analyze.numeric_value :rows_sent, :category => PER_USER_QUERY, :title => "Rows sent"
59
64
  end
60
65
 
61
66
  class Request < RequestLogAnalyzer::Request
@@ -5,7 +5,5 @@ module RequestLogAnalyzer::FileFormat
5
5
  def self.create(*args)
6
6
  super(:rack, *args)
7
7
  end
8
-
9
8
  end
10
-
11
- end
9
+ end
@@ -7,6 +7,8 @@ module RequestLogAnalyzer::FileFormat
7
7
  # method as first argument.
8
8
  class Rails < Base
9
9
 
10
+ extend CommonRegularExpressions
11
+
10
12
  # Creates a Rails FileFormat instance.
11
13
  #
12
14
  # The lines that will be parsed can be defined by the argument to this function,
@@ -64,11 +66,11 @@ module RequestLogAnalyzer::FileFormat
64
66
  end
65
67
 
66
68
  if lines.has_key?(:rendered)
67
- analyze.duration :render_duration, :category => :render_file, :multiple_per_request => true, :title => 'Partial rendering duration'
69
+ analyze.duration :render_duration, :category => :render_file, :multiple => true, :title => 'Partial rendering duration'
68
70
  end
69
71
 
70
72
  if lines.has_key?(:query_executed)
71
- analyze.duration :query_duration, :category => :query_sql, :multiple_per_request => true, :title => 'Query duration'
73
+ analyze.duration :query_duration, :category => :query_sql, :multiple => true, :title => 'Query duration'
72
74
  end
73
75
 
74
76
  return analyze.trackers + report_definer.trackers
@@ -86,7 +88,7 @@ module RequestLogAnalyzer::FileFormat
86
88
  LINE_DEFINITIONS = {
87
89
  :processing => RequestLogAnalyzer::LineDefinition.new(:processing, :header => true,
88
90
  :teaser => /Processing /,
89
- :regexp => /Processing ((?:\w+::)?\w+)#(\w+)(?: to (\w+))? \(for ([\d\.:]+) at (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\) \[([A-Z]+)\]/,
91
+ :regexp => /Processing ((?:\w+::)*\w+)#(\w+)(?: to (\w+))? \(for (#{ip_address}) at (#{timestamp('%Y-%m-%d %H:%M:%S')})\) \[([A-Z]+)\]/,
90
92
  :captures => [{ :name => :controller, :type => :string },
91
93
  { :name => :action, :type => :string },
92
94
  { :name => :format, :type => :string, :default => 'html' },
@@ -170,7 +172,5 @@ module RequestLogAnalyzer::FileFormat
170
172
  sql.gsub(/\b\d+\b/, ':int').gsub(/`([^`]+)`/, '\1').gsub(/'[^']*'/, ':string').rstrip
171
173
  end
172
174
  end
173
-
174
175
  end
175
-
176
176
  end
@@ -56,6 +56,10 @@ module RequestLogAnalyzer::Output
56
56
  @options = options
57
57
  @style = options[:style] || { :cell_separator => true, :table_border => false }
58
58
  end
59
+
60
+ def report_tracker(tracker)
61
+ tracker.report(self)
62
+ end
59
63
 
60
64
  # Apply a style block.. with style :)
61
65
  def with_style(temp_style = {})
@@ -0,0 +1,31 @@
1
+ begin
2
+ require 'rubygems'
3
+ require 'gchart'
4
+ rescue LoadError
5
+ $stderr.puts "The FancyHTML output format requires the googlechart gem:"
6
+ $stderr.puts " (sudo) gem install googlecharts"
7
+ end
8
+
9
+ module RequestLogAnalyzer::Output
10
+
11
+ class FancyHTML < HTML
12
+
13
+ # Load class files if needed
14
+ def self.const_missing(const)
15
+ RequestLogAnalyzer::load_default_class_file(self, const)
16
+ end
17
+
18
+ def report_tracker(tracker)
19
+ case tracker
20
+ when RequestLogAnalyzer::Tracker::HourlySpread then report_hourly_spread(tracker)
21
+ else tracker.report(self)
22
+ end
23
+ end
24
+
25
+ def report_hourly_spread(tracker)
26
+ title tracker.title
27
+ puts tag(:img, nil, :width => '700', :height => '120', :src =>
28
+ Gchart.sparkline(:data => tracker.hour_frequencies, :size => '700x120', :line_colors => '0077CC'))
29
+ end
30
+ end
31
+ end
@@ -98,109 +98,4 @@ module RequestLogAnalyzer::Tracker
98
98
  nil
99
99
  end
100
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
101
  end