production_log_analyzer 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|