brontes3d-production_log_analyzer 2009022403

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/History.txt +34 -0
  2. data/LICENSE.txt +27 -0
  3. data/Manifest.txt +18 -0
  4. data/README.txt +147 -0
  5. data/Rakefile +17 -0
  6. data/bin/action_errors +46 -0
  7. data/bin/action_grep +19 -0
  8. data/bin/pl_analyze +36 -0
  9. data/lib/passenger_log_per_proc.rb +55 -0
  10. data/lib/production_log/action_grep.rb +41 -0
  11. data/lib/production_log/analyzer.rb +416 -0
  12. data/lib/production_log/parser.rb +228 -0
  13. data/test/test_action_grep.rb +72 -0
  14. data/test/test_analyzer.rb +425 -0
  15. data/test/test_helper.rb +68 -0
  16. data/test/test_parser.rb +420 -0
  17. data/test/test_passenger_log_per_proc.rb +88 -0
  18. data/test/test_syslogs/test.syslog.0.14.x.log +4 -0
  19. data/test/test_syslogs/test.syslog.1.2.shortname.log +4 -0
  20. data/test/test_syslogs/test.syslog.empty.log +0 -0
  21. data/test/test_syslogs/test.syslog.log +256 -0
  22. data/test/test_vanilla/test.0.14.x.log +4 -0
  23. data/test/test_vanilla/test.1.2.shortname.log +4 -0
  24. data/test/test_vanilla/test.empty.log +0 -0
  25. data/test/test_vanilla/test.log +255 -0
  26. data/test/test_vanilla/test_log_parts/1_online1-rails-59600.log +7 -0
  27. data/test/test_vanilla/test_log_parts/2_online2-rails-59628.log +11 -0
  28. data/test/test_vanilla/test_log_parts/3_online1-rails-59628.log +9 -0
  29. data/test/test_vanilla/test_log_parts/4_online1-rails-59645.log +30 -0
  30. data/test/test_vanilla/test_log_parts/5_online1-rails-59629.log +38 -0
  31. data/test/test_vanilla/test_log_parts/6_online1-rails-60654.log +32 -0
  32. data/test/test_vanilla/test_log_parts/7_online1-rails-59627.log +70 -0
  33. data/test/test_vanilla/test_log_parts/8_online1-rails-59635.log +58 -0
  34. metadata +113 -0
