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.
- data/bin/request-log-analyzer +9 -5
- data/lib/request_log_analyzer.rb +1 -1
- data/lib/request_log_analyzer/aggregator/echo.rb +2 -1
- data/lib/request_log_analyzer/aggregator/summarizer.rb +5 -37
- data/lib/request_log_analyzer/controller.rb +4 -3
- data/lib/request_log_analyzer/file_format.rb +68 -1
- data/lib/request_log_analyzer/file_format/amazon_s3.rb +3 -1
- data/lib/request_log_analyzer/file_format/apache.rb +7 -2
- data/lib/request_log_analyzer/file_format/merb.rb +3 -1
- data/lib/request_log_analyzer/file_format/mysql.rb +10 -5
- data/lib/request_log_analyzer/file_format/rack.rb +1 -3
- data/lib/request_log_analyzer/file_format/rails.rb +5 -5
- data/lib/request_log_analyzer/output.rb +4 -0
- data/lib/request_log_analyzer/output/fancy_html.rb +31 -0
- data/lib/request_log_analyzer/tracker.rb +0 -105
- data/lib/request_log_analyzer/tracker/duration.rb +3 -62
- data/lib/request_log_analyzer/tracker/frequency.rb +18 -5
- data/lib/request_log_analyzer/tracker/numeric_value.rb +223 -0
- data/lib/request_log_analyzer/tracker/traffic.rb +6 -72
- data/request-log-analyzer.gemspec +4 -4
- data/spec/lib/mocks.rb +1 -0
- data/spec/unit/file_format/common_regular_expressions_spec.rb +53 -0
- data/spec/unit/file_format/mysql_format_spec.rb +1 -2
- data/spec/unit/file_format/rails_format_spec.rb +9 -4
- data/spec/unit/tracker/duration_tracker_spec.rb +2 -83
- data/spec/unit/tracker/{count_tracker_spec.rb → numeric_value_tracker_spec.rb} +50 -7
- data/spec/unit/tracker/tracker_api_spec.rb +3 -2
- data/spec/unit/tracker/traffic_tracker_spec.rb +0 -79
- metadata +8 -5
- data/lib/request_log_analyzer/tracker/count.rb +0 -93
data/bin/request-log-analyzer
CHANGED
@@ -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 " --
|
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 " --
|
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
|
91
|
-
puts " request-log-analyzer
|
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:"
|
data/lib/request_log_analyzer.rb
CHANGED
@@ -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.
|
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.
|
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,
|
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|
|
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 "
|
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] = '
|
57
|
-
options[:output] = '
|
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?
|
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 = /^([^\ ]+) ([^\ ]+) \[(
|
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 =>
|
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 =>
|
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: (
|
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\.-]*) \[(
|
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
|
-
|
57
|
-
analyze.
|
58
|
-
|
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
|
@@ -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, :
|
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, :
|
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+::)
|
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
|