production_log_analyzer 1.1.0
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/LICENSE +27 -0
- data/Manifest.txt +11 -0
- data/README +121 -0
- data/Rakefile +62 -0
- data/bin/pl_analyze +50 -0
- data/lib/production_log/analyzer.rb +266 -0
- data/lib/production_log/parser.rb +144 -0
- data/lib/production_log/syslog_logger.rb +121 -0
- data/test/test.syslog.log +255 -0
- data/test/test_analyzer.rb +183 -0
- data/test/test_parser.rb +140 -0
- metadata +48 -0
data/LICENSE
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright 2005 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
data/README
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
Production Log Analyzer
|
2
|
+
|
3
|
+
http://rails-analyzer.rubyforge.org
|
4
|
+
|
5
|
+
http://rubyforge.org/projects/rails-analyzer
|
6
|
+
|
7
|
+
= About
|
8
|
+
|
9
|
+
The Production Analyzer lets you find out which pages on your site are
|
10
|
+
dragging you down. PL Analyze requires the use of SyslogLogger (included)
|
11
|
+
because the default Logger doesn't give any way to associate lines logged to a
|
12
|
+
request.
|
13
|
+
|
14
|
+
SyslogLogger gives you many other advantages, such as the ability to combine
|
15
|
+
logs from multiple machines.
|
16
|
+
|
17
|
+
= Installing
|
18
|
+
|
19
|
+
== Download
|
20
|
+
|
21
|
+
Grab the gem (or tar.gz or .zip) from the RubyForge project:
|
22
|
+
|
23
|
+
http://rubyforge.org/frs/?group_id=586
|
24
|
+
|
25
|
+
== What you need
|
26
|
+
|
27
|
+
Either:
|
28
|
+
|
29
|
+
A syslogd that doesn't suck. This means that +man syslog.conf+ shows a
|
30
|
+
!prog specification. (FreeBSD's syslogd doesn't suck, but OS X's syslogd
|
31
|
+
does.)
|
32
|
+
|
33
|
+
or:
|
34
|
+
|
35
|
+
Some hacking skills to make Rails log the pid of the process for each line
|
36
|
+
logged. You'll also have to teach LogParser#parse about this. Feel free to
|
37
|
+
submit patches with tests. (Patches without tests are useless to me.)
|
38
|
+
|
39
|
+
== What you need to do
|
40
|
+
|
41
|
+
Either:
|
42
|
+
|
43
|
+
Use SyslogLogger according to directions on the SyslogLogger page, including
|
44
|
+
setting up your non-sucky syslogd as directed.
|
45
|
+
|
46
|
+
or:
|
47
|
+
|
48
|
+
Use your hacking skills and set up a logger that your hacked LogParser#parse
|
49
|
+
can deal with.
|
50
|
+
|
51
|
+
Then:
|
52
|
+
|
53
|
+
Set up a cronjob (or something like that) to run log files through pl_analyze.
|
54
|
+
|
55
|
+
= Using pl_analyze
|
56
|
+
|
57
|
+
To run pl_analyze simply give it the name of a log file to analyze.
|
58
|
+
|
59
|
+
pl_analyze /var/log/production.log
|
60
|
+
|
61
|
+
If you want, you can run it from a cron something like this:
|
62
|
+
|
63
|
+
/usr/bin/gzip -dc /var/log/production.log.0.gz | /usr/local/bin/pl_analyze /dev/stdin
|
64
|
+
|
65
|
+
In the future, pl_analyze will be able to read from STDIN.
|
66
|
+
|
67
|
+
= Sample output
|
68
|
+
|
69
|
+
Average Request Time: 0.279874593327209
|
70
|
+
Request Time Std Dev: 0.351590385021209
|
71
|
+
|
72
|
+
Slowest Request Times:
|
73
|
+
ZeitgeistController#goals took 30.889858s
|
74
|
+
ZeitgeistController#goals took 29.657513s
|
75
|
+
EntriesController#save_comment took 20.499292s
|
76
|
+
AccountController#create took 19.539545s
|
77
|
+
EntriesController#save_comment took 15.46844s
|
78
|
+
ZeitgeistController#goals took 14.814086s
|
79
|
+
ZeitgeistController#goals took 13.943129s
|
80
|
+
ZeitgeistController#goals took 13.113908s
|
81
|
+
ZeitgeistController#completed_goals took 12.776777s
|
82
|
+
ZeitgeistController#goals took 12.32529s
|
83
|
+
|
84
|
+
Average DB Time: 0.0649204642242509
|
85
|
+
DB Time Std Dev: 0.214050667483775
|
86
|
+
|
87
|
+
Slowest Total DB Times:
|
88
|
+
ZeitgeistController#goals took 30.797014s
|
89
|
+
ZeitgeistController#goals took 29.567076s
|
90
|
+
ZeitgeistController#goals took 14.709733s
|
91
|
+
ZeitgeistController#goals took 13.84484s
|
92
|
+
ZeitgeistController#goals took 12.968071s
|
93
|
+
ZeitgeistController#completed_goals took 12.400506s
|
94
|
+
ZeitgeistController#goals took 12.241167s
|
95
|
+
ZeitgeistController#goals took 11.561719s
|
96
|
+
ZeitgeistController#goals took 11.445382s
|
97
|
+
ZeitgeistController#goals took 11.085795s
|
98
|
+
|
99
|
+
Average Render Time: 0.128757978789508
|
100
|
+
Render Time Std Dev: 0.131171213785894
|
101
|
+
|
102
|
+
Slowest Total Render Times:
|
103
|
+
TeamsController#progress took 4.698406s
|
104
|
+
TeamsController#progress took 4.679505s
|
105
|
+
PeopleController#doing_same_things took 3.628557s
|
106
|
+
ThingsController#view took 3.34039s
|
107
|
+
ThingsController#view took 2.096405s
|
108
|
+
RssController#goals took 1.759452s
|
109
|
+
EntriesController#view took 1.423261s
|
110
|
+
ThingsController#view took 1.422453s
|
111
|
+
ThingsController#people took 1.377157s
|
112
|
+
PeopleController#view took 1.195831s
|
113
|
+
|
114
|
+
= What's missing
|
115
|
+
|
116
|
+
* More reports
|
117
|
+
* Command line arguments including:
|
118
|
+
* Number of lines to report
|
119
|
+
* What type of log file you've got (if somebody sends patches with tests)
|
120
|
+
* Lots more
|
121
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
require 'rake/contrib/sshpublisher'
|
7
|
+
|
8
|
+
$VERBOSE = nil
|
9
|
+
|
10
|
+
spec = Gem::Specification.new do |s|
|
11
|
+
s.name = "production_log_analyzer"
|
12
|
+
s.version = "1.1.0"
|
13
|
+
s.summary = "Extracts statistics from Rails production logs"
|
14
|
+
s.author = "Eric Hodel"
|
15
|
+
s.email = "hodel@robotcoop.com"
|
16
|
+
|
17
|
+
s.has_rdoc = true
|
18
|
+
s.files = File.read("Manifest.txt").split($/)
|
19
|
+
s.require_path = 'lib'
|
20
|
+
s.executables = ["pl_analyze"]
|
21
|
+
s.default_executable = "pl_analyze"
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "Run tests"
|
25
|
+
task :default => [ :test ]
|
26
|
+
|
27
|
+
Rake::TestTask.new("test") do |t|
|
28
|
+
t.libs << "test"
|
29
|
+
t.pattern = "test/test_*.rb"
|
30
|
+
t.verbose = true
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "Generate RDoc"
|
34
|
+
Rake::RDocTask.new :rdoc do |rd|
|
35
|
+
rd.rdoc_dir = "doc"
|
36
|
+
rd.rdoc_files.add "lib", "README", "LICENSE"
|
37
|
+
rd.main = "README"
|
38
|
+
rd.options << "-d" if `which dot` =~ /\/dot/
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "Build Gem"
|
42
|
+
Rake::GemPackageTask.new spec do |pkg|
|
43
|
+
pkg.need_zip = false
|
44
|
+
pkg.need_tar = false
|
45
|
+
end
|
46
|
+
|
47
|
+
desc "Sends RDoc to RubyForge"
|
48
|
+
task :send_rdoc => [ :rerdoc ] do
|
49
|
+
publisher = Rake::SshDirPublisher.new('drbrain@rubyforge.org',
|
50
|
+
'/var/www/gforge-projects/rails-analyzer',
|
51
|
+
'doc')
|
52
|
+
publisher.upload
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Clean up"
|
56
|
+
task :clean => [ :clobber_rdoc, :clobber_package ]
|
57
|
+
|
58
|
+
desc "Clean up"
|
59
|
+
task :clobber => [ :clean ]
|
60
|
+
|
61
|
+
# vim: ts=4 sts=4 sw=4 syntax=Ruby
|
62
|
+
|
data/bin/pl_analyze
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
|
3
|
+
require 'production_log/analyzer'
|
4
|
+
|
5
|
+
file_name = ARGV.shift
|
6
|
+
|
7
|
+
if file_name.nil? then
|
8
|
+
puts "Usage: #{$0} file_name"
|
9
|
+
exit 1
|
10
|
+
end
|
11
|
+
|
12
|
+
analyzer = Analyzer.new file_name
|
13
|
+
analyzer.process
|
14
|
+
|
15
|
+
count = ARGV.shift
|
16
|
+
count = count.nil? ? 10 : count.to_i
|
17
|
+
|
18
|
+
request_times = analyzer.slowest_request_times(count).map do |time, name|
|
19
|
+
"\t#{name} took #{time}s"
|
20
|
+
end
|
21
|
+
|
22
|
+
puts "Average Request Time: #{analyzer.average_request_time}"
|
23
|
+
puts "Request Time Std Dev: #{analyzer.request_time_std_dev}"
|
24
|
+
puts
|
25
|
+
puts "Slowest Request Times:"
|
26
|
+
puts request_times.join($/)
|
27
|
+
puts
|
28
|
+
|
29
|
+
db_times = analyzer.slowest_db_times(count).map do |time, name|
|
30
|
+
"\t#{name} took #{time}s"
|
31
|
+
end
|
32
|
+
|
33
|
+
puts "Average DB Time: #{analyzer.average_db_time}"
|
34
|
+
puts "DB Time Std Dev: #{analyzer.db_time_std_dev}"
|
35
|
+
puts
|
36
|
+
puts "Slowest Total DB Times:"
|
37
|
+
puts db_times.join($/)
|
38
|
+
puts
|
39
|
+
|
40
|
+
render_times = analyzer.slowest_render_times(count).map do |time, name|
|
41
|
+
"\t#{name} took #{time}s"
|
42
|
+
end
|
43
|
+
|
44
|
+
puts "Average Render Time: #{analyzer.average_render_time}"
|
45
|
+
puts "Render Time Std Dev: #{analyzer.render_time_std_dev}"
|
46
|
+
puts
|
47
|
+
puts "Slowest Total Render Times:"
|
48
|
+
puts render_times.join($/)
|
49
|
+
puts
|
50
|
+
|
@@ -0,0 +1,266 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
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
|
+
def initialize(limit)
|
96
|
+
super limit do |arr, new_item|
|
97
|
+
fastest_time = arr.sort_by { |time, name| time }.first
|
98
|
+
if fastest_time.first < new_item.first then
|
99
|
+
arr.delete_at index(fastest_time)
|
100
|
+
true
|
101
|
+
else
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Calculates statistics for production logs.
|
111
|
+
|
112
|
+
class Analyzer
|
113
|
+
|
114
|
+
##
|
115
|
+
# The logfile being read by the Analyzer.
|
116
|
+
|
117
|
+
attr_reader :logfile_name
|
118
|
+
|
119
|
+
##
|
120
|
+
# An Array of all the request total times for the log file.
|
121
|
+
|
122
|
+
attr_reader :request_times
|
123
|
+
|
124
|
+
##
|
125
|
+
# An Array of all the request database times for the log file.
|
126
|
+
|
127
|
+
attr_reader :db_times
|
128
|
+
|
129
|
+
##
|
130
|
+
# An Array of all the request render times for the log file.
|
131
|
+
|
132
|
+
attr_reader :render_times
|
133
|
+
|
134
|
+
# attr_reader :query_times
|
135
|
+
|
136
|
+
##
|
137
|
+
# Creates a new Analyzer that will read data from +logfile_name+.
|
138
|
+
|
139
|
+
def initialize(logfile_name)
|
140
|
+
@logfile_name = logfile_name
|
141
|
+
@request_times = Hash.new { |h,k| h[k] = [] }
|
142
|
+
@db_times = Hash.new { |h,k| h[k] = [] }
|
143
|
+
@render_times = Hash.new { |h,k| h[k] = [] }
|
144
|
+
# @query_times = Hash.new { |h,k| h[k] = [] }
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Processes the log file collecting statistics from each found LogEntry.
|
149
|
+
|
150
|
+
def process
|
151
|
+
File.open @logfile_name do |fp|
|
152
|
+
LogParser.parse fp do |entry|
|
153
|
+
entry_page = entry.page
|
154
|
+
@request_times[entry_page] << entry.request_time
|
155
|
+
@db_times[entry_page] << entry.db_time
|
156
|
+
@render_times[entry_page] << entry.render_time
|
157
|
+
# entry.queries.each do |name, time, sql|
|
158
|
+
# @query_times[entry_page] << time
|
159
|
+
# end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
##
|
165
|
+
# The average total request time for all requests.
|
166
|
+
|
167
|
+
def average_request_time
|
168
|
+
times = @request_times.values.flatten
|
169
|
+
times.delete 0
|
170
|
+
return times.average
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# The standard deviation of the total request time for all requests.
|
175
|
+
|
176
|
+
def request_time_std_dev
|
177
|
+
times = @request_times.values.flatten
|
178
|
+
times.delete 0
|
179
|
+
return times.standard_deviation
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# The +limit+ slowest total request times.
|
184
|
+
|
185
|
+
def slowest_request_times(limit = 10)
|
186
|
+
slowest_times = SlowestTimes.new limit
|
187
|
+
|
188
|
+
@request_times.each do |name, times|
|
189
|
+
times.each do |time|
|
190
|
+
slowest_times << [time, name]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
return slowest_times.sort_by { |time, name| time }.reverse
|
195
|
+
end
|
196
|
+
|
197
|
+
##
|
198
|
+
# The average total database time for all requests.
|
199
|
+
|
200
|
+
def average_db_time
|
201
|
+
times = @db_times.values.flatten
|
202
|
+
times.delete 0
|
203
|
+
return times.average
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# The standard deviation of the total database time for all requests.
|
208
|
+
|
209
|
+
def db_time_std_dev
|
210
|
+
times = @db_times.values.flatten
|
211
|
+
times.delete 0
|
212
|
+
return times.standard_deviation
|
213
|
+
end
|
214
|
+
|
215
|
+
##
|
216
|
+
# The +limit+ slowest total database times.
|
217
|
+
|
218
|
+
def slowest_db_times(limit = 10)
|
219
|
+
slowest_times = SlowestTimes.new limit
|
220
|
+
|
221
|
+
@db_times.each do |name, times|
|
222
|
+
times.each do |time|
|
223
|
+
slowest_times << [time, name]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
return slowest_times.sort_by { |time, name| time }.reverse
|
228
|
+
end
|
229
|
+
|
230
|
+
##
|
231
|
+
# The average total render time for all requests.
|
232
|
+
|
233
|
+
def average_render_time
|
234
|
+
times = @render_times.values.flatten
|
235
|
+
times.delete 0
|
236
|
+
return times.average
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# The standard deviation of the total render time for all requests.
|
241
|
+
|
242
|
+
def render_time_std_dev
|
243
|
+
times = @render_times.values.flatten
|
244
|
+
times.delete 0
|
245
|
+
return times.standard_deviation
|
246
|
+
end
|
247
|
+
|
248
|
+
##
|
249
|
+
# The +limit+ slowest total render times for all requests.
|
250
|
+
|
251
|
+
def slowest_render_times(limit = 10)
|
252
|
+
slowest_times = SlowestTimes.new limit
|
253
|
+
|
254
|
+
@render_times.each do |name, times|
|
255
|
+
times.each do |time|
|
256
|
+
slowest_times << [time, name]
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
return slowest_times.sort_by { |time, name| time }.reverse
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
|
265
|
+
# vim: ts=4 sts=4 sw=4
|
266
|
+
|