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 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
+