@@ -0,0 +1,416 @@
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
+ ##
141
+ # Generates and sends an email report with lots of fun stuff in it. This
142
+ # way, Mail.app will behave when given tabs.
143
+
144
+ def self.email(file_name, recipient, subject, count = 10)
145
+ analyzer = self.new file_name
146
+ analyzer.process
147
+ body = analyzer.report count
148
+
149
+ email = self.envelope(recipient, subject)
150
+ email << nil
151
+ email << "<pre>#{body}</pre>"
152
+ email = email.join($/) << $/
153
+
154
+ return email if $TESTING
155
+
156
+ IO.popen("/usr/sbin/sendmail -i -t", "w+") do |sm|
157
+ sm.print email
158
+ sm.flush
159
+ end
160
+ end
161
+
162
+ def self.envelope(recipient, subject = nil) # :nodoc:
163
+ envelope = {}
164
+ envelope['To'] = recipient
165
+ envelope['Subject'] = subject || "pl_analyze"
166
+ envelope['Content-Type'] = "text/html"
167
+
168
+ return envelope.map { |(k,v)| "#{k}: #{v}" }
169
+ end
170
+
171
+ ##
172
+ # Creates a new Analyzer that will read data from +logfile_name+.
173
+
174
+ def initialize(logfile_name)
175
+ @logfile_name = logfile_name
176
+ @request_times = Hash.new { |h,k| h[k] = [] }
177
+ @db_times = Hash.new { |h,k| h[k] = [] }
178
+ @render_times = Hash.new { |h,k| h[k] = [] }
179
+ end
180
+
181
+ ##
182
+ # Processes the log file collecting statistics from each found LogEntry.
183
+
184
+ def process
185
+ if File.directory?(@logfile_name)
186
+ dir_path = @logfile_name
187
+ Dir.new(dir_path).each do |filename|
188
+ unless filename[0,1] == "."
189
+ file_path = File.join(dir_path, filename)
190
+ File.open file_path do |fp|
191
+ LogParser.parse fp do |entry|
192
+ entry_page = entry.page
193
+ next if entry_page.nil?
194
+ @request_times[entry_page] << entry.request_time
195
+ @db_times[entry_page] << entry.db_time
196
+ @render_times[entry_page] << entry.render_time
197
+ end
198
+ end
199
+ end
200
+ end
201
+ else
202
+ File.open @logfile_name do |fp|
203
+ LogParser.parse fp do |entry|
204
+ entry_page = entry.page
205
+ next if entry_page.nil?
206
+ @request_times[entry_page] << entry.request_time
207
+ @db_times[entry_page] << entry.db_time
208
+ @render_times[entry_page] << entry.render_time
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ ##
215
+ # The average total request time for all requests.
216
+
217
+ def average_request_time
218
+ return time_average(@request_times)
219
+ end
220
+
221
+ ##
222
+ # The standard deviation of the total request time for all requests.
223
+
224
+ def request_time_std_dev
225
+ return time_std_dev(@request_times)
226
+ end
227
+
228
+ ##
229
+ # The +limit+ slowest total request times.
230
+
231
+ def slowest_request_times(limit = 10)
232
+ return slowest_times(@request_times, limit)
233
+ end
234
+
235
+ ##
236
+ # The average total database time for all requests.
237
+
238
+ def average_db_time
239
+ return time_average(@db_times)
240
+ end
241
+
242
+ ##
243
+ # The standard deviation of the total database time for all requests.
244
+
245
+ def db_time_std_dev
246
+ return time_std_dev(@db_times)
247
+ end
248
+
249
+ ##
250
+ # The +limit+ slowest total database times.
251
+
252
+ def slowest_db_times(limit = 10)
253
+ return slowest_times(@db_times, limit)
254
+ end
255
+
256
+ ##
257
+ # The average total render time for all requests.
258
+
259
+ def average_render_time
260
+ return time_average(@render_times)
261
+ end
262
+
263
+ ##
264
+ # The standard deviation of the total render time for all requests.
265
+
266
+ def render_time_std_dev
267
+ return time_std_dev(@render_times)
268
+ end
269
+
270
+ ##
271
+ # The +limit+ slowest total render times for all requests.
272
+
273
+ def slowest_render_times(limit = 10)
274
+ return slowest_times(@render_times, limit)
275
+ end
276
+
277
+ ##
278
+ # A list of count/min/max/avg/std dev for request times.
279
+
280
+ def request_times_summary
281
+ return summarize("Request Times", @request_times)
282
+ end
283
+
284
+ ##
285
+ # A list of count/min/max/avg/std dev for database times.
286
+
287
+ def db_times_summary
288
+ return summarize("DB Times", @db_times)
289
+ end
290
+
291
+ ##
292
+ # A list of count/min/max/avg/std dev for request times.
293
+
294
+ def render_times_summary
295
+ return summarize("Render Times", @render_times)
296
+ end
297
+
298
+ ##
299
+ # Builds a report containing +count+ slow items.
300
+
301
+ def report(count)
302
+ return "No requests to analyze" if request_times.empty?
303
+
304
+ text = []
305
+
306
+ text << request_times_summary
307
+ text << nil
308
+ text << "Slowest Request Times:"
309
+ slowest_request_times(count).each do |time, name|
310
+ text << "\t#{name} took #{'%0.3f' % time}s"
311
+ end
312
+ text << nil
313
+ text << "-" * 72
314
+ text << nil
315
+
316
+ text << db_times_summary
317
+ text << nil
318
+ text << "Slowest Total DB Times:"
319
+ slowest_db_times(count).each do |time, name|
320
+ text << "\t#{name} took #{'%0.3f' % time}s"
321
+ end
322
+ text << nil
323
+ text << "-" * 72
324
+ text << nil
325
+
326
+ text << render_times_summary
327
+ text << nil
328
+ text << "Slowest Total Render Times:"
329
+ slowest_render_times(count).each do |time, name|
330
+ unless ('%0.3f' % time) == "0.000"
331
+ text << "\t#{name} took #{'%0.3f' % time}s"
332
+ end
333
+ end
334
+ text << nil
335
+
336
+ return text.join($/)
337
+ end
338
+
339
+ private unless $TESTING
340
+
341
+ def summarize(title, records) # :nodoc:
342
+ record = nil
343
+ list = []
344
+
345
+ # header
346
+ record = [pad_request_name("#{title} Summary"), 'Count', 'Avg', 'Std Dev',
347
+ 'Min', 'Max']
348
+ list << record.join("\t")
349
+
350
+ # all requests
351
+ times = records.values.flatten
352
+ record = [times.average, times.standard_deviation, times.min, times.max]
353
+ record.map! { |v| "%0.3f" % v }
354
+ record.unshift [pad_request_name('ALL REQUESTS'), times.size]
355
+ list << record.join("\t")
356
+
357
+ # spacer
358
+ list << nil
359
+
360
+ records.sort_by { |k,v| v.size}.reverse_each do |req, times|
361
+ record = [times.average, times.standard_deviation, times.min, times.max]
362
+ record.map! { |v| "%0.3f" % v }
363
+ record.unshift ["#{pad_request_name req}", times.size]
364
+ list << record.join("\t")
365
+ end
366
+
367
+ return list.join("\n")
368
+ end
369
+
370
+ def slowest_times(records, limit) # :nodoc:
371
+ slowest_times = SlowestTimes.new limit
372
+
373
+ records.each do |name, times|
374
+ times.each do |time|
375
+ slowest_times << [time, name]
376
+ end
377
+ end
378
+
379
+ return slowest_times.sort_by { |time, name| time }.reverse
380
+ end
381
+
382
+ def time_average(records) # :nodoc:
383
+ times = records.values.flatten
384
+ times.delete 0
385
+ return times.average
386
+ end
387
+
388
+ def time_std_dev(records) # :nodoc:
389
+ times = records.values.flatten
390
+ times.delete 0
391
+ return times.standard_deviation
392
+ end
393
+
394
+ def longest_request_name # :nodoc:
395
+ return @longest_req if defined? @longest_req
396
+
397
+ names = @request_times.keys.map do |name|
398
+ (name||'Unknown').length + 1 # + : - HACK where does nil come from?
399
+ end
400
+
401
+ @longest_req = names.max
402
+
403
+ @longest_req = 'Unknown'.length + 1 if @longest_req.nil?
404
+
405
+ return @longest_req
406
+ end
407
+
408
+ def pad_request_name(name) # :nodoc:
409
+ name = (name||'Unknown') + ':' # HACK where does nil come from?
410
+ padding_width = longest_request_name - name.length
411
+ padding_width = 0 if padding_width < 0
412
+ name += (' ' * padding_width)
413
+ end
414
+
415
+ end
416
+
@@ -0,0 +1,228 @@
1
+ ##
2
+ # LogParser parses a Syslog log file looking for lines logged by the 'rails'
3
+ # program. A typical log line looks like this:
4
+ #
5
+ # Mar 7 00:00:20 online1 rails[59600]: Person Load (0.001884) SELECT * FROM people WHERE id = 10519 LIMIT 1
6
+ #
7
+ # LogParser does not work with Rails' default logger because there is no way
8
+ # to group all the log output of a single request. You must use SyslogLogger.
9
+
10
+ module LogParser
11
+
12
+ def self.syslog_mode?
13
+ @syslog_mode
14
+ end
15
+
16
+ def self.syslog_mode!
17
+ @syslog_mode = true
18
+ end
19
+ def self.vanilla_mode!
20
+ @syslog_mode = false
21
+ end
22
+
23
+ ##
24
+ # LogEntry contains a summary of log data for a single request.
25
+
26
+ class LogEntry
27
+
28
+ ##
29
+ # Controller and action for this request
30
+
31
+ attr_reader :page
32
+
33
+ ##
34
+ # Requesting IP
35
+
36
+ attr_reader :ip
37
+
38
+ ##
39
+ # Time the request was made
40
+
41
+ attr_reader :time
42
+
43
+ ##
44
+ # Array of SQL queries containing query type and time taken. The
45
+ # complete text of the SQL query is not saved to reduct memory usage.
46
+
47
+ attr_reader :queries
48
+
49
+ ##
50
+ # Total request time, including database, render and other.
51
+
52
+ attr_reader :request_time
53
+
54
+ ##
55
+ # Total render time.
56
+
57
+ attr_reader :render_time
58
+
59
+ ##
60
+ # Total database time
61
+
62
+ attr_reader :db_time
63
+
64
+ ##
65
+ # Creates a new LogEntry from the log data in +entry+.
66
+
67
+ attr_reader :row_count, :query_count, :request_size, :response_size
68
+
69
+ def initialize(entry)
70
+ @page = nil
71
+ @ip = nil
72
+ @time = nil
73
+ @queries = []
74
+ @request_time = 0
75
+ @render_time = 0
76
+ @db_time = 0
77
+ @in_component = 0
78
+
79
+ parse entry
80
+ end
81
+
82
+ ##
83
+ # Extracts log data from +entry+, which is an Array of lines from the
84
+ # same request.
85
+
86
+ def parse(entry)
87
+ entry.each do |line|
88
+ case line
89
+ when /^Parameters/, /^Cookie set/, /^Rendering/,
90
+ /^Redirected/ then
91
+ # nothing
92
+ when /^Processing ([\S]+) \(for (.+) at (.*)\)/ then
93
+ next if @in_component > 0
94
+ @page = $1
95
+ @ip = $2
96
+ @time = $3
97
+ when /^Completed in ([\S]+) \(\d* reqs\/sec\) \| (.+)/,
98
+ /^Completed in ([\S]+) \((.+)\)/ then
99
+
100
+ next if @in_component > 0
101
+ # handle millisecond times as well as fractional seconds
102
+ @times_in_milliseconds = $1[-2..-1] == 'ms'
103
+
104
+ @request_time = @times_in_milliseconds ? ($1.to_i/1000.0) : $1.to_f
105
+ log_info = $2
106
+
107
+ log_info = log_info.split(/[,|]/)
108
+ log_info = log_info.map do |entry|
109
+ next nil unless entry.index(': ')
110
+ result = entry.strip.split(': ')
111
+ if result.size > 2
112
+ result = [result[0], result[1..-1].join(':')]
113
+ end
114
+ result
115
+ end.compact.flatten
116
+
117
+ log_info = Hash[*log_info]
118
+
119
+ @row_count = log_info['Rows'].to_i
120
+ @query_count = log_info['Queries'].to_i
121
+ @request_size = log_info['Request Size'].to_i
122
+ @response_size = log_info['Response Size'].to_i
123
+
124
+ @page = log_info['Processed'] if log_info['Processed']
125
+ @page += ".#{log_info['Response Format']}" if log_info['Response Format']
126
+
127
+ if x = (log_info['DB'])
128
+ x = x.split(' ').first
129
+ @db_time = @times_in_milliseconds ? (x.to_i/1000.0) : x.to_f
130
+ end
131
+
132
+ if x = (log_info['Rendering'] || log_info['View'])
133
+ x = x.split(' ').first
134
+ @render_time = @times_in_milliseconds ? (x.to_i/1000.0) : x.to_f
135
+ end
136
+
137
+ when /(.+?) \(([^)]+)\) / then
138
+ @queries << [$1, $2.to_f]
139
+ when /^Start rendering component / then
140
+ @in_component += 1
141
+ when /^End of component rendering$/ then
142
+ @in_component -= 1
143
+ when /^Fragment hit: / then
144
+ else # noop
145
+ # raise "Can't handle #{line.inspect}" if $TESTING
146
+ end
147
+ end
148
+ end
149
+
150
+ def ==(other) # :nodoc:
151
+ other.class == self.class and
152
+ other.page == self.page and
153
+ other.ip == self.ip and
154
+ other.time == self.time and
155
+ other.queries == self.queries and
156
+ other.request_time == self.request_time and
157
+ other.render_time == self.render_time and
158
+ other.db_time == self.db_time
159
+ end
160
+
161
+ end
162
+
163
+ def self.extract_bucket_and_data(line)
164
+ if LogParser.syslog_mode?
165
+ line =~ / ([^ ]+) ([^ ]+)\[(\d+)\]: (.*)/
166
+ return nil if $2.nil? or $2 == 'newsyslog'
167
+ bucket = [$1, $2, $3].join '-'
168
+ data = $4
169
+ return [bucket, data]
170
+ else
171
+ return ["(none)", line]
172
+ end
173
+ end
174
+
175
+ def self.detect_mode(stream)
176
+ lines_read = []
177
+ while(!stream.eof? && lines_read.size < 5)
178
+ lines_read << stream.readline
179
+ end
180
+ stream.rewind
181
+
182
+ lines_read.each do |line|
183
+ line =~ / ([^ ]+) ([^ ]+)\[(\d+)\]: (.*)/
184
+ if $4
185
+ return syslog_mode!
186
+ end
187
+ end
188
+ return vanilla_mode!
189
+ end
190
+
191
+ ##
192
+ # Parses IO stream +stream+, creating a LogEntry for each recognizable log
193
+ # entry.
194
+ #
195
+ # Log entries are recognised as starting with Processing, continuing with
196
+ # the same process id through Completed.
197
+
198
+ def self.parse(stream) # :yields: log_entry
199
+ buckets = Hash.new { |h,k| h[k] = [] }
200
+ comp_count = Hash.new 0
201
+
202
+ LogParser.detect_mode(stream)
203
+
204
+ stream.each_line do |line|
205
+ bucket, data = LogParser.extract_bucket_and_data(line)
206
+ next if !bucket
207
+
208
+ buckets[bucket] << data
209
+
210
+ case data
211
+ when /^Start rendering component / then
212
+ comp_count[bucket] += 1
213
+ when /^End of component rendering$/ then
214
+ comp_count[bucket] -= 1
215
+ when /^Completed/ then
216
+ next unless comp_count[bucket] == 0
217
+ entry = buckets.delete bucket
218
+ yield LogEntry.new(entry)
219
+ end
220
+ end
221
+
222
+ buckets.each do |bucket, data|
223
+ yield LogEntry.new(data)
224
+ end
225
+ end
226
+
227
+ end
228
+