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.
@@ -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
- # These are for development logs
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
- event = data.merge(pending[id] || {})
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 = self.match_and_process_completed line
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
- event = data.merge (pending[id] || {})
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
- db
287
- end
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
- # Error Messages
318
- # [584cffcc-f1fd-4b5c-bb8b-b89621bd4921] ActionController::RoutingError (No route matches [GET] "/assets/foundation-icons.svg"):
319
- # [fd8df8b5-83c9-48b5-a056-e5026e31bd5e] ActionView::Template::Error (undefined method `all_my_ancestor' for nil:NilClass):
320
- # [d17ed55c-f5f1-442a-a9d6-3035ab91adf0] ActionView::Template::Error (undefined method `volunteer_for' for #<DonationsController:0x007f4864c564b8>
321
- EXCEPTION = /[A-Za-z_0-9:]+(Error)?/
322
- ERROR_REGEXP = /^\[#{ID}\] (?<context>#{EXCEPTION}) \((?<description>(#{EXCEPTION})?.*)\):/
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
- def match_and_process_error(line)
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 -- : \[#{ID}\] Started #{VERB} "#{URL}" for #{IP} at/
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[:id],
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 -- : \[#{ID}\] Completed #{STATUS} #{STATUS_IN_WORDS} in (?<total>#{MSECS})ms \((Views: (?<views>#{MSECS})ms \| )?ActiveRecord: (?<arec>#{MSECS})ms( \| Allocations: (?<alloc>[0-9]+))?\)/
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[:id],
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 -- : \[#{ID}\] Processing by (?<controller>[^ ]+) as/
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[:id],
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
- FATAL_REGEXP = /F, \[#{TIMESTAMP} #[0-9]+\] FATAL -- : \[#{ID}\] (?<comment>.*)$/
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[:id],
400
- comment: matchdata[:comment]
501
+ log_id: matchdata[:log_id],
401
502
  }
402
503
  end
403
504
  end
404
505
 
405
- # Started GET "/projects?locale=it" for 127.0.0.1 at 2024-06-06 23:23:31 +0200
406
- # Processing by EmployeesController#index as HTML
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
- # Started GET "/serviceworker.js" for 127.0.0.1 at 2024-06-06 23:23:29 +0200
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
- # Started POST "/projects?locale=it" for 127.0.0.1 at 2024-06-06 23:34:33 +0200
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
- # Completed in 48801ms (ActiveRecord: 17.8ms | Allocations: 2274498)
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
- # Completed 500 Internal Server Error in 24ms (ActiveRecord: 1.4ms | Allocations: 4660)
423
- # ActionView::Template::Error (Error: Undefined variable: "$white".
424
- # on line 6:28 of app/assets/stylesheets/_animations.scss
425
- # from line 16:9 of app/assets/stylesheets/application.scss
426
- # >> from { background-color: $white; }
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
- # 9: = csrf_meta_tags
431
- # 10: = csp_meta_tag
432
- # 11:
433
- # 12: = stylesheet_link_tag "application", "data-turbo-track": "reload"
434
- # 13: = javascript_importmap_tags
435
- # 14:
436
- # 15: %body
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
- # app/views/layouts/application.html.haml:12
439
- # app/controllers/application_controller.rb:26:in `switch_locale'
440
-
441
- # Rendered devise/sessions/_project_partial.html.erb (Duration: 78.4ms | Allocations: 88373)
442
- # Rendered devise/sessions/new.html.haml within layouts/application (Duration: 100.0ms | Allocations: 104118)
443
- # Rendered application/_favicon.html.erb (Duration: 2.6ms | Allocations: 4454)
444
- # Rendered layouts/_manage_notice.html.erb (Duration: 0.3ms | Allocations: 193)
445
- # Rendered layout layouts/application.html.erb (Duration: 263.4ms | Allocations: 367467)
446
- # Rendered donations/_switcher.html.haml (Duration: 41.1ms | Allocations: 9550)
447
- # Rendered donations/_status_header.html.haml (Duration: 1.4ms | Allocations: 3192)
448
- # Rendered donations/_status_header.html.haml (Duration: 0.0ms | Allocations: 7)
449
- RENDERED_REGEXP = /^ *Rendered (?<partial>[^ ]+) .*\(Duration: (?<duration>[0-9.]+)ms \| Allocations: (?<allocations>[0-9]+)\)$/
450
-
451
- def match_and_process_rendered(line)
452
- matchdata = RENDERED_REGEXP.match line
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
- partial: matchdata[:partial],
456
- duration: matchdata[:duration],
457
- allocations: matchdata[:allocations]
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"