onlylogs 0.9.0 → 0.10.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,110 @@
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
+ font: inherit;
247
+ border: 1px solid var(--ol-accent-border);
248
+ background-color: var(--ol-accent-soft);
249
+ color: var(--ol-accent-ink);
250
+ border-radius: var(--ol-radius);
251
+ padding: 0.22rem 0.5rem;
252
+ cursor: pointer;
253
+ transition: background-color 0.15s;
254
+
255
+ &:hover {
256
+ background-color: #d7e5fd;
257
+ }
153
258
  }
154
259
  .clear-filter-button {
155
260
  position: absolute;
156
- right: 0.25rem;
261
+ right: 0.2rem;
157
262
  background: none;
158
263
  border: none;
159
- color: #666;
264
+ color: var(--ol-muted);
160
265
  cursor: pointer;
161
266
  font-size: 0.8rem;
162
267
  padding: 0;
@@ -175,16 +280,15 @@
175
280
  }
176
281
 
177
282
  .grep-warning {
178
- color: #f59e0b;
283
+ color: var(--ol-amber);
179
284
  font-size: 1.2em;
180
285
  cursor: help;
181
- margin-left: auto;
182
286
  }
183
287
 
184
288
  .file-size {
185
- color: #666;
289
+ color: var(--ol-muted);
186
290
  cursor: help;
187
- margin-left: auto;
291
+ white-space: nowrap;
188
292
  }
189
293
 
190
294
  .websocket-status {
@@ -192,17 +296,17 @@
192
296
  border-radius: 50%;
193
297
  background-color: rgba(255, 255, 255, 0.1);
194
298
 
195
- &--connected {
299
+ &.websocket-status--connected {
196
300
  background-color: rgba(34, 197, 94, 0.2);
197
301
  animation: pulse 2s infinite;
198
302
  }
199
-
200
- &--disconnected {
303
+
304
+ &.websocket-status--disconnected {
201
305
  background-color: rgba(239, 68, 68, 0.2);
202
306
  animation: pulse 2s infinite;
203
307
  }
204
-
205
- &--rejected {
308
+
309
+ &.websocket-status--rejected {
206
310
  background-color: rgba(245, 158, 11, 0.2);
207
311
  animation: pulse 2s infinite;
208
312
  }
@@ -214,11 +318,13 @@
214
318
  }
215
319
 
216
320
  .stop-search-button {
217
- background-color: #dc2626;
321
+ font: inherit;
322
+ background-color: var(--ol-alert);
218
323
  color: white;
219
- border: none;
220
- padding: 0.25rem 0.5rem;
221
- border-radius: 0.25rem;
324
+ border: 1px solid var(--ol-alert);
325
+ padding: 0.22rem 0.5rem;
326
+ border-radius: var(--ol-radius);
327
+ cursor: pointer;
222
328
  transition: background-color 0.2s;
223
329
 
224
330
  &:hover {
@@ -253,6 +359,10 @@
253
359
  /* Toolbar utilities */
254
360
  .toolbar-label {
255
361
  margin-bottom: 0;
362
+ display: inline-flex;
363
+ align-items: center;
364
+ gap: 0.35rem;
365
+ color: var(--ol-ink-3);
256
366
  }
257
367
 
258
368
  .filter-input-wrapper {
@@ -262,61 +372,133 @@
262
372
  }
263
373
 
264
374
  .filter-input-wrapper input {
265
- padding-right: 1.5rem;
266
- }
375
+ font: inherit;
376
+ padding: 0.22rem 1.4rem 0.22rem 0.4rem;
377
+ border: 1px solid var(--ol-control-border);
378
+ border-radius: var(--ol-radius);
379
+ background-color: var(--ol-surface);
380
+ color: var(--ol-ink);
381
+ width: 11rem;
382
+
383
+ &::placeholder {
384
+ color: var(--ol-muted);
385
+ }
267
386
 
268
- .clear-button-wrapper {
269
- width: 67px;
387
+ &:focus-visible {
388
+ outline: 2px solid var(--ol-accent);
389
+ outline-offset: 1px;
390
+ }
270
391
  }
271
392
 
272
393
  .results-text {
273
- color: #666;
394
+ color: var(--ol-ink-3);
395
+ white-space: nowrap;
396
+ font-variant-numeric: tabular-nums;
397
+
398
+ &.results-text--watching {
399
+ color: var(--ol-amber);
400
+ }
401
+
402
+ &.results-text--live,
403
+ &.results-text--found {
404
+ color: var(--ol-live);
405
+ }
406
+ }
407
+
408
+ /* The finished/watching state colours the whole status line, not just the count. */
409
+ .onlylogs-toolbar-row--status:has(.results-text--watching) .status-message {
410
+ color: var(--ol-amber);
411
+ }
412
+
413
+ .onlylogs-toolbar-row--status:has(.results-text--found) .status-message,
414
+ .onlylogs-toolbar-row--status:has(.results-text--live) .status-message {
415
+ color: var(--ol-live);
274
416
  }
275
417
 
276
418
  .hidden-button {
277
419
  display: none;
278
420
  }
279
421
 
422
+ /* Sits over the (empty) log pane while search has no query. */
423
+ .onlylogs-search-placeholder {
424
+ /* Shares the log pane's grid cell rather than being positioned over
425
+ the container, which would also cover the toolbar. */
426
+ grid-row: 1;
427
+ grid-column: 1;
428
+ display: flex;
429
+ flex-direction: column;
430
+ align-items: center;
431
+ justify-content: center;
432
+ gap: 0.35rem;
433
+ text-align: center;
434
+ padding: 1rem;
435
+ color: var(--ol-muted);
436
+ background-color: var(--ol-surface);
437
+ pointer-events: none;
438
+
439
+ &[hidden] {
440
+ display: none;
441
+ }
442
+ }
443
+
444
+ .onlylogs-search-placeholder__title {
445
+ margin: 0;
446
+ font-weight: 600;
447
+ color: var(--ol-ink-3);
448
+ }
449
+
450
+ .onlylogs-search-placeholder__hint {
451
+ margin: 0;
452
+ max-width: 32rem;
453
+ }
454
+
455
+ /* Claims whatever the row has left over: a wider track is a finer
456
+ scrubber over the byte range. */
280
457
  .range-slider-wrapper {
281
458
  display: flex;
282
459
  align-items: center;
283
460
  gap: 0.5rem;
461
+ flex: 1 1 250px;
284
462
  min-width: 250px;
285
- flex-shrink: 0;
463
+ max-width: 30rem;
286
464
  }
287
465
 
288
466
  .range-slider__container {
289
467
  display: flex;
290
468
  align-items: center;
291
- gap: 0.4rem;
469
+ gap: 0.45rem;
292
470
  flex: 1;
293
- height: 1.5rem;
294
471
  }
295
472
 
296
473
  .range-slider__label {
297
- font-size: 0.75rem;
298
474
  font-weight: 500;
299
475
  white-space: nowrap;
300
476
  flex-shrink: 0;
301
477
  }
302
478
 
479
+ /* Track and percentages sit on one line so the group is the same height
480
+ as every other control and centres on the toolbar's baseline. */
303
481
  .range-slider__slider {
304
- display: grid;
305
- gap: 0.15rem;
482
+ display: flex;
483
+ align-items: center;
484
+ gap: 0.45rem;
306
485
  flex: 1;
307
486
  }
308
487
 
309
488
  .range-slider__values {
310
489
  display: flex;
311
490
  gap: 0.15rem;
312
- font-size: 0.6rem;
491
+ color: var(--ol-muted);
313
492
  font-variant-numeric: tabular-nums;
314
- justify-content: center;
493
+ white-space: nowrap;
494
+ flex-shrink: 0;
315
495
  }
316
496
 
317
497
  .range-slider__track {
318
498
  position: relative;
319
499
  height: var(--range-thumb-size);
500
+ flex: 1;
501
+ min-width: 5rem;
320
502
  }
321
503
 
322
504
  .range-slider__track::before,
@@ -376,11 +558,11 @@
376
558
  }
377
559
 
378
560
  .reset-range-button {
379
- background-color: transparent;
380
- color: #666;
381
- border: 1px solid #ccc;
561
+ background-color: var(--ol-surface);
562
+ color: var(--ol-ink-2);
563
+ border: 1px solid var(--ol-control-border);
382
564
  padding: 0.3rem 0.5rem;
383
- border-radius: 2px;
565
+ border-radius: var(--ol-radius);
384
566
  cursor: pointer;
385
567
  font-size: 0.8rem;
386
568
  transition: background-color 0.2s;
@@ -390,13 +572,28 @@
390
572
  flex-shrink: 0;
391
573
 
392
574
  &:hover {
393
- background-color: #f0f0f0;
575
+ background-color: var(--ol-hover);
394
576
  }
395
577
 
396
578
  &:active {
397
- background-color: #e0e0e0;
579
+ background-color: #e0e6ee;
580
+ }
581
+ }
582
+
583
+ .clear-logs-button {
584
+ font: inherit;
585
+ line-height: 1;
586
+ background-color: var(--ol-surface);
587
+ color: var(--ol-ink-2);
588
+ border: 1px solid var(--ol-control-border);
589
+ border-radius: var(--ol-radius);
590
+ padding: 0.22rem 0.5rem;
591
+ cursor: pointer;
592
+ transition: background-color 0.15s;
593
+
594
+ &:hover {
595
+ background-color: var(--ol-hover);
398
596
  }
399
597
  }
400
598
  }
401
- }
402
599
  </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