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 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
+
@@ -0,0 +1,11 @@
1
+ Rakefile
2
+ README
3
+ LICENSE
4
+ Manifest.txt
5
+ bin/pl_analyze
6
+ lib/production_log/analyzer.rb
7
+ lib/production_log/parser.rb
8
+ lib/production_log/syslog_logger.rb
9
+ test/test.syslog.log
10
+ test/test_analyzer.rb
11
+ test/test_parser.rb
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
+
@@ -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
+
@@ -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
+