brontes3d-production_log_analyzer 2009022403
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/History.txt +34 -0
- data/LICENSE.txt +27 -0
- data/Manifest.txt +18 -0
- data/README.txt +147 -0
- data/Rakefile +17 -0
- data/bin/action_errors +46 -0
- data/bin/action_grep +19 -0
- data/bin/pl_analyze +36 -0
- data/lib/passenger_log_per_proc.rb +55 -0
- data/lib/production_log/action_grep.rb +41 -0
- data/lib/production_log/analyzer.rb +416 -0
- data/lib/production_log/parser.rb +228 -0
- data/test/test_action_grep.rb +72 -0
- data/test/test_analyzer.rb +425 -0
- data/test/test_helper.rb +68 -0
- data/test/test_parser.rb +420 -0
- data/test/test_passenger_log_per_proc.rb +88 -0
- data/test/test_syslogs/test.syslog.0.14.x.log +4 -0
- data/test/test_syslogs/test.syslog.1.2.shortname.log +4 -0
- data/test/test_syslogs/test.syslog.empty.log +0 -0
- data/test/test_syslogs/test.syslog.log +256 -0
- data/test/test_vanilla/test.0.14.x.log +4 -0
- data/test/test_vanilla/test.1.2.shortname.log +4 -0
- data/test/test_vanilla/test.empty.log +0 -0
- data/test/test_vanilla/test.log +255 -0
- data/test/test_vanilla/test_log_parts/1_online1-rails-59600.log +7 -0
- data/test/test_vanilla/test_log_parts/2_online2-rails-59628.log +11 -0
- data/test/test_vanilla/test_log_parts/3_online1-rails-59628.log +9 -0
- data/test/test_vanilla/test_log_parts/4_online1-rails-59645.log +30 -0
- data/test/test_vanilla/test_log_parts/5_online1-rails-59629.log +38 -0
- data/test/test_vanilla/test_log_parts/6_online1-rails-60654.log +32 -0
- data/test/test_vanilla/test_log_parts/7_online1-rails-59627.log +70 -0
- data/test/test_vanilla/test_log_parts/8_online1-rails-59635.log +58 -0
- 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
|
+
|