log_sense 2.0.0 → 2.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.org +23 -12
- data/Gemfile.lock +1 -1
- data/README.org +4 -3
- data/exe/log_sense +2 -0
- data/ip_locations/dbip-country-lite.sqlite3 +0 -0
- data/lib/log_sense/apache_aggregator.rb +1 -1
- data/lib/log_sense/emitter.rb +9 -0
- data/lib/log_sense/rails/log_parser.rb +373 -154
- data/lib/log_sense/rails_aggregator.rb +68 -39
- data/lib/log_sense/rails_report_shaper.rb +92 -35
- data/lib/log_sense/report_shaper.rb +9 -3
- data/lib/log_sense/templates/_output_table.html.erb +13 -16
- data/lib/log_sense/templates/_stylesheet.css +20 -1
- data/lib/log_sense/templates/report_html.erb +4 -0
- data/lib/log_sense/templates/world.svg +1 -1
- data/lib/log_sense/version.rb +1 -1
- data/todo.org +27 -3
- metadata +2 -2
|
@@ -84,28 +84,6 @@ module LogSense
|
|
|
84
84
|
values (?, ?, ?, ?, ?)
|
|
85
85
|
EOS
|
|
86
86
|
|
|
87
|
-
db.execute <<-EOS
|
|
88
|
-
CREATE TABLE IF NOT EXISTS Render(
|
|
89
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
90
|
-
partial TEXT,
|
|
91
|
-
duration_ms FLOAT,
|
|
92
|
-
allocations INTEGER,
|
|
93
|
-
filename TEXT,
|
|
94
|
-
line_number INTEGER
|
|
95
|
-
)
|
|
96
|
-
EOS
|
|
97
|
-
|
|
98
|
-
ins_rendered = db.prepare <<-EOS
|
|
99
|
-
insert into Render(
|
|
100
|
-
partial,
|
|
101
|
-
duration_ms,
|
|
102
|
-
allocations,
|
|
103
|
-
filename,
|
|
104
|
-
line_number
|
|
105
|
-
)
|
|
106
|
-
values (?, ?, ?, ?, ?)
|
|
107
|
-
EOS
|
|
108
|
-
|
|
109
87
|
db.execute <<-EOS
|
|
110
88
|
CREATE TABLE IF NOT EXISTS BrowserInfo(
|
|
111
89
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -134,6 +112,51 @@ module LogSense
|
|
|
134
112
|
values (?, ?, ?, ?, ?, ?, ?, ?)
|
|
135
113
|
EOS
|
|
136
114
|
|
|
115
|
+
# jobs
|
|
116
|
+
db.execute <<-EOS
|
|
117
|
+
CREATE TABLE IF NOT EXISTS Job(
|
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
+
started_at TEXT,
|
|
120
|
+
ended_at TEXT,
|
|
121
|
+
duration_total_ms FLOAT,
|
|
122
|
+
worker TEXT,
|
|
123
|
+
host TEXT,
|
|
124
|
+
pid TEXT,
|
|
125
|
+
log_id TEXT,
|
|
126
|
+
job_id TEXT,
|
|
127
|
+
object_id TEXT,
|
|
128
|
+
method TEXT,
|
|
129
|
+
arguments TEXT,
|
|
130
|
+
exit_status TEXT,
|
|
131
|
+
attempt INTEGER,
|
|
132
|
+
error_msg TEXT,
|
|
133
|
+
source_file TEXT,
|
|
134
|
+
line_number INTEGER
|
|
135
|
+
)
|
|
136
|
+
EOS
|
|
137
|
+
|
|
138
|
+
ins_job = db.prepare <<-EOS
|
|
139
|
+
insert into Job(
|
|
140
|
+
started_at,
|
|
141
|
+
ended_at,
|
|
142
|
+
duration_total_ms,
|
|
143
|
+
worker,
|
|
144
|
+
host,
|
|
145
|
+
pid,
|
|
146
|
+
log_id,
|
|
147
|
+
job_id,
|
|
148
|
+
object_id,
|
|
149
|
+
method,
|
|
150
|
+
arguments,
|
|
151
|
+
exit_status,
|
|
152
|
+
attempt,
|
|
153
|
+
error_msg,
|
|
154
|
+
source_file,
|
|
155
|
+
line_number
|
|
156
|
+
)
|
|
157
|
+
values (#{Array.new(16, "?").join(", ")})
|
|
158
|
+
EOS
|
|
159
|
+
|
|
137
160
|
# requests in the log might be interleaved.
|
|
138
161
|
#
|
|
139
162
|
# We use the 'pending' variable to progressively store data
|
|
@@ -142,6 +165,15 @@ module LogSense
|
|
|
142
165
|
# hash
|
|
143
166
|
pending = {}
|
|
144
167
|
|
|
168
|
+
# for delayed jobs
|
|
169
|
+
pending_jobs = {}
|
|
170
|
+
|
|
171
|
+
# Fatal explanation messages span several lines (2, 4, ?)
|
|
172
|
+
#
|
|
173
|
+
# We keep a Hash with the FATAL explanation messages and we persist when
|
|
174
|
+
# the parsing ends
|
|
175
|
+
fatal_explanation_messages = {}
|
|
176
|
+
|
|
145
177
|
# Log lines are either one of:
|
|
146
178
|
#
|
|
147
179
|
# LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Started VERB "URL" for IP at TIMESTAMP
|
|
@@ -158,24 +190,9 @@ module LogSense
|
|
|
158
190
|
stream.readlines.each_with_index do |line, line_number|
|
|
159
191
|
filename = stream == $stdin ? "stdin" : stream.path
|
|
160
192
|
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
data = match_and_process_rendered line
|
|
166
|
-
if data
|
|
167
|
-
ins_rendered.execute(
|
|
168
|
-
data[:partial], data[:duration], data[:allocations],
|
|
169
|
-
filename, line_number
|
|
170
|
-
)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
|
|
177
|
-
# I and F for completed requests, [ is for error messages
|
|
178
|
-
next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
|
|
193
|
+
# I, F are for completed and failed requests, [ is for the FATAL
|
|
194
|
+
# error message explanation
|
|
195
|
+
next unless ['I', 'F', 'E', '['].include? line[0]
|
|
179
196
|
|
|
180
197
|
data = match_and_process_browser_info line
|
|
181
198
|
if data
|
|
@@ -190,16 +207,6 @@ module LogSense
|
|
|
190
207
|
next
|
|
191
208
|
end
|
|
192
209
|
|
|
193
|
-
data = match_and_process_error line
|
|
194
|
-
if data
|
|
195
|
-
ins_error.execute(data[:log_id],
|
|
196
|
-
data[:context],
|
|
197
|
-
data[:description],
|
|
198
|
-
filename,
|
|
199
|
-
line_number)
|
|
200
|
-
next
|
|
201
|
-
end
|
|
202
|
-
|
|
203
210
|
data = match_and_process_start line
|
|
204
211
|
if data
|
|
205
212
|
id = data[:log_id]
|
|
@@ -214,15 +221,14 @@ module LogSense
|
|
|
214
221
|
next
|
|
215
222
|
end
|
|
216
223
|
|
|
224
|
+
# fatal message is alternative to completed and is used to insert an
|
|
225
|
+
# Event
|
|
217
226
|
data = match_and_process_fatal line
|
|
218
227
|
if data
|
|
219
228
|
id = data[:log_id]
|
|
220
|
-
# it might as well be that the first event started before
|
|
221
|
-
# the log. With this, we make sure we add only events whose
|
|
222
|
-
# start was logged and parsed
|
|
223
229
|
if pending[id]
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
# data last, so that we respect, for instance, the 'F' state
|
|
231
|
+
event = pending[id].merge(data)
|
|
226
232
|
ins.execute(
|
|
227
233
|
event[:exit_status],
|
|
228
234
|
event[:started_at],
|
|
@@ -247,7 +253,7 @@ module LogSense
|
|
|
247
253
|
end
|
|
248
254
|
end
|
|
249
255
|
|
|
250
|
-
data =
|
|
256
|
+
data = match_and_process_completed line
|
|
251
257
|
if data
|
|
252
258
|
id = data[:log_id]
|
|
253
259
|
|
|
@@ -255,7 +261,9 @@ module LogSense
|
|
|
255
261
|
# the log. With this, we make sure we add only events whose
|
|
256
262
|
# start was logged and parsed
|
|
257
263
|
if pending[id]
|
|
258
|
-
|
|
264
|
+
# data last, so that we respect the most recent data (the last
|
|
265
|
+
# log line)
|
|
266
|
+
event = pending[id].merge(data)
|
|
259
267
|
|
|
260
268
|
ins.execute(
|
|
261
269
|
event[:exit_status],
|
|
@@ -280,67 +288,160 @@ module LogSense
|
|
|
280
288
|
pending.delete(id)
|
|
281
289
|
end
|
|
282
290
|
end
|
|
291
|
+
|
|
292
|
+
# fatal_explanations are multiple lines with a description of the
|
|
293
|
+
# fatal error and they all use the ID of the FATAL event (so that
|
|
294
|
+
# we can later join)
|
|
295
|
+
data = match_and_process_fatal_explanation line
|
|
296
|
+
if data
|
|
297
|
+
previous = fatal_explanation_messages[data[:log_id]]
|
|
298
|
+
|
|
299
|
+
# keep adding to the explanation
|
|
300
|
+
fatal_explanation_messages[data[:log_id]] = [
|
|
301
|
+
data[:log_id],
|
|
302
|
+
[previous ? previous[1] : "", data[:context]].compact.join(" "),
|
|
303
|
+
[previous ? previous[2] : "", data[:description]].compact.join(" "),
|
|
304
|
+
filename,
|
|
305
|
+
line_number
|
|
306
|
+
]
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
#
|
|
311
|
+
# Match enqueuing job
|
|
312
|
+
#
|
|
313
|
+
data = match_and_process_enqueuing_job line
|
|
314
|
+
if data
|
|
315
|
+
id = data[:job_id]
|
|
316
|
+
pending_jobs[id] = data.merge(pending_jobs[id] || {})
|
|
317
|
+
next
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
#
|
|
321
|
+
# Match running
|
|
322
|
+
#
|
|
323
|
+
data = match_and_process_running_job line
|
|
324
|
+
if data
|
|
325
|
+
id = data[:job_id]
|
|
326
|
+
# change the key to pid
|
|
327
|
+
pid = data[:object_id]
|
|
328
|
+
pending_jobs[pid] = data.merge(pending_jobs[id] || {})
|
|
329
|
+
|
|
330
|
+
pending_jobs.delete(id)
|
|
331
|
+
next
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
#
|
|
335
|
+
# Match completed
|
|
336
|
+
#
|
|
337
|
+
data = match_and_process_completed_job line
|
|
338
|
+
if data
|
|
339
|
+
id = data[:object_id]
|
|
340
|
+
# it has to be there!
|
|
341
|
+
if pending_jobs[id]
|
|
342
|
+
data = (pending_jobs[id] || {}).merge(data)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
ins_job.execute(
|
|
346
|
+
data[:started_at],
|
|
347
|
+
data[:ended_at],
|
|
348
|
+
data[:duration_total_ms],
|
|
349
|
+
data[:worker],
|
|
350
|
+
data[:host],
|
|
351
|
+
data[:pid],
|
|
352
|
+
data[:log_id],
|
|
353
|
+
data[:job_id], # no longer necessary
|
|
354
|
+
data[:object_id], # completed jobs are destroyed
|
|
355
|
+
data[:method],
|
|
356
|
+
data[:arguments],
|
|
357
|
+
data[:exit_status],
|
|
358
|
+
data[:attempt],
|
|
359
|
+
data[:error_msg],
|
|
360
|
+
filename,
|
|
361
|
+
line_number
|
|
362
|
+
)
|
|
363
|
+
pending_jobs.delete(id)
|
|
364
|
+
next
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
#
|
|
368
|
+
# Match job errors
|
|
369
|
+
#
|
|
370
|
+
data = match_and_process_job_error line
|
|
371
|
+
if data
|
|
372
|
+
# it has to be there!
|
|
373
|
+
if pending_jobs[id]
|
|
374
|
+
data = (pending_jobs[id] || {}).merge(data)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
ins_job.execute(
|
|
378
|
+
data[:started_at],
|
|
379
|
+
data[:ended_at],
|
|
380
|
+
0,
|
|
381
|
+
data[:worker],
|
|
382
|
+
data[:host],
|
|
383
|
+
data[:pid],
|
|
384
|
+
data[:log_id],
|
|
385
|
+
"",
|
|
386
|
+
data[:object_id],
|
|
387
|
+
data[:method],
|
|
388
|
+
data[:arguments],
|
|
389
|
+
data[:exit_status],
|
|
390
|
+
data[:attempt],
|
|
391
|
+
data[:error_msg],
|
|
392
|
+
filename,
|
|
393
|
+
line_number
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
pending_jobs.delete(id)
|
|
397
|
+
end
|
|
283
398
|
end
|
|
284
399
|
end
|
|
285
400
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
TIMESTAMP = /(?<timestamp>[^ ]+)/
|
|
291
|
-
ID = /(?<id>[a-z0-9-]+)/
|
|
292
|
-
VERB = /(?<verb>GET|POST|PATCH|PUT|DELETE)/
|
|
293
|
-
URL = /(?<url>[^"]+)/
|
|
294
|
-
IP = /(?<ip>[0-9.]+)/
|
|
295
|
-
STATUS = /(?<status>[0-9]+)/
|
|
296
|
-
STATUS_IN_WORDS = /(OK|Unauthorized|Found|Internal Server Error|Bad Request|Method Not Allowed|Request Timeout|Not Implemented|Bad Gateway|Service Unavailable)/
|
|
297
|
-
MSECS = /[0-9.]+/
|
|
298
|
-
|
|
299
|
-
# I, [2024-07-01T02:21:34.339058 #1392909] INFO -- : [815b3e28-8d6e-4741-8605-87654a9ff58c] BrowserInfo: "Unknown Browser","unknown_platform","Unknown","Devise::SessionsController","new","html","4db749654a0fcacbf3868f87723926e7405262f8d596e8514f4997dc80a3cd7e","2024-07-01T02:21:34+02:00"
|
|
300
|
-
BROWSER_INFO_REGEXP = /BrowserInfo: "(?<browser>.+)","(?<platform>.+)","(?<device_name>.+)","(?<controller>.+)","(?<method>.+)","(?<request_format>.+)","(?<anon_ip>.+)","(?<timestamp>.+)"/
|
|
301
|
-
def match_and_process_browser_info(line)
|
|
302
|
-
matchdata = BROWSER_INFO_REGEXP.match line
|
|
303
|
-
if matchdata
|
|
304
|
-
{
|
|
305
|
-
browser: matchdata[:browser],
|
|
306
|
-
platform: matchdata[:platform],
|
|
307
|
-
device_name: matchdata[:device_name],
|
|
308
|
-
controller: matchdata[:controller],
|
|
309
|
-
method: matchdata[:method],
|
|
310
|
-
request_format: matchdata[:request_format],
|
|
311
|
-
anon_ip: matchdata[:anon_ip],
|
|
312
|
-
timestamp: matchdata[:timestamp],
|
|
313
|
-
}
|
|
401
|
+
# persist the fatal error messages
|
|
402
|
+
# TODO massive update
|
|
403
|
+
fatal_explanation_messages.values.map do |value|
|
|
404
|
+
ins_error.execute(value)
|
|
314
405
|
end
|
|
315
|
-
end
|
|
316
406
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
407
|
+
# DO NOT persist the pending_jobs which have not yet completed (those
|
|
408
|
+
# still available at: pending_jobs).
|
|
409
|
+
#
|
|
410
|
+
# In fact various entries initiated with RUNNING end up with "performed"
|
|
411
|
+
# (rather than COMPLETED). Notice that entries COMPLETED always have a
|
|
412
|
+
# peformed entry as well.
|
|
413
|
+
#
|
|
414
|
+
# Since we do not yet process "performed" entry log and in pending jobs
|
|
415
|
+
# we end up accumulating a bunch of entries which are marked as
|
|
416
|
+
# "performed"
|
|
417
|
+
#
|
|
418
|
+
# Performed entries are tricky since they use JOB_ID, rather than the
|
|
419
|
+
# object_id and probably requires to change how we enter pending_ids
|
|
323
420
|
|
|
324
|
-
|
|
325
|
-
matchdata = ERROR_REGEXP.match line
|
|
326
|
-
if matchdata
|
|
327
|
-
{
|
|
328
|
-
log_id: matchdata[:id],
|
|
329
|
-
context: matchdata[:context],
|
|
330
|
-
description: matchdata[:description]
|
|
331
|
-
}
|
|
332
|
-
end
|
|
421
|
+
db
|
|
333
422
|
end
|
|
334
423
|
|
|
424
|
+
# could be private here, I guess we keep them public to make them simpler
|
|
425
|
+
# to try from irb
|
|
426
|
+
|
|
427
|
+
TIMESTAMP = '(?<timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]+)'
|
|
428
|
+
LOG_ID = '(?<log_id>[a-z0-9-]+)'
|
|
429
|
+
VERB = '(?<verb>GET|POST|PATCH|PUT|DELETE)'
|
|
430
|
+
URL = '(?<url>[^"]+)'
|
|
431
|
+
IP = '(?<ip>[0-9.]+)'
|
|
432
|
+
STATUS = '(?<status>[0-9]+)'
|
|
433
|
+
STATUS_IN_WORDS = '(OK|Unauthorized|Found|Internal Server Error|Bad Request|Method Not Allowed|Request Timeout|Not Implemented|Bad Gateway|Service Unavailable)'
|
|
434
|
+
MSECS = '[0-9.]+'
|
|
435
|
+
|
|
335
436
|
# I, [2021-10-19T08:16:34.343858 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Started GET "/grow/people/471" for 217.77.80.35 at 2021-10-19 08:16:34 +0000
|
|
336
|
-
STARTED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{
|
|
437
|
+
STARTED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{LOG_ID}\] Started #{VERB} "#{URL}" for #{IP} at/o
|
|
337
438
|
|
|
338
439
|
def match_and_process_start(line)
|
|
339
440
|
matchdata = STARTED_REGEXP.match line
|
|
340
441
|
if matchdata
|
|
341
442
|
{
|
|
342
443
|
started_at: matchdata[:timestamp],
|
|
343
|
-
log_id: matchdata[:
|
|
444
|
+
log_id: matchdata[:log_id],
|
|
344
445
|
html_verb: matchdata[:verb],
|
|
345
446
|
url: matchdata[:url],
|
|
346
447
|
ip: matchdata[:ip]
|
|
@@ -352,7 +453,7 @@ module LogSense
|
|
|
352
453
|
# I, [2021-10-19T08:16:34.712331 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Completed 200 OK in 367ms (Views: 216.7ms | ActiveRecord: 141.3ms | Allocations: 168792)
|
|
353
454
|
# I, [2021-12-09T16:53:52.657727 #2735058] INFO -- : [0064e403-9eb2-439d-8fe1-a334c86f5532] Completed 200 OK in 13ms (Views: 11.1ms | ActiveRecord: 1.2ms)
|
|
354
455
|
# I, [2021-12-06T14:28:19.736545 #2804090] INFO -- : [34091cb5-3e7b-4042-aaf8-6c6510d3f14c] Completed 500 Internal Server Error in 66ms (ActiveRecord: 8.0ms | Allocations: 24885)
|
|
355
|
-
COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{
|
|
456
|
+
COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{LOG_ID}\] Completed #{STATUS} #{STATUS_IN_WORDS} in (?<total>#{MSECS})ms \((Views: (?<views>#{MSECS})ms \| )?ActiveRecord: (?<arec>#{MSECS})ms( \| Allocations: (?<alloc>[0-9]+))?\)/o
|
|
356
457
|
|
|
357
458
|
def match_and_process_completed(line)
|
|
358
459
|
matchdata = (COMPLETED_REGEXP.match line)
|
|
@@ -361,7 +462,7 @@ module LogSense
|
|
|
361
462
|
{
|
|
362
463
|
exit_status: "I",
|
|
363
464
|
ended_at: matchdata[:timestamp],
|
|
364
|
-
log_id: matchdata[:
|
|
465
|
+
log_id: matchdata[:log_id],
|
|
365
466
|
status: matchdata[:status],
|
|
366
467
|
duration_total_ms: matchdata[:total],
|
|
367
468
|
duration_views_ms: matchdata[:views],
|
|
@@ -373,13 +474,13 @@ module LogSense
|
|
|
373
474
|
end
|
|
374
475
|
|
|
375
476
|
# I, [2021-10-19T08:16:34.345162 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Processing by PeopleController#show as HTML
|
|
376
|
-
PROCESSING_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{
|
|
477
|
+
PROCESSING_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{LOG_ID}\] Processing by (?<controller>[^ ]+) as/o
|
|
377
478
|
|
|
378
479
|
def match_and_process_processing_by line
|
|
379
480
|
matchdata = PROCESSING_REGEXP.match line
|
|
380
481
|
if matchdata
|
|
381
482
|
{
|
|
382
|
-
log_id: matchdata[:
|
|
483
|
+
log_id: matchdata[:log_id],
|
|
383
484
|
controller: matchdata[:controller]
|
|
384
485
|
}
|
|
385
486
|
end
|
|
@@ -389,76 +490,194 @@ module LogSense
|
|
|
389
490
|
# F, [2021-12-04T00:34:05.839157 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728] ActionController::RoutingError (No route matches [GET] "/wp/wp-includes/wlwmanifest.xml"):
|
|
390
491
|
# F, [2021-12-04T00:34:05.839209 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728]
|
|
391
492
|
# F, [2021-12-04T00:34:05.839269 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728] actionpack (5.2.4.4) lib/action_dispatch/middleware/debug_exceptions.rb:65:in `call'
|
|
392
|
-
|
|
493
|
+
|
|
494
|
+
FATAL_REGEXP = /F, \[#{TIMESTAMP} #[0-9]+\] FATAL -- : \[#{LOG_ID}\]/o
|
|
393
495
|
|
|
394
496
|
def match_and_process_fatal(line)
|
|
395
497
|
matchdata = FATAL_REGEXP.match line
|
|
396
498
|
if matchdata
|
|
397
499
|
{
|
|
398
500
|
exit_status: "F",
|
|
399
|
-
log_id: matchdata[:
|
|
400
|
-
comment: matchdata[:comment]
|
|
501
|
+
log_id: matchdata[:log_id],
|
|
401
502
|
}
|
|
402
503
|
end
|
|
403
504
|
end
|
|
404
505
|
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
# Parameters: {"locale"=>"it"}
|
|
408
|
-
# [...]
|
|
409
|
-
# Completed 200 OK in 135ms (Views: 128.0ms | ActiveRecord: 2.5ms | Allocations: 453450)
|
|
506
|
+
# Explanation of what caused a FATAL event. List of lines starting with the
|
|
507
|
+
# ID of the fatal event and providing some sort of explanation.
|
|
410
508
|
#
|
|
411
|
-
#
|
|
412
|
-
# ActionController::RoutingError (No route matches [GET] "/serviceworker.js"):
|
|
509
|
+
# Notice that we have more than one line per FATAL event
|
|
413
510
|
#
|
|
511
|
+
# [584cffcc-f1fd-4b5c-bb8b-b89621bd4921] ActionController::RoutingError (No route matches [GET] "/assets/foundation-icons.svg"):
|
|
414
512
|
#
|
|
415
|
-
#
|
|
416
|
-
# Processing by ProjectsController#create as TURBO_STREAM
|
|
417
|
-
# Parameters: {"authenticity_token"=>"[FILTERED]", "project"=>{"name"=>"AA", "funding_agency"=>"", "total_cost"=>"0,00", "personnel_cost"=>"0,00", "percentage_funded"=>"0", "from_date"=>"2024-01-01", "to_date"=>"2025-12-31", "notes"=>""}, "commit"=>"Crea Progetto", "locale"=>"it"}
|
|
513
|
+
# [fd8df8b5-83c9-48b5-a056-e5026e31bd5e] ActionView::Template::Error (undefined method `all_my_ancestor' for nil:NilClass):
|
|
418
514
|
#
|
|
419
|
-
#
|
|
420
|
-
# Completed 422 Unprocessable Entity in 16ms (Views: 5.1ms | ActiveRecord: 2.0ms | Allocations: 10093)
|
|
515
|
+
# [d17ed55c-f5f1-442a-a9d6-3035ab91adf0] ActionView::Template::Error (undefined method `volunteer_for' for #<DonationsController:0x007f4864c564b8>
|
|
421
516
|
#
|
|
422
|
-
|
|
423
|
-
#
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
517
|
+
#, [2024-08-20T09:41:35.140725 #4151931] FATAL -- : [f57e3648-568a-48f9-ae3a-a522b1ff3298]
|
|
518
|
+
# [f57e3648-568a-48f9-ae3a-a522b1ff3298] NoMethodError (undefined method `available_quantity' for nil:NilClass
|
|
519
|
+
# [f57e3648-568a-48f9-ae3a-a522b1ff3298]
|
|
520
|
+
# [f57e3648-568a-48f9-ae3a-a522b1ff3298] app/models/donations/donation.rb:462:in `block in build_items_for_delivery'
|
|
521
|
+
# [f57e3648-568a-48f9-ae3a-a522b1ff3298] app/models/donations/donation.rb:440:in `build_items_for_delivery'
|
|
522
|
+
# [f57e3648-568a-48f9-ae3a-a522b1ff3298] app/controllers/donations_controller.rb:1395:in `create_delivery'
|
|
523
|
+
|
|
524
|
+
EXCEPTION = "[A-Za-z_0-9:]+(Error|NotFound|Invalid|Unknown|Missing|ENOSPC)"
|
|
525
|
+
FATAL_EXPLANATION_REGEXP = /^\[#{LOG_ID}\] (?<context>#{EXCEPTION})?(?<description>.*)/o
|
|
526
|
+
def match_and_process_fatal_explanation(line)
|
|
527
|
+
matchdata = FATAL_EXPLANATION_REGEXP.match line
|
|
528
|
+
if matchdata
|
|
529
|
+
{
|
|
530
|
+
log_id: matchdata[:log_id],
|
|
531
|
+
context: matchdata[:context],
|
|
532
|
+
description: matchdata[:description].gsub(/^ *\(/, "").gsub(/\):$/, "")
|
|
533
|
+
}
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# I, [2024-07-01T02:21:34.339058 #1392909] INFO -- : [815b3e28-8d6e-4741-8605-87654a9ff58c] BrowserInfo: "Unknown Browser","unknown_platform","Unknown","Devise::SessionsController","new","html","4db749654a0fcacbf3868f87723926e7405262f8d596e8514f4997dc80a3cd7e","2024-07-01T02:21:34+02:00"
|
|
538
|
+
BROWSER_INFO_REGEXP = /BrowserInfo: "(?<browser>.+)","(?<platform>.+)","(?<device_name>.+)","(?<controller>.+)","(?<method>.+)","(?<request_format>.+)","(?<anon_ip>.+)","(?<timestamp>.+)"/o
|
|
539
|
+
|
|
540
|
+
def match_and_process_browser_info(line)
|
|
541
|
+
matchdata = BROWSER_INFO_REGEXP.match line
|
|
542
|
+
if matchdata
|
|
543
|
+
{
|
|
544
|
+
browser: matchdata[:browser],
|
|
545
|
+
platform: matchdata[:platform],
|
|
546
|
+
device_name: matchdata[:device_name],
|
|
547
|
+
controller: matchdata[:controller],
|
|
548
|
+
method: matchdata[:method],
|
|
549
|
+
request_format: matchdata[:request_format],
|
|
550
|
+
anon_ip: matchdata[:anon_ip],
|
|
551
|
+
timestamp: matchdata[:timestamp],
|
|
552
|
+
}
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Sequence:
|
|
557
|
+
#
|
|
558
|
+
# - enqueued (LOG_ID user event, JOB_ID assigned by system)
|
|
559
|
+
# - running (JOB_ID links to enqueued; PID assigned by system; OBJECT_ID assigned by the system)
|
|
560
|
+
# - performing (OBJECT_ID links to running; OBJECT_ID assigned by the system; JOB_ID is new)
|
|
561
|
+
# - (rendering)
|
|
562
|
+
# - performed (OBJECT_ID links to running; JOB_ID links to previous)
|
|
563
|
+
# - completed (OBJECT_ID links to running; JOB_ID links to previous)
|
|
564
|
+
#
|
|
565
|
+
# SOMETIMES PERFORMED APPEARS WITH NO COMPLETED.
|
|
566
|
+
#
|
|
567
|
+
# I, [2024-08-01T06:21:16.302152 #3569287] INFO -- : [96d14192-c7cc-48a9-9df7-3786de20b085] [ActiveJob] Enqueued ActionMailer::Parameterized::DeliveryJob (Job ID: 01e82c5c-fb42-4e5f-b0a7-6fa9512a9fb5) to DelayedJob(mailers) with arguments: "MessageMailer", "build_message", "deliver_now", {:project_id=>1, :email_to=>"activpentrutine@gmail.com", :hash=>{:event_name=>"download", :subject=>"Aviz BRAC-MEGA240176", :download=>#<GlobalID:0x00007f02d8e1ad98 @uri=#<URI::GID gid://btf3/Download/10652>>, :group=>#<GlobalID:0x00007f02d8e1a820 @uri=#<URI::GID gid://btf3/Organization/10061>>}, :locale=>:ro}
|
|
568
|
+
#
|
|
569
|
+
# I, [2024-08-01T06:21:21.235006 #3563911] INFO -- : 2024-08-01T06:21:21+0200: [Worker(delayed_job host:shair1 pid:3563911)] Job ActionMailer::Parameterized::DeliveryJob [01e82c5c-fb42-4e5f-b0a7-6fa9512a9fb5] from DelayedJob(mailers) with arguments: ["MessageMailer", "build_message", "deliver_now", {"project_id"=>1, "email_to"=>"activpentrutine@gmail.com", "hash"=>{"event_name"=>"download", "subject"=>"Aviz BRAC-MEGA240176", "download"=>{"_aj_globalid"=>"gid://btf3/Download/10652"}, "group"=>{"_aj_globalid"=>"gid://btf3/Organization/10061"}, "_aj_symbol_keys"=>["event_name", "subject", "download", "group"]}, "locale"=>{"_aj_serialized"=>"ActiveJob::Serializers::SymbolSerializer", "value"=>"ro"}, "_aj_symbol_keys"=>["project_id", "email_to", "hash", "locale"]}] (id=212885) (queue=mailers) RUNNING
|
|
570
|
+
#
|
|
571
|
+
# I, [2024-08-01T06:21:21.251282 #3563911] INFO -- : [ActiveJob] [ActionMailer::Parameterized::DeliveryJob] [01e82c5c-fb42-4e5f-b0a7-6fa9512a9fb5] Performing ActionMailer::Parameterized::DeliveryJob (Job ID: 01e82c5c-fb42-4e5f-b0a7-6fa9512a9fb5) from DelayedJob(mailers) enqueued at 2024-08-01T04:21:16Z with arguments: "MessageMailer", "build_message", "deliver_now", {:project_id=>1, :email_to=>"activpentrutine@gmail.com", :hash=>{:event_name=>"download", :subject=>"Aviz BRAC-MEGA240176", :download=>#<GlobalID:0x00007fbb86760950 @uri=#<URI::GID gid://btf3/Download/10652>>, :group=>#<GlobalID:0x00007fbb86760220 @uri=#<URI::GID gid://btf3/Organization/10061>>}, :locale=>:ro}
|
|
572
|
+
#
|
|
573
|
+
# I, [2024-08-01T06:21:22.137863 #3563911] INFO -- : [ActiveJob] [ActionMailer::Parameterized::DeliveryJob] [01e82c5c-fb42-4e5f-b0a7-6fa9512a9fb5] Performed ActionMailer::Parameterized::DeliveryJob (Job ID: 01e82c5c-fb42-4e5f-b0a7-6fa9512a9fb5) from DelayedJob(mailers) in 886.42ms
|
|
574
|
+
#
|
|
575
|
+
#
|
|
576
|
+
# I, [2024-08-01T06:38:41.005687 #3563911] INFO -- : 2024-08-01T06:38:41+0200: [Worker(delayed_job host:shair1 pid:3563911)] 1 jobs processed at 1.4476 j/s, 0 failed
|
|
577
|
+
#
|
|
578
|
+
#
|
|
579
|
+
# I, [2024-08-01T06:21:22.141853 #3563911] INFO -- : 2024-08-01T06:21:22+0200: [Worker(delayed_job host:shair1 pid:3563911)] Job ActionMailer::Parameterized::DeliveryJob [01e82c5c-fb42-4e5f-b0a7-6fa9512a9fb5] from DelayedJob(mailers) with arguments: ["MessageMailer", "build_message", "deliver_now", {"project_id"=>1, "email_to"=>"activpentrutine@gmail.com", "hash"=>{"event_name"=>"download", "subject"=>"Aviz BRAC-MEGA240176", "download"=>{"_aj_globalid"=>"gid://btf3/Download/10652"}, "group"=>{"_aj_globalid"=>"gid://btf3/Organization/10061"}, "_aj_symbol_keys"=>["event_name", "subject", "download", "group"]}, "locale"=>{"_aj_serialized"=>"ActiveJob::Serializers::SymbolSerializer", "value"=>"ro"}, "_aj_symbol_keys"=>["project_id", "email_to", "hash", "locale"]}] (id=212885) (queue=mailers) COMPLETED after 0.9067
|
|
580
|
+
|
|
581
|
+
# Sequence with errors:
|
|
582
|
+
# (two log entries per error)
|
|
583
|
+
#
|
|
584
|
+
# E, [2024-08-15T05:10:30.613623 #4150573] ERROR -- : [ActiveJob] [ActionMailer::Parameterized::DeliveryJob] [79ea42c0-d280-4cf9-b77e-65917d4bc9fc] Error performing ActionMailer::Parameterized::DeliveryJob (Job ID: 79ea42c0-d280-4cf9-b77e-65917d4bc9fc) from DelayedJob(mailers) in 462.62ms: Net::SMTPFatalError (553 Recipient domain not specified.
|
|
585
|
+
|
|
586
|
+
# E, [2024-08-15T05:10:30.614189 #4150573] ERROR -- : 2024-08-15T05:10:30+0200: [Worker(delayed_job host:shair1 pid:4150573)] Job ActionMailer::Parameterized::DeliveryJob [79ea42c0-d280-4cf9-b77e-65917d4bc9fc] from DelayedJob(mailers) with arguments: ["MessageMailer", "build_message", "deliver_now", {"project_id"=>1, "email_to"=>"-", "hash"=>{"event_name"=>"download", "subject"=>"Aviz BvREWE240258.2", "download"=>{"_aj_globalid"=>"gid://btf3/Download/10877"}, "group"=>{"_aj_globalid"=>"gid://btf3/Organization/10060"}, "_aj_symbol_keys"=>["event_name", "subject", "download", "group"]}, "locale"=>{"_aj_serialized"=>"ActiveJob::Serializers::SymbolSerializer", "value"=>"ro"}, "_aj_symbol_keys"=>["project_id", "email_to", "hash", "locale"]}] (id=213242) (queue=mailers) FAILED (22 prior attempts) with Net::SMTPFatalError: 553 Recipient domain not specified.
|
|
427
587
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
588
|
+
TIMESTAMP_WITH_TZONE = '(?<timestamp_tzone>[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\+[0-9]+)'
|
|
589
|
+
ID = '(?<id>[0-9]+)'
|
|
590
|
+
JOB_ID = '(?<job_id>[a-zA-Z0-9-]+)'
|
|
591
|
+
WORKER = 'Worker\\((?<worker>.+) host:(?<host>.+) pid:(?<pid>[0-9]+)\\)'
|
|
592
|
+
METHOD = '(?<method>[A-Za-z0-9:#_]+)'
|
|
593
|
+
TIMES = '(?<attempt>[0-9]+)'
|
|
594
|
+
ERROR_MSG = '(?<error_msg>.+)'
|
|
595
|
+
ARGUMENTS = '(?<arguments>.+)'
|
|
596
|
+
|
|
437
597
|
#
|
|
438
|
-
#
|
|
439
|
-
#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
598
|
+
# these are together, since they return temporary data
|
|
599
|
+
#
|
|
600
|
+
ENQUEUEING = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{LOG_ID}\] \[ActiveJob\] Enqueued #{METHOD} \(Job ID: #{JOB_ID}\) to .* with arguments: #{ARGUMENTS}/o
|
|
601
|
+
|
|
602
|
+
def match_and_process_enqueuing_job(line)
|
|
603
|
+
matchdata = ENQUEUEING.match line
|
|
604
|
+
if matchdata
|
|
605
|
+
{
|
|
606
|
+
log_id: matchdata[:log_id],
|
|
607
|
+
job_id: matchdata[:job_id]
|
|
608
|
+
}
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
RUNNING_MESSAGE = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : #{TIMESTAMP_WITH_TZONE}: \[#{WORKER}\] Job #{METHOD} \[#{JOB_ID}\] from .+ with arguments: \[#{ARGUMENTS}\] \(id=#{ID}\) \(queue=.*\) RUNNING/o
|
|
613
|
+
|
|
614
|
+
def match_and_process_running_job(line)
|
|
615
|
+
matchdata = RUNNING_MESSAGE.match line
|
|
453
616
|
if matchdata
|
|
454
617
|
{
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
618
|
+
started_at: matchdata[:timestamp],
|
|
619
|
+
job_id: matchdata[:job_id],
|
|
620
|
+
object_id: matchdata[:id],
|
|
621
|
+
pid: matchdata[:pid]
|
|
458
622
|
}
|
|
459
623
|
end
|
|
460
624
|
end
|
|
461
625
|
|
|
626
|
+
COMPLETED_MESSAGE = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : #{TIMESTAMP_WITH_TZONE}: \[#{WORKER}\] Job #{METHOD} \[#{JOB_ID}\] from .+ with arguments: \[#{ARGUMENTS}\] \(id=#{ID}\) \(queue=.*\) COMPLETED after (?<duration_total_ms>#{MSECS})/o
|
|
627
|
+
|
|
628
|
+
def match_and_process_completed_job(line)
|
|
629
|
+
matchdata = COMPLETED_MESSAGE.match line
|
|
630
|
+
if matchdata
|
|
631
|
+
{
|
|
632
|
+
ended_at: matchdata[:timestamp],
|
|
633
|
+
duration_total_ms: matchdata[:duration_total_ms],
|
|
634
|
+
id: matchdata[:id],
|
|
635
|
+
job_id: matchdata[:job_id],
|
|
636
|
+
worker: matchdata[:worker],
|
|
637
|
+
host: matchdata[:host],
|
|
638
|
+
pid: matchdata[:pid],
|
|
639
|
+
log_id: matchdata.named_captures["log_id"],
|
|
640
|
+
object_id: matchdata[:id],
|
|
641
|
+
method: matchdata[:method],
|
|
642
|
+
exit_status: 'C',
|
|
643
|
+
arguments: matchdata[:arguments]
|
|
644
|
+
}
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# similar to completed with I->E, INFO->ERROR, COMPLETED->FAILED and final message structure a bit different
|
|
649
|
+
ERROR_MESSAGE_PERMANENT = /E, \[#{TIMESTAMP} #[0-9]+\] ERROR -- : #{TIMESTAMP_WITH_TZONE}: \[#{WORKER}\] Job #{METHOD} \[#{JOB_ID}\] from .+ with arguments: \[#{ARGUMENTS}\] \(id=#{ID}\) \(queue=.*\) (?<error_msg>FAILED permanently because of #{TIMES} consecutive failures)/o
|
|
650
|
+
|
|
651
|
+
ERROR_MESSAGE = /E, \[#{TIMESTAMP} #[0-9]+\] ERROR -- : #{TIMESTAMP_WITH_TZONE}: \[#{WORKER}\] Job #{METHOD} \[#{JOB_ID}\] from .+ with arguments: \[#{ARGUMENTS}\] \(id=#{ID}\) \(queue=.*\) FAILED \(#{TIMES} prior attempts\) with #{ERROR_MSG}/o
|
|
652
|
+
|
|
653
|
+
ERROR_MESSAGE_SHORT = /E, \[#{TIMESTAMP} #[0-9]+\] ERROR -- : #{TIMESTAMP_WITH_TZONE}: \[#{WORKER}\] Job #{METHOD} \(id=#{ID}\) FAILED \(#{TIMES} prior attempts\) with #{ERROR_MSG}/o
|
|
654
|
+
|
|
655
|
+
def match_and_process_job_error(line)
|
|
656
|
+
[ERROR_MESSAGE_PERMANENT, ERROR_MESSAGE, ERROR_MESSAGE_SHORT].map do |regexp|
|
|
657
|
+
matchdata = regexp.match line
|
|
658
|
+
if matchdata
|
|
659
|
+
exit_status = matchdata[:error_msg].include? "permanently" ? "F" : "E"
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
ended_at: matchdata[:timestamp],
|
|
663
|
+
duration_total_ms: nil, # we could compute the time to failure
|
|
664
|
+
worker: matchdata[:worker],
|
|
665
|
+
host: matchdata[:host],
|
|
666
|
+
pid: matchdata[:pid],
|
|
667
|
+
job_id: matchdata.named_captures["job_id"],
|
|
668
|
+
object_id: matchdata[:id],
|
|
669
|
+
method: matchdata[:method],
|
|
670
|
+
arguments: matchdata.named_captures["arguments"],
|
|
671
|
+
exit_status: 'E',
|
|
672
|
+
attempt: matchdata[:attempt],
|
|
673
|
+
error_msg: matchdata[:error_msg],
|
|
674
|
+
}
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
nil
|
|
679
|
+
end
|
|
680
|
+
|
|
462
681
|
# generate a unique visitor id from an event
|
|
463
682
|
def unique_visitor_id(event)
|
|
464
683
|
date = event[:started_at] || event[:ended_at] || "1970-01-01"
|