onlylogs 0.9.0 → 0.11.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.
@@ -1,11 +1,32 @@
1
1
  <style>
2
2
  .onlylogs-log-container {
3
+ --ol-surface: #ffffff;
4
+ --ol-toolbar-bg: #fbfcfe;
5
+ --ol-border: #dde3ec;
6
+ --ol-control-border: #b9c4d3;
7
+ --ol-hover: #eef2f8;
8
+ --ol-ink: #12171f;
9
+ --ol-ink-2: #2c3441;
10
+ --ol-ink-3: #58626f;
11
+ --ol-muted: #8a95a3;
12
+ --ol-faint: #97a1af;
13
+ --ol-accent: #2563eb;
14
+ --ol-accent-soft: #e8f0fe;
15
+ --ol-accent-border: #9dbcf7;
16
+ --ol-accent-ink: #1a4fc4;
17
+ --ol-live: #16a34a;
18
+ --ol-alert: #dc2626;
19
+ --ol-amber: #b7791f;
20
+ --ol-radius: 3px;
21
+
3
22
  background-color: var(--onlylogs-bg-color, none);
4
23
  width: var(--onlylogs-width, 100%);
5
24
  height: var(--onlylogs-height, 100%); /* Take full height of parent container */
6
25
  min-height: var(--onlylogs-min-height, 400px);
7
26
  display: grid;
8
- grid-template-rows: auto var(--onlylogs-toolbar-height, 80px);
27
+ /* The toolbar sizes itself so a wrapping row can never clip; the log pane
28
+ takes whatever is left. Hosts can still pin a height with the variable. */
29
+ grid-template-rows: 1fr var(--onlylogs-toolbar-height, auto);
9
30
 
10
31
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
11
32
  font-size: var(--onlylogs-font-size, 0.8rem);
@@ -18,6 +39,9 @@
18
39
  .onlylogs-log-lines {
19
40
  overflow-y: auto;
20
41
  overflow-x: auto;
42
+ background-color: var(--ol-surface);
43
+ color: var(--ol-ink-2);
44
+ padding: 0.35rem 0.2rem;
21
45
 
22
46
  pre {
23
47
  overflow: visible;
@@ -45,14 +69,6 @@
45
69
  min-width: max-content;
46
70
  }
47
71
 
48
- .color-success {
49
- color: green;
50
- }
51
-
52
- .color-error {
53
- color: red;
54
- }
55
-
56
72
  .fw-bold {
57
73
  font-weight: 600;
58
74
  }
@@ -91,7 +107,7 @@
91
107
 
92
108
  pre {
93
109
  margin: 0 !important;
94
- padding: 0.2rem;
110
+ padding: 0.1rem 0.45rem;
95
111
  word-break: break-word; /* allow breaking long tokens like UUIDs/SQL */
96
112
  white-space: pre;
97
113
 
@@ -142,21 +158,111 @@
142
158
 
143
159
  .onlylogs-log-toolbar {
144
160
  flex-shrink: 0;
145
- padding: 0.5rem;
161
+ padding: 0.5rem 0.65rem;
146
162
  display: flex;
147
- flex-wrap: wrap;
148
- align-items: center;
149
- gap: 0.75rem;
163
+ flex-direction: column;
164
+ gap: 0.4rem;
165
+ background-color: var(--ol-toolbar-bg);
166
+ border-top: 1px solid var(--ol-border);
167
+ color: var(--ol-ink-3);
168
+ }
169
+
170
+ /* Controls on the first row, state on the second, so what you can change
171
+ never has to compete for space with what the viewer is telling you. */
172
+ .onlylogs-toolbar-row {
173
+ display: flex;
174
+ flex-wrap: wrap;
175
+ align-items: center;
176
+ gap: 0.75rem;
177
+ min-height: 1.6rem;
178
+ }
179
+
180
+ .onlylogs-toolbar-row--status {
181
+ gap: 0.4rem;
182
+ }
183
+
184
+ .onlylogs-toolbar-row__end {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 0.5rem;
188
+ margin-left: auto;
189
+ }
150
190
 
191
+ .status-message:not(:empty)::before {
192
+ content: "· ";
193
+ }
151
194
 
195
+ /* The toolbar is a different shape in each mode: the range slider is
196
+ meaningless while tailing, autoscroll is meaningless while searching. */
197
+ &[data-log-streamer-mode-value="live"] [data-onlylogs-mode="static"],
198
+ &[data-log-streamer-mode-value="static"] [data-onlylogs-mode="live"] {
199
+ display: none;
200
+ }
152
201
 
202
+ .onlylogs-mode-switch {
203
+ display: inline-flex;
204
+ border: 1px solid var(--ol-control-border);
205
+ border-radius: 4px;
206
+ overflow: hidden;
207
+ background-color: var(--ol-surface);
208
+ flex-shrink: 0;
209
+ }
210
+
211
+ .onlylogs-mode-switch__button {
212
+ font: inherit;
213
+ font-weight: 500;
214
+ border: 0;
215
+ background: transparent;
216
+ color: var(--ol-ink-3);
217
+ padding: 0.25rem 0.6rem;
218
+ cursor: pointer;
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 0.3rem;
222
+ transition: background-color 0.15s, color 0.15s;
223
+
224
+ & + & {
225
+ border-left: 1px solid var(--ol-border);
226
+ }
227
+
228
+ &:hover {
229
+ background-color: var(--ol-hover);
230
+ }
231
+
232
+ &[aria-pressed="true"] {
233
+ color: #fff;
234
+ }
235
+ }
236
+
237
+ &[data-log-streamer-mode-value="live"] .onlylogs-mode-switch__button[data-onlylogs-mode-button="live"] {
238
+ background-color: var(--ol-live);
239
+ }
240
+
241
+ &[data-log-streamer-mode-value="static"] .onlylogs-mode-switch__button[data-onlylogs-mode-button="static"] {
242
+ background-color: var(--ol-accent);
243
+ }
244
+
245
+ .search-whole-file-button,
246
+ .reload-button {
247
+ font: inherit;
248
+ border: 1px solid var(--ol-accent-border);
249
+ background-color: var(--ol-accent-soft);
250
+ color: var(--ol-accent-ink);
251
+ border-radius: var(--ol-radius);
252
+ padding: 0.22rem 0.5rem;
253
+ cursor: pointer;
254
+ transition: background-color 0.15s;
255
+
256
+ &:hover {
257
+ background-color: #d7e5fd;
258
+ }
153
259
  }
154
260
  .clear-filter-button {
155
261
  position: absolute;
156
- right: 0.25rem;
262
+ right: 0.2rem;
157
263
  background: none;
158
264
  border: none;
159
- color: #666;
265
+ color: var(--ol-muted);
160
266
  cursor: pointer;
161
267
  font-size: 0.8rem;
162
268
  padding: 0;
@@ -175,16 +281,15 @@
175
281
  }
176
282
 
177
283
  .grep-warning {
178
- color: #f59e0b;
284
+ color: var(--ol-amber);
179
285
  font-size: 1.2em;
180
286
  cursor: help;
181
- margin-left: auto;
182
287
  }
183
288
 
184
289
  .file-size {
185
- color: #666;
290
+ color: var(--ol-muted);
186
291
  cursor: help;
187
- margin-left: auto;
292
+ white-space: nowrap;
188
293
  }
189
294
 
190
295
  .websocket-status {
@@ -192,17 +297,17 @@
192
297
  border-radius: 50%;
193
298
  background-color: rgba(255, 255, 255, 0.1);
194
299
 
195
- &--connected {
300
+ &.websocket-status--connected {
196
301
  background-color: rgba(34, 197, 94, 0.2);
197
302
  animation: pulse 2s infinite;
198
303
  }
199
-
200
- &--disconnected {
304
+
305
+ &.websocket-status--disconnected {
201
306
  background-color: rgba(239, 68, 68, 0.2);
202
307
  animation: pulse 2s infinite;
203
308
  }
204
-
205
- &--rejected {
309
+
310
+ &.websocket-status--rejected {
206
311
  background-color: rgba(245, 158, 11, 0.2);
207
312
  animation: pulse 2s infinite;
208
313
  }
@@ -214,11 +319,13 @@
214
319
  }
215
320
 
216
321
  .stop-search-button {
217
- background-color: #dc2626;
322
+ font: inherit;
323
+ background-color: var(--ol-alert);
218
324
  color: white;
219
- border: none;
220
- padding: 0.25rem 0.5rem;
221
- border-radius: 0.25rem;
325
+ border: 1px solid var(--ol-alert);
326
+ padding: 0.22rem 0.5rem;
327
+ border-radius: var(--ol-radius);
328
+ cursor: pointer;
222
329
  transition: background-color 0.2s;
223
330
 
224
331
  &:hover {
@@ -253,6 +360,10 @@
253
360
  /* Toolbar utilities */
254
361
  .toolbar-label {
255
362
  margin-bottom: 0;
363
+ display: inline-flex;
364
+ align-items: center;
365
+ gap: 0.35rem;
366
+ color: var(--ol-ink-3);
256
367
  }
257
368
 
258
369
  .filter-input-wrapper {
@@ -262,61 +373,133 @@
262
373
  }
263
374
 
264
375
  .filter-input-wrapper input {
265
- padding-right: 1.5rem;
266
- }
376
+ font: inherit;
377
+ padding: 0.22rem 1.4rem 0.22rem 0.4rem;
378
+ border: 1px solid var(--ol-control-border);
379
+ border-radius: var(--ol-radius);
380
+ background-color: var(--ol-surface);
381
+ color: var(--ol-ink);
382
+ width: 11rem;
383
+
384
+ &::placeholder {
385
+ color: var(--ol-muted);
386
+ }
267
387
 
268
- .clear-button-wrapper {
269
- width: 67px;
388
+ &:focus-visible {
389
+ outline: 2px solid var(--ol-accent);
390
+ outline-offset: 1px;
391
+ }
270
392
  }
271
393
 
272
394
  .results-text {
273
- color: #666;
395
+ color: var(--ol-ink-3);
396
+ white-space: nowrap;
397
+ font-variant-numeric: tabular-nums;
398
+
399
+ &.results-text--watching {
400
+ color: var(--ol-amber);
401
+ }
402
+
403
+ &.results-text--live,
404
+ &.results-text--found {
405
+ color: var(--ol-live);
406
+ }
407
+ }
408
+
409
+ /* The finished/watching state colours the whole status line, not just the count. */
410
+ .onlylogs-toolbar-row--status:has(.results-text--watching) .status-message {
411
+ color: var(--ol-amber);
412
+ }
413
+
414
+ .onlylogs-toolbar-row--status:has(.results-text--found) .status-message,
415
+ .onlylogs-toolbar-row--status:has(.results-text--live) .status-message {
416
+ color: var(--ol-live);
274
417
  }
275
418
 
276
419
  .hidden-button {
277
420
  display: none;
278
421
  }
279
422
 
423
+ /* Sits over the (empty) log pane while search has no query. */
424
+ .onlylogs-search-placeholder {
425
+ /* Shares the log pane's grid cell rather than being positioned over
426
+ the container, which would also cover the toolbar. */
427
+ grid-row: 1;
428
+ grid-column: 1;
429
+ display: flex;
430
+ flex-direction: column;
431
+ align-items: center;
432
+ justify-content: center;
433
+ gap: 0.35rem;
434
+ text-align: center;
435
+ padding: 1rem;
436
+ color: var(--ol-muted);
437
+ background-color: var(--ol-surface);
438
+ pointer-events: none;
439
+
440
+ &[hidden] {
441
+ display: none;
442
+ }
443
+ }
444
+
445
+ .onlylogs-search-placeholder__title {
446
+ margin: 0;
447
+ font-weight: 600;
448
+ color: var(--ol-ink-3);
449
+ }
450
+
451
+ .onlylogs-search-placeholder__hint {
452
+ margin: 0;
453
+ max-width: 32rem;
454
+ }
455
+
456
+ /* Claims whatever the row has left over: a wider track is a finer
457
+ scrubber over the byte range. */
280
458
  .range-slider-wrapper {
281
459
  display: flex;
282
460
  align-items: center;
283
461
  gap: 0.5rem;
462
+ flex: 1 1 250px;
284
463
  min-width: 250px;
285
- flex-shrink: 0;
464
+ max-width: 30rem;
286
465
  }
287
466
 
288
467
  .range-slider__container {
289
468
  display: flex;
290
469
  align-items: center;
291
- gap: 0.4rem;
470
+ gap: 0.45rem;
292
471
  flex: 1;
293
- height: 1.5rem;
294
472
  }
295
473
 
296
474
  .range-slider__label {
297
- font-size: 0.75rem;
298
475
  font-weight: 500;
299
476
  white-space: nowrap;
300
477
  flex-shrink: 0;
301
478
  }
302
479
 
480
+ /* Track and percentages sit on one line so the group is the same height
481
+ as every other control and centres on the toolbar's baseline. */
303
482
  .range-slider__slider {
304
- display: grid;
305
- gap: 0.15rem;
483
+ display: flex;
484
+ align-items: center;
485
+ gap: 0.45rem;
306
486
  flex: 1;
307
487
  }
308
488
 
309
489
  .range-slider__values {
310
490
  display: flex;
311
491
  gap: 0.15rem;
312
- font-size: 0.6rem;
492
+ color: var(--ol-muted);
313
493
  font-variant-numeric: tabular-nums;
314
- justify-content: center;
494
+ white-space: nowrap;
495
+ flex-shrink: 0;
315
496
  }
316
497
 
317
498
  .range-slider__track {
318
499
  position: relative;
319
500
  height: var(--range-thumb-size);
501
+ flex: 1;
502
+ min-width: 5rem;
320
503
  }
321
504
 
322
505
  .range-slider__track::before,
@@ -376,11 +559,11 @@
376
559
  }
377
560
 
378
561
  .reset-range-button {
379
- background-color: transparent;
380
- color: #666;
381
- border: 1px solid #ccc;
562
+ background-color: var(--ol-surface);
563
+ color: var(--ol-ink-2);
564
+ border: 1px solid var(--ol-control-border);
382
565
  padding: 0.3rem 0.5rem;
383
- border-radius: 2px;
566
+ border-radius: var(--ol-radius);
384
567
  cursor: pointer;
385
568
  font-size: 0.8rem;
386
569
  transition: background-color 0.2s;
@@ -390,13 +573,28 @@
390
573
  flex-shrink: 0;
391
574
 
392
575
  &:hover {
393
- background-color: #f0f0f0;
576
+ background-color: var(--ol-hover);
394
577
  }
395
578
 
396
579
  &:active {
397
- background-color: #e0e0e0;
580
+ background-color: #e0e6ee;
581
+ }
582
+ }
583
+
584
+ .clear-logs-button {
585
+ font: inherit;
586
+ line-height: 1;
587
+ background-color: var(--ol-surface);
588
+ color: var(--ol-ink-2);
589
+ border: 1px solid var(--ol-control-border);
590
+ border-radius: var(--ol-radius);
591
+ padding: 0.22rem 0.5rem;
592
+ cursor: pointer;
593
+ transition: background-color 0.15s;
594
+
595
+ &:hover {
596
+ background-color: var(--ol-hover);
398
597
  }
399
598
  }
400
599
  }
401
- }
402
600
  </style>
@@ -1,8 +1,10 @@
1
1
  <%# app/views/onlylogs/shared/_range_slider.html.erb %>
2
- <%# locals: (start_name:, end_name:, start_value:, end_value:, min: 0, max: 100, step: 1, label: nil, show_reset: false) %>
2
+ <%# locals: (start_name:, end_name:, start_value:, end_value:, min: 0, max: 100, step: 1, label: nil) %>
3
3
 
4
4
  <div class="range-slider range-slider--double"
5
+ data-controller="range-slider"
5
6
  data-log-streamer-target="rangeSliderContainer"
7
+ data-action="range-slider:change->log-streamer#handleRangeUpdate range-slider:refresh->range-slider#refresh"
6
8
  style="--range-min: <%= min %>; --range-max: <%= max %>; --range-start: <%= start_value %>; --range-end: <%= end_value %>;">
7
9
 
8
10
  <div class="range-slider__container">
@@ -24,7 +26,8 @@
24
26
  step="<%= step %>"
25
27
  value="<%= start_value %>"
26
28
  data-log-streamer-target="startSlider"
27
- data-action="input->log-streamer#updateRangeVisuals input->range-slider#updateVisuals change->log-streamer#updateRangeVisuals change->range-slider#updateVisuals">
29
+ data-range-slider-target="startInput"
30
+ data-action="input->range-slider#updateVisuals change->range-slider#updateVisuals">
28
31
 
29
32
  <input class="range-slider__input range-slider__input--end"
30
33
  type="range"
@@ -34,13 +37,14 @@
34
37
  step="<%= step %>"
35
38
  value="<%= end_value %>"
36
39
  data-log-streamer-target="endSlider"
37
- data-action="input->log-streamer#updateRangeVisuals input->range-slider#updateVisuals change->log-streamer#updateRangeVisuals change->range-slider#updateVisuals">
40
+ data-range-slider-target="endInput"
41
+ data-action="input->range-slider#updateVisuals change->range-slider#updateVisuals">
38
42
  </div>
39
43
 
40
44
  <div class="range-slider__values">
41
- <output data-log-streamer-target="startOutput"><%= start_value %></output>
45
+ <output data-range-slider-target="startOutput"></output>
42
46
  <span>–</span>
43
- <output data-log-streamer-target="endOutput"><%= end_value %></output>
47
+ <output data-range-slider-target="endOutput"></output>
44
48
  </div>
45
49
  </div>
46
50
  </div>
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "securerandom"
5
+
6
+ module Onlylogs
7
+ # Appends realistic fake Rails request logs to a file, forever, for manual
8
+ # testing of live streaming and of the UI.
9
+ #
10
+ # Each "log" is a full request: Started / Processing / Parameters / SQL /
11
+ # Completed, with the ANSI colors ActiveRecord and ActionView emit in
12
+ # development, prefixed the way Onlylogs::Logger with
13
+ # `config.log_tags = [:request_id]` prefixes lines.
14
+ #
15
+ # Onlylogs::ContinuousLogWriter.new("log/development.log", logs_per_batch: 5, interval: 1).call
16
+ #
17
+ # This is a development tool. It is deliberately plain Ruby (no Rails, no
18
+ # ActiveSupport) so it can run without booting an app, and it is not required
19
+ # by `onlylogs.rb` — require it explicitly where you need it.
20
+ class ContinuousLogWriter
21
+ # [method, path, controller, action, format, model]
22
+ ROUTES = [
23
+ ["GET", "/workflow_events", "WorkflowEventsController", "index", "html", "WorkflowEvent"],
24
+ ["GET", "/materials/search?q=steel", "MaterialsController", "search", "html", "Material"],
25
+ ["GET", "/projects/42", "ProjectsController", "show", "html", "Project"],
26
+ ["POST", "/sessions", "SessionsController", "create", "html", "User"],
27
+ ["POST", "/api/logs", "Api::LogsController", "ingest", "json", "LogEntry"],
28
+ ["GET", "/dashboard", "DashboardController", "index", "html", "Widget"]
29
+ ].freeze
30
+
31
+ STATUSES = [200, 200, 200, 200, 302, 404, 500].freeze
32
+
33
+ STATUS_TEXTS = {
34
+ 200 => "OK", 302 => "Found", 404 => "Not Found", 500 => "Internal Server Error"
35
+ }.freeze
36
+
37
+ attr_reader :counter
38
+
39
+ def initialize(path, logs_per_batch: 1, interval: 2, out: $stdout)
40
+ @path = path.to_s
41
+ @logs_per_batch = logs_per_batch.to_i
42
+ @interval = interval.to_f
43
+ @out = out
44
+ @counter = 0
45
+ end
46
+
47
+ # Runs until interrupted (Ctrl+C).
48
+ def call
49
+ loop do
50
+ @logs_per_batch.times { write_request }
51
+ sleep @interval
52
+ end
53
+ rescue Interrupt
54
+ @out.puts "\nStopped after #{@counter} line(s)."
55
+ end
56
+
57
+ # All the lines of one request, each in the onlylogs format:
58
+ # [request_id] [ISO8601] [severity] <what Rails logs in development>
59
+ def build_request_lines(status: STATUSES.sample)
60
+ method, path, controller, action, format, model = ROUTES.sample
61
+ request_id = SecureRandom.uuid
62
+ time = Time.now
63
+
64
+ messages = request_messages(method, path, controller, action, format, model, status, time)
65
+ messages.map { |severity, text| "[#{request_id}] [#{time.iso8601}] [#{severity}] #{text}" }
66
+ end
67
+
68
+ private
69
+
70
+ BOLD = "\e[1m"
71
+ CLEAR = "\e[0m"
72
+ COLORS = {red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37}.freeze
73
+
74
+ def paint(text, color, bold: true)
75
+ prefix = bold ? BOLD : ""
76
+ "#{prefix}\e[#{COLORS.fetch(color)}m#{text}#{CLEAR}"
77
+ end
78
+
79
+ def request_messages(method, path, controller, action, format, model, status, time)
80
+ db = (rand * 20 + 0.5).round(1)
81
+ views = (rand * 40 + 1).round(1)
82
+ total = (db + views + rand * 15).round
83
+
84
+ messages = []
85
+ messages << ["I", %(Started #{method} "#{path}" for 127.0.0.1 at #{time})]
86
+ messages << ["I", "Processing by #{controller}##{action} as #{format.upcase}"]
87
+ messages << ["I", " Parameters: #{parameters(method, path)}"] if method == "POST" || path.include?("?")
88
+ messages.concat(sql_messages(method, model, db, status))
89
+ messages.concat(render_messages(controller, action, format, views)) if status != 500
90
+ messages << ["I", completed(status, total, views, db)]
91
+ messages.concat(error_messages(controller, action, model)) if status == 500
92
+ messages
93
+ end
94
+
95
+ def parameters(method, path)
96
+ if method == "POST"
97
+ %({"authenticity_token" => "[FILTERED]", "session" => {"email" => "user@example.com", "password" => "[FILTERED]"}})
98
+ else
99
+ query = path.split("?").last
100
+ key, value = query.split("=")
101
+ %({"#{key}" => "#{value}"})
102
+ end
103
+ end
104
+
105
+ def sql_messages(method, model, db, status)
106
+ table = "#{model.gsub(/([a-z])([A-Z])/, '\1_\2').downcase}s"
107
+ messages = []
108
+ messages << ["D", sql("#{model} Load (#{(db / 2).round(1)}ms)", :cyan,
109
+ %(SELECT "#{table}".* FROM "#{table}" WHERE "#{table}"."id" = $1 LIMIT $2), :blue)]
110
+ messages << ["D", sql("User Load (#{(rand * 2).round(1)}ms)", :cyan,
111
+ %(SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 /*action='show'*/), :magenta)]
112
+
113
+ if method == "POST" && status < 400
114
+ messages << ["D", sql("TRANSACTION (0.1ms)", :yellow, "BEGIN", :blue)]
115
+ messages << ["D", sql("#{model} Create (#{(db / 2).round(1)}ms)", :green,
116
+ %(INSERT INTO "#{table}" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"), :magenta)]
117
+ messages << ["D", sql("TRANSACTION (0.3ms)", :yellow, "COMMIT", :blue)]
118
+ end
119
+ messages
120
+ end
121
+
122
+ def sql(name, name_color, statement, statement_color)
123
+ " #{paint(name, name_color)} #{paint(statement, statement_color)}"
124
+ end
125
+
126
+ def render_messages(controller, action, format, views)
127
+ return [] unless format == "html"
128
+
129
+ template = "#{controller.sub("Controller", "").gsub(/([a-z])([A-Z])/, '\1_\2').downcase}/#{action}.html.erb"
130
+ [
131
+ ["D", " Rendering layout layouts/application.html.erb"],
132
+ ["D", " Rendering #{template} within layouts/application"],
133
+ ["I", " Rendered #{template} within layouts/application (Duration: #{views}ms | GC: 0.2ms)"]
134
+ ]
135
+ end
136
+
137
+ def completed(status, total, views, db)
138
+ breakdown = (status == 500) ? "ActiveRecord: #{db}ms" : "Views: #{views}ms | ActiveRecord: #{db}ms"
139
+ "Completed #{status} #{STATUS_TEXTS.fetch(status)} in #{total}ms (#{breakdown} | Allocations: #{rand(5_000..40_000)})"
140
+ end
141
+
142
+ def error_messages(controller, action, model)
143
+ variable = model.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
144
+ [
145
+ ["F", ""],
146
+ ["F", paint("NoMethodError (undefined method 'name' for nil):", :red)],
147
+ ["F", ""],
148
+ ["F", "app/models/#{variable}.rb:42:in '#{model}#display_name'"],
149
+ ["F", "app/controllers/#{controller.gsub(/([a-z])([A-Z])/, '\1_\2').downcase}.rb:18:in '#{controller}##{action}'"]
150
+ ]
151
+ end
152
+
153
+ def write_request
154
+ lines = build_request_lines
155
+ ::File.open(@path, "a") { |f| lines.each { |line| f.puts(line) } }
156
+ @counter += lines.size
157
+ summary = lines.first[/Started .*?" /] || lines.first[0, 60]
158
+ @out.puts "✓ #{@counter} lines: #{summary}(#{lines.size} lines)"
159
+ end
160
+ end
161
+ end
@@ -10,11 +10,12 @@ module Onlylogs
10
10
  end
11
11
 
12
12
  def call(severity, time, progname, msg)
13
- return nil if "Onlylogs::LogsChannel".in?(msg)
14
- return nil if denylist.any? { |pattern| pattern.match?(msg) }
13
+ text = msg2str(msg)
14
+ return nil if text.include?("Onlylogs::LogsChannel")
15
+ return nil if denylist.any? { |pattern| pattern.match?(text) }
15
16
  tags = [time.iso8601, severity[0].upcase]
16
17
  push_tags tags
17
- str = super
18
+ str = super(severity, time, progname, text)
18
19
  pop_tags tags.size
19
20
  str
20
21
  end