erkki-production_log_analyzer 2009022401
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/History.txt +34 -0
- data/LICENSE.txt +27 -0
- data/Manifest.txt +18 -0
- data/README.txt +147 -0
- data/Rakefile +17 -0
- data/bin/action_errors +46 -0
- data/bin/action_grep +19 -0
- data/bin/pl_analyze +36 -0
- data/lib/production_log/action_grep.rb +42 -0
- data/lib/production_log/analyzer.rb +406 -0
- data/lib/production_log/parser.rb +178 -0
- data/test/test.syslog.0.14.x.log +5 -0
- data/test/test.syslog.1.2.shortname.log +5 -0
- data/test/test.syslog.empty.log +2 -0
- data/test/test.syslog.log +258 -0
- data/test/test_action_grep.rb +66 -0
- data/test/test_analyzer.rb +418 -0
- data/test/test_parser.rb +297 -0
- metadata +97 -0
data/History.txt
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
= 1.5.0
|
2
|
+
|
3
|
+
* Fixed empty log bug. Patch by Tim Lucas.
|
4
|
+
* Fixed bug where sometimes lines would be logged before the
|
5
|
+
Processing line. Patch by Geoff Grosenbach.
|
6
|
+
|
7
|
+
= 1.4.0
|
8
|
+
|
9
|
+
* Switched to Hoe
|
10
|
+
* Allowed action_errors to suppress routing errors with > 3 occurances
|
11
|
+
* action_grep now works correctly with components
|
12
|
+
* pl_analyze now works correctly with components
|
13
|
+
* Added action_errors to extract error counts from logs
|
14
|
+
* Retabbed to match the rest of the world
|
15
|
+
|
16
|
+
= 1.3.0
|
17
|
+
|
18
|
+
* Added action_grep
|
19
|
+
* Added support for newer log format
|
20
|
+
|
21
|
+
= 1.2.0
|
22
|
+
|
23
|
+
* pl_analyze calculates per-action statistics
|
24
|
+
* pl_analyze can send an email with its output
|
25
|
+
|
26
|
+
= 1.1.0
|
27
|
+
|
28
|
+
* RDoc
|
29
|
+
* Other various fixes lost to time.
|
30
|
+
|
31
|
+
= 1.0.0
|
32
|
+
|
33
|
+
* Birthday!
|
34
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright 2005, 2007 Eric Hodel, The Robot Co-op. All rights reserved.
|
2
|
+
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
4
|
+
modification, are permitted provided that the following conditions
|
5
|
+
are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
notice, this list of conditions and the following disclaimer in the
|
11
|
+
documentation and/or other materials provided with the distribution.
|
12
|
+
3. Neither the names of the authors nor the names of their contributors
|
13
|
+
may be used to endorse or promote products derived from this software
|
14
|
+
without specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
17
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
19
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
|
20
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
21
|
+
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
22
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
23
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
24
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
25
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
26
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
27
|
+
|
data/Manifest.txt
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
History.txt
|
2
|
+
LICENSE.txt
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
bin/action_errors
|
7
|
+
bin/action_grep
|
8
|
+
bin/pl_analyze
|
9
|
+
lib/production_log/action_grep.rb
|
10
|
+
lib/production_log/analyzer.rb
|
11
|
+
lib/production_log/parser.rb
|
12
|
+
test/test.syslog.0.14.x.log
|
13
|
+
test/test.syslog.1.2.shortname.log
|
14
|
+
test/test.syslog.empty.log
|
15
|
+
test/test.syslog.log
|
16
|
+
test/test_action_grep.rb
|
17
|
+
test/test_analyzer.rb
|
18
|
+
test/test_parser.rb
|
data/README.txt
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
= production_log_analyzer
|
2
|
+
|
3
|
+
production_log_analyzer lets you find out which actions on a Rails
|
4
|
+
site are slowing you down.
|
5
|
+
|
6
|
+
http://seattlerb.rubyforge.org/production_log_analyzer
|
7
|
+
|
8
|
+
http://rubyforge.org/projects/seattlerb
|
9
|
+
|
10
|
+
Bug reports:
|
11
|
+
|
12
|
+
http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921
|
13
|
+
|
14
|
+
== About
|
15
|
+
|
16
|
+
production_log_analyzer provides three tools to analyze log files
|
17
|
+
created by SyslogLogger. pl_analyze for getting daily reports,
|
18
|
+
action_grep for pulling log lines for a single action and
|
19
|
+
action_errors to summarize errors with counts.
|
20
|
+
|
21
|
+
The analyzer currently requires the use of SyslogLogger because the
|
22
|
+
default Logger doesn't give any way to associate lines logged to a
|
23
|
+
request.
|
24
|
+
|
25
|
+
The PL Analyzer also includes action_grep which lets you grab lines from a log
|
26
|
+
that only match a single action.
|
27
|
+
|
28
|
+
action_grep RssController#uber /var/log/production.log
|
29
|
+
|
30
|
+
== Installing
|
31
|
+
|
32
|
+
sudo gem install production_log_analyzer
|
33
|
+
|
34
|
+
=== Setup
|
35
|
+
|
36
|
+
First:
|
37
|
+
|
38
|
+
Set up SyslogLogger according to the instructions here:
|
39
|
+
|
40
|
+
http://seattlerb.rubyforge.org/SyslogLogger/
|
41
|
+
|
42
|
+
Then:
|
43
|
+
|
44
|
+
Set up a cronjob (or something like that) to run log files through pl_analyze.
|
45
|
+
|
46
|
+
== Using pl_analyze
|
47
|
+
|
48
|
+
To run pl_analyze simply give it the name of a log file to analyze.
|
49
|
+
|
50
|
+
pl_analyze /var/log/production.log
|
51
|
+
|
52
|
+
If you want, you can run it from a cron something like this:
|
53
|
+
|
54
|
+
/usr/bin/gzip -dc /var/log/production.log.0.gz | /usr/local/bin/pl_analyze /dev/stdin
|
55
|
+
|
56
|
+
Or, have pl_analyze email you (which is preferred, because tabs get preserved):
|
57
|
+
|
58
|
+
/usr/bin/gzip -dc /var/log/production.log.0.gz | /usr/local/bin/pl_analyze /dev/stdin -e devnull@robotcoop.com -s "pl_analyze for `date -v-1d "+%D"`"
|
59
|
+
|
60
|
+
In the future, pl_analyze will be able to read from STDIN.
|
61
|
+
|
62
|
+
== Sample output
|
63
|
+
|
64
|
+
Request Times Summary: Count Avg Std Dev Min Max
|
65
|
+
ALL REQUESTS: 11 0.576 0.508 0.000 1.470
|
66
|
+
|
67
|
+
ThingsController#view: 3 0.716 0.387 0.396 1.260
|
68
|
+
TeamsController#progress: 2 0.841 0.629 0.212 1.470
|
69
|
+
RssController#uber: 2 0.035 0.000 0.035 0.035
|
70
|
+
PeopleController#progress: 2 0.489 0.489 0.000 0.977
|
71
|
+
PeopleController#view: 2 0.731 0.371 0.360 1.102
|
72
|
+
|
73
|
+
Average Request Time: 0.634
|
74
|
+
Request Time Std Dev: 0.498
|
75
|
+
|
76
|
+
Slowest Request Times:
|
77
|
+
TeamsController#progress took 1.470s
|
78
|
+
ThingsController#view took 1.260s
|
79
|
+
PeopleController#view took 1.102s
|
80
|
+
PeopleController#progress took 0.977s
|
81
|
+
ThingsController#view took 0.492s
|
82
|
+
ThingsController#view took 0.396s
|
83
|
+
PeopleController#view took 0.360s
|
84
|
+
TeamsController#progress took 0.212s
|
85
|
+
RssController#uber took 0.035s
|
86
|
+
RssController#uber took 0.035s
|
87
|
+
|
88
|
+
------------------------------------------------------------------------
|
89
|
+
|
90
|
+
DB Times Summary: Count Avg Std Dev Min Max
|
91
|
+
ALL REQUESTS: 11 0.366 0.393 0.000 1.144
|
92
|
+
|
93
|
+
ThingsController#view: 3 0.403 0.362 0.122 0.914
|
94
|
+
TeamsController#progress: 2 0.646 0.497 0.149 1.144
|
95
|
+
RssController#uber: 2 0.008 0.000 0.008 0.008
|
96
|
+
PeopleController#progress: 2 0.415 0.415 0.000 0.830
|
97
|
+
PeopleController#view: 2 0.338 0.149 0.189 0.486
|
98
|
+
|
99
|
+
Average DB Time: 0.402
|
100
|
+
DB Time Std Dev: 0.394
|
101
|
+
|
102
|
+
Slowest Total DB Times:
|
103
|
+
TeamsController#progress took 1.144s
|
104
|
+
ThingsController#view took 0.914s
|
105
|
+
PeopleController#progress took 0.830s
|
106
|
+
PeopleController#view took 0.486s
|
107
|
+
PeopleController#view took 0.189s
|
108
|
+
ThingsController#view took 0.173s
|
109
|
+
TeamsController#progress took 0.149s
|
110
|
+
ThingsController#view took 0.122s
|
111
|
+
RssController#uber took 0.008s
|
112
|
+
RssController#uber took 0.008s
|
113
|
+
|
114
|
+
------------------------------------------------------------------------
|
115
|
+
|
116
|
+
Render Times Summary: Count Avg Std Dev Min Max
|
117
|
+
ALL REQUESTS: 11 0.219 0.253 0.000 0.695
|
118
|
+
|
119
|
+
ThingsController#view: 3 0.270 0.171 0.108 0.506
|
120
|
+
TeamsController#progress: 2 0.000 0.000 0.000 0.000
|
121
|
+
RssController#uber: 2 0.012 0.000 0.012 0.012
|
122
|
+
PeopleController#progress: 2 0.302 0.302 0.000 0.604
|
123
|
+
PeopleController#view: 2 0.487 0.209 0.278 0.695
|
124
|
+
|
125
|
+
Average Render Time: 0.302
|
126
|
+
Render Time Std Dev: 0.251
|
127
|
+
|
128
|
+
Slowest Total Render Times:
|
129
|
+
PeopleController#view took 0.695s
|
130
|
+
PeopleController#progress took 0.604s
|
131
|
+
ThingsController#view took 0.506s
|
132
|
+
PeopleController#view took 0.278s
|
133
|
+
ThingsController#view took 0.197s
|
134
|
+
ThingsController#view took 0.108s
|
135
|
+
RssController#uber took 0.012s
|
136
|
+
RssController#uber took 0.012s
|
137
|
+
TeamsController#progress took 0.000s
|
138
|
+
TeamsController#progress took 0.000s
|
139
|
+
|
140
|
+
== What's missing
|
141
|
+
|
142
|
+
* More reports
|
143
|
+
* Command line arguments including:
|
144
|
+
* Help
|
145
|
+
* What type of log file you've got (if somebody sends patches with tests)
|
146
|
+
* Read from STDIN
|
147
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'hoe'
|
2
|
+
|
3
|
+
$:.unshift './lib'
|
4
|
+
require 'production_log/analyzer'
|
5
|
+
|
6
|
+
Hoe.new 'production_log_analyzer', '2009022401' do |p|
|
7
|
+
p.summary = p.paragraphs_of('README.txt', 1).join ' '
|
8
|
+
p.description = p.paragraphs_of('README.txt', 7).join ' '
|
9
|
+
p.author = 'Eric Hodel'
|
10
|
+
p.email = 'drbrain@segment7.net'
|
11
|
+
p.url = p.paragraphs_of('README.txt', 2).join ' '
|
12
|
+
|
13
|
+
p.rubyforge_name = 'seattlerb'
|
14
|
+
|
15
|
+
p.extra_deps << ['rails_analyzer_tools', '>= 1.4.0']
|
16
|
+
end
|
17
|
+
|
data/bin/action_errors
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/local/bin/ruby -ws
|
2
|
+
|
3
|
+
$h ||= false
|
4
|
+
$r ||= false
|
5
|
+
$o ||= false
|
6
|
+
|
7
|
+
$r = $r ? ($r.to_i rescue false) : false
|
8
|
+
|
9
|
+
if $h then
|
10
|
+
$stderr.puts "Usage: #{$0} [-r=N] LOGFILE"
|
11
|
+
$stderr.puts "\t-r=N\tShow routing errors with N or more occurances"
|
12
|
+
$stderr.puts "\t-o\tShow errors with one occurance"
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
errors = {}
|
17
|
+
counts = Hash.new 0
|
18
|
+
|
19
|
+
ARGF.each_line do |line|
|
20
|
+
line =~ /\]: (.*?) (.*)/
|
21
|
+
next if $1.nil?
|
22
|
+
msg = $1
|
23
|
+
trace = $2
|
24
|
+
key = msg.gsub(/\d/, '#')
|
25
|
+
counts[key] += 1
|
26
|
+
next if counts[key] > 1
|
27
|
+
trace = trace.split(' ')[0..-2].map { |l| l.strip }.join("\n\t")
|
28
|
+
error = "#{msg}\n\t#{trace}"
|
29
|
+
errors[key] = error
|
30
|
+
end
|
31
|
+
|
32
|
+
counts.sort_by { |_,c| -c }.each do |key, count|
|
33
|
+
next if count == 1 and not $o
|
34
|
+
error = errors[key]
|
35
|
+
|
36
|
+
if error =~ /^ActionController::RoutingError/ then
|
37
|
+
next unless $r
|
38
|
+
next if $r and count < $r
|
39
|
+
end
|
40
|
+
|
41
|
+
puts "count: #{count}"
|
42
|
+
puts "{{{"
|
43
|
+
puts error
|
44
|
+
puts "}}}"
|
45
|
+
end
|
46
|
+
|
data/bin/action_grep
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
|
3
|
+
require 'production_log/action_grep'
|
4
|
+
|
5
|
+
action_name = ARGV.shift
|
6
|
+
file_name = ARGV.shift
|
7
|
+
|
8
|
+
if action_name.nil? or file_name.nil? then
|
9
|
+
puts "Usage: #{$0} action_name file_name"
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
ActionGrep.grep action_name, file_name
|
15
|
+
rescue ArgumentError => e
|
16
|
+
puts e
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
data/bin/pl_analyze
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/bin/env ruby -w
|
2
|
+
|
3
|
+
$:.unshift "#{File.dirname(__FILE__)}/../lib/"
|
4
|
+
require "production_log/analyzer"
|
5
|
+
|
6
|
+
file_name = ARGV.shift
|
7
|
+
|
8
|
+
if file_name.nil? then
|
9
|
+
puts "Usage: #{$0} file_name [-e email_recipient [-s subject]] [count]"
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
email_recipient = nil
|
14
|
+
subject = nil
|
15
|
+
|
16
|
+
if ARGV.first == '-e' then
|
17
|
+
ARGV.shift # -e
|
18
|
+
email_recipient = ARGV.shift
|
19
|
+
end
|
20
|
+
|
21
|
+
if email_recipient and ARGV.first == '-s' then
|
22
|
+
ARGV.shift # -s
|
23
|
+
subject = ARGV.shift
|
24
|
+
end
|
25
|
+
|
26
|
+
count = ARGV.shift
|
27
|
+
count = count.nil? ? 10 : Integer(count)
|
28
|
+
|
29
|
+
if email_recipient.nil? then
|
30
|
+
analyzer = Analyzer.new file_name
|
31
|
+
analyzer.process
|
32
|
+
puts analyzer.report(count)
|
33
|
+
else
|
34
|
+
Analyzer.email file_name, email_recipient, subject, count
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActionGrep; end
|
2
|
+
|
3
|
+
class << ActionGrep
|
4
|
+
|
5
|
+
def grep(action_name, file_name)
|
6
|
+
unless action_name =~ /\A([A-Z][A-Za-z\d]*)(?:#([A-Za-z]\w*))?\Z/ then
|
7
|
+
raise ArgumentError, "Invalid action name #{action_name} expected something like SomeController#action"
|
8
|
+
end
|
9
|
+
|
10
|
+
unless File.file? file_name and File.readable? file_name then
|
11
|
+
raise ArgumentError, "Unable to read #{file_name}"
|
12
|
+
end
|
13
|
+
|
14
|
+
buckets = Hash.new { |h,k| h[k] = [] }
|
15
|
+
comp_count = Hash.new 0
|
16
|
+
|
17
|
+
File.open file_name do |fp|
|
18
|
+
fp.each_line do |line|
|
19
|
+
line =~ / ([^ ]+) ([^ ]+)\[(\d+)\]: (.*)/
|
20
|
+
next if $2.nil? or $2 == 'newsyslog'
|
21
|
+
bucket = [$1, $2, $3].join '-'
|
22
|
+
data = $4
|
23
|
+
|
24
|
+
buckets[bucket] << line
|
25
|
+
|
26
|
+
case data
|
27
|
+
when /^Start rendering component / then
|
28
|
+
comp_count[bucket] += 1
|
29
|
+
when /^End of component rendering$/ then
|
30
|
+
comp_count[bucket] -= 1
|
31
|
+
when /^Completed/ then
|
32
|
+
next unless comp_count[bucket] == 0
|
33
|
+
action = buckets.delete bucket
|
34
|
+
next unless action.any? { |l| l =~ /: Processing #{action_name}/ }
|
35
|
+
puts action.join
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,406 @@
|
|
1
|
+
$TESTING = false unless defined? $TESTING
|
2
|
+
|
3
|
+
require 'production_log/parser'
|
4
|
+
|
5
|
+
module Enumerable
|
6
|
+
|
7
|
+
##
|
8
|
+
# Sum of all the elements of the Enumerable
|
9
|
+
|
10
|
+
def sum
|
11
|
+
return self.inject(0) { |acc, i| acc + i }
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Average of all the elements of the Enumerable
|
16
|
+
#
|
17
|
+
# The Enumerable must respond to #length
|
18
|
+
|
19
|
+
def average
|
20
|
+
return self.sum / self.length.to_f
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Sample variance of all the elements of the Enumerable
|
25
|
+
#
|
26
|
+
# The Enumerable must respond to #length
|
27
|
+
|
28
|
+
def sample_variance
|
29
|
+
avg = self.average
|
30
|
+
sum = self.inject(0) { |acc, i| acc + (i - avg) ** 2 }
|
31
|
+
return (1 / self.length.to_f * sum)
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Standard deviation of all the elements of the Enumerable
|
36
|
+
#
|
37
|
+
# The Enumerable must respond to #length
|
38
|
+
|
39
|
+
def standard_deviation
|
40
|
+
return Math.sqrt(self.sample_variance)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# A list that only stores +limit+ items.
|
47
|
+
|
48
|
+
class SizedList < Array
|
49
|
+
|
50
|
+
##
|
51
|
+
# Creates a new SizedList that can hold up to +limit+ items. Whenever
|
52
|
+
# adding a new item to the SizedList would make the list larger than
|
53
|
+
# +limit+, +delete_block+ is called.
|
54
|
+
#
|
55
|
+
# +delete_block+ is passed the list and the item being added.
|
56
|
+
# +delete_block+ must take action to remove an item and return true or
|
57
|
+
# return false if the item should not be added to the list.
|
58
|
+
|
59
|
+
def initialize(limit, &delete_block)
|
60
|
+
@limit = limit
|
61
|
+
@delete_block = delete_block
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Attempts to add +obj+ to the list.
|
66
|
+
|
67
|
+
def <<(obj)
|
68
|
+
return super if self.length < @limit
|
69
|
+
return super if @delete_block.call self, obj
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Stores +limit+ time/object pairs, keeping only the largest +limit+ items.
|
76
|
+
#
|
77
|
+
# Sample usage:
|
78
|
+
#
|
79
|
+
# l = SlowestTimes.new 5
|
80
|
+
#
|
81
|
+
# l << [Time.now, 'one']
|
82
|
+
# l << [Time.now, 'two']
|
83
|
+
# l << [Time.now, 'three']
|
84
|
+
# l << [Time.now, 'four']
|
85
|
+
# l << [Time.now, 'five']
|
86
|
+
# l << [Time.now, 'six']
|
87
|
+
#
|
88
|
+
# p l.map { |i| i.last }
|
89
|
+
|
90
|
+
class SlowestTimes < SizedList
|
91
|
+
|
92
|
+
##
|
93
|
+
# Creates a new SlowestTimes SizedList that holds only +limit+ time/object
|
94
|
+
# pairs.
|
95
|
+
|
96
|
+
def initialize(limit)
|
97
|
+
super limit do |arr, new_item|
|
98
|
+
fastest_time = arr.sort_by { |time, name| time }.first
|
99
|
+
if fastest_time.first < new_item.first then
|
100
|
+
arr.delete_at index(fastest_time)
|
101
|
+
true
|
102
|
+
else
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Calculates statistics for production logs.
|
112
|
+
|
113
|
+
class Analyzer
|
114
|
+
|
115
|
+
##
|
116
|
+
# The version of the production log analyzer you are using.
|
117
|
+
|
118
|
+
VERSION = '1.5.0'
|
119
|
+
|
120
|
+
##
|
121
|
+
# The logfile being read by the Analyzer.
|
122
|
+
|
123
|
+
attr_reader :logfile_name
|
124
|
+
|
125
|
+
##
|
126
|
+
# An Array of all the request total times for the log file.
|
127
|
+
|
128
|
+
attr_reader :request_times
|
129
|
+
|
130
|
+
##
|
131
|
+
# An Array of all the request database times for the log file.
|
132
|
+
|
133
|
+
attr_reader :db_times
|
134
|
+
|
135
|
+
##
|
136
|
+
# An Array of all the request render times for the log file.
|
137
|
+
|
138
|
+
attr_reader :render_times
|
139
|
+
|
140
|
+
attr_reader :row_counts, :query_counts
|
141
|
+
|
142
|
+
##
|
143
|
+
# Generates and sends an email report with lots of fun stuff in it. This
|
144
|
+
# way, Mail.app will behave when given tabs.
|
145
|
+
|
146
|
+
def self.email(file_name, recipient, subject, count = 10)
|
147
|
+
analyzer = self.new file_name
|
148
|
+
analyzer.process
|
149
|
+
body = analyzer.report count
|
150
|
+
|
151
|
+
email = self.envelope(recipient, subject)
|
152
|
+
email << nil
|
153
|
+
email << "<pre>#{body}</pre>"
|
154
|
+
email = email.join($/) << $/
|
155
|
+
|
156
|
+
return email if $TESTING
|
157
|
+
|
158
|
+
IO.popen("/usr/sbin/sendmail -i -t", "w+") do |sm|
|
159
|
+
sm.print email
|
160
|
+
sm.flush
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.envelope(recipient, subject = nil) # :nodoc:
|
165
|
+
envelope = {}
|
166
|
+
envelope['To'] = recipient
|
167
|
+
envelope['Subject'] = subject || "pl_analyze"
|
168
|
+
envelope['Content-Type'] = "text/html"
|
169
|
+
|
170
|
+
return envelope.map { |(k,v)| "#{k}: #{v}" }
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Creates a new Analyzer that will read data from +logfile_name+.
|
175
|
+
|
176
|
+
def initialize(logfile_name)
|
177
|
+
@logfile_name = logfile_name
|
178
|
+
@request_times = Hash.new { |h,k| h[k] = [] }
|
179
|
+
@db_times = Hash.new { |h,k| h[k] = [] }
|
180
|
+
@row_counts = Hash.new { |h,k| h[k] = [] }
|
181
|
+
@query_counts = Hash.new { |h,k| h[k] = [] }
|
182
|
+
@render_times = Hash.new { |h,k| h[k] = [] }
|
183
|
+
end
|
184
|
+
|
185
|
+
##
|
186
|
+
# Processes the log file collecting statistics from each found LogEntry.
|
187
|
+
|
188
|
+
def process
|
189
|
+
File.open @logfile_name do |fp|
|
190
|
+
LogParser.parse fp do |entry|
|
191
|
+
entry_page = entry.page
|
192
|
+
next if entry_page.nil?
|
193
|
+
@request_times[entry_page] << entry.request_time
|
194
|
+
@db_times[entry_page] << entry.db_time
|
195
|
+
@row_counts[entry_page] << entry.row_count
|
196
|
+
@query_counts[entry_page] << entry.query_count
|
197
|
+
@render_times[entry_page] << entry.render_time
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# The average total request time for all requests.
|
204
|
+
|
205
|
+
def average_request_time
|
206
|
+
return time_average(@request_times)
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# The standard deviation of the total request time for all requests.
|
211
|
+
|
212
|
+
def request_time_std_dev
|
213
|
+
return time_std_dev(@request_times)
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# The +limit+ slowest total request times.
|
218
|
+
|
219
|
+
def slowest_request_times(limit = 10)
|
220
|
+
return slowest_times(@request_times, limit)
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# The average total database time for all requests.
|
225
|
+
|
226
|
+
def average_db_time
|
227
|
+
return time_average(@db_times)
|
228
|
+
end
|
229
|
+
|
230
|
+
##
|
231
|
+
# The standard deviation of the total database time for all requests.
|
232
|
+
|
233
|
+
def db_time_std_dev
|
234
|
+
return time_std_dev(@db_times)
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# The +limit+ slowest total database times.
|
239
|
+
|
240
|
+
def slowest_db_times(limit = 10)
|
241
|
+
return slowest_times(@db_times, limit)
|
242
|
+
end
|
243
|
+
|
244
|
+
##
|
245
|
+
# The average total render time for all requests.
|
246
|
+
|
247
|
+
def average_render_time
|
248
|
+
return time_average(@render_times)
|
249
|
+
end
|
250
|
+
|
251
|
+
##
|
252
|
+
# The standard deviation of the total render time for all requests.
|
253
|
+
|
254
|
+
def render_time_std_dev
|
255
|
+
return time_std_dev(@render_times)
|
256
|
+
end
|
257
|
+
|
258
|
+
##
|
259
|
+
# The +limit+ slowest total render times for all requests.
|
260
|
+
|
261
|
+
def slowest_render_times(limit = 10)
|
262
|
+
return slowest_times(@render_times, limit)
|
263
|
+
end
|
264
|
+
|
265
|
+
##
|
266
|
+
# A list of count/min/max/avg/std dev for request times.
|
267
|
+
|
268
|
+
def request_times_summary
|
269
|
+
return summarize("Request Times", @request_times)
|
270
|
+
end
|
271
|
+
|
272
|
+
##
|
273
|
+
# A list of count/min/max/avg/std dev for database times.
|
274
|
+
|
275
|
+
def db_times_summary
|
276
|
+
return summarize("DB Times", @db_times)
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# A list of count/min/max/avg/std dev for request times.
|
281
|
+
|
282
|
+
def render_times_summary
|
283
|
+
return summarize("Render Times", @render_times)
|
284
|
+
end
|
285
|
+
|
286
|
+
##
|
287
|
+
# Builds a report containing +count+ slow items.
|
288
|
+
|
289
|
+
def report(count)
|
290
|
+
return "No requests to analyze" if request_times.empty?
|
291
|
+
|
292
|
+
text = []
|
293
|
+
|
294
|
+
text << request_times_summary
|
295
|
+
text << nil
|
296
|
+
text << "Slowest Request Times:"
|
297
|
+
slowest_request_times(count).each do |time, name|
|
298
|
+
text << "\t#{name} took #{'%0.3f' % time}s"
|
299
|
+
end
|
300
|
+
text << nil
|
301
|
+
text << "-" * 72
|
302
|
+
text << nil
|
303
|
+
|
304
|
+
text << db_times_summary
|
305
|
+
text << nil
|
306
|
+
text << "Slowest Total DB Times:"
|
307
|
+
slowest_db_times(count).each do |time, name|
|
308
|
+
text << "\t#{name} took #{'%0.3f' % time}s"
|
309
|
+
end
|
310
|
+
text << nil
|
311
|
+
text << "-" * 72
|
312
|
+
text << nil
|
313
|
+
|
314
|
+
text << render_times_summary
|
315
|
+
text << nil
|
316
|
+
text << "Slowest Total Render Times:"
|
317
|
+
slowest_render_times(count).each do |time, name|
|
318
|
+
text << "\t#{name} took #{'%0.3f' % time}s"
|
319
|
+
end
|
320
|
+
text << nil
|
321
|
+
|
322
|
+
return text.join($/)
|
323
|
+
end
|
324
|
+
|
325
|
+
private unless $TESTING
|
326
|
+
|
327
|
+
def summarize(title, records) # :nodoc:
|
328
|
+
record = nil
|
329
|
+
list = []
|
330
|
+
|
331
|
+
# header
|
332
|
+
record = [pad_request_name("#{title} Summary"), 'Count', 'Avg', 'Std Dev',
|
333
|
+
'Min', 'Max', 'Queries', 'Rows']
|
334
|
+
list << record.join("\t")
|
335
|
+
|
336
|
+
# all requests
|
337
|
+
times = records.values.flatten
|
338
|
+
record = [times.average, times.standard_deviation, times.min, times.max, 0, 0]
|
339
|
+
record.map! { |v| "%0.3f" % v }
|
340
|
+
record.unshift [pad_request_name('ALL REQUESTS'), times.size]
|
341
|
+
list << record.join("\t")
|
342
|
+
|
343
|
+
# spacer
|
344
|
+
list << nil
|
345
|
+
|
346
|
+
records.sort_by { |k,v| v.size}.reverse_each do |req, times|
|
347
|
+
if "DB Times" == title
|
348
|
+
average_rows = @row_counts[req].average
|
349
|
+
average_queries = @query_counts[req].average
|
350
|
+
end
|
351
|
+
record = [times.average, times.standard_deviation, times.min, times.max, average_queries || 0, average_rows || 0]
|
352
|
+
record.map! { |v| "%0.3f" % v }
|
353
|
+
record.unshift ["#{pad_request_name req}", times.size]
|
354
|
+
list << record.join("\t")
|
355
|
+
end
|
356
|
+
|
357
|
+
return list.join("\n")
|
358
|
+
end
|
359
|
+
|
360
|
+
def slowest_times(records, limit) # :nodoc:
|
361
|
+
slowest_times = SlowestTimes.new limit
|
362
|
+
|
363
|
+
records.each do |name, times|
|
364
|
+
times.each do |time|
|
365
|
+
slowest_times << [time, name]
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
return slowest_times.sort_by { |time, name| time }.reverse
|
370
|
+
end
|
371
|
+
|
372
|
+
def time_average(records) # :nodoc:
|
373
|
+
times = records.values.flatten
|
374
|
+
times.delete 0
|
375
|
+
return times.average
|
376
|
+
end
|
377
|
+
|
378
|
+
def time_std_dev(records) # :nodoc:
|
379
|
+
times = records.values.flatten
|
380
|
+
times.delete 0
|
381
|
+
return times.standard_deviation
|
382
|
+
end
|
383
|
+
|
384
|
+
def longest_request_name # :nodoc:
|
385
|
+
return @longest_req if defined? @longest_req
|
386
|
+
|
387
|
+
names = @request_times.keys.map do |name|
|
388
|
+
(name||'Unknown').length + 1 # + : - HACK where does nil come from?
|
389
|
+
end
|
390
|
+
|
391
|
+
@longest_req = names.max
|
392
|
+
|
393
|
+
@longest_req = 'Unknown'.length + 1 if @longest_req.nil?
|
394
|
+
|
395
|
+
return @longest_req
|
396
|
+
end
|
397
|
+
|
398
|
+
def pad_request_name(name) # :nodoc:
|
399
|
+
name = (name||'Unknown') + ':' # HACK where does nil come from?
|
400
|
+
padding_width = longest_request_name - name.length
|
401
|
+
padding_width = 0 if padding_width < 0
|
402
|
+
name += (' ' * padding_width)
|
403
|
+
end
|
404
|
+
|
405
|
+
end
|
406
|
+
|