request-log-analyzer 1.5.2 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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