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.
- checksums.yaml +4 -4
- data/README.md +11 -1
- data/app/channels/onlylogs/logs_channel.rb +82 -70
- data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +15 -11
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +170 -202
- data/app/javascript/onlylogs/controllers/range_slider_controller.js +96 -0
- data/app/javascript/onlylogs/controllers/text_selection_controller.js +1 -0
- data/app/models/onlylogs/grep.rb +14 -6
- data/app/views/onlylogs/logs/index.html.erb +25 -5
- data/app/views/onlylogs/shared/_log_container.html.erb +71 -35
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +246 -49
- data/app/views/onlylogs/shared/_range_slider.html.erb +9 -5
- data/lib/onlylogs/continuous_log_writer.rb +161 -0
- data/lib/onlylogs/formatter.rb +4 -3
- data/lib/onlylogs/http_device.rb +319 -84
- data/lib/onlylogs/http_logger.rb +4 -7
- data/lib/onlylogs/spool.rb +61 -13
- data/lib/onlylogs/version.rb +1 -1
- metadata +3 -1
|
@@ -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
|
-
|
|
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.
|
|
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-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
261
|
+
right: 0.2rem;
|
|
157
262
|
background: none;
|
|
158
263
|
border: none;
|
|
159
|
-
color:
|
|
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:
|
|
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:
|
|
289
|
+
color: var(--ol-muted);
|
|
186
290
|
cursor: help;
|
|
187
|
-
|
|
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
|
-
|
|
299
|
+
&.websocket-status--connected {
|
|
196
300
|
background-color: rgba(34, 197, 94, 0.2);
|
|
197
301
|
animation: pulse 2s infinite;
|
|
198
302
|
}
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
+
font: inherit;
|
|
322
|
+
background-color: var(--ol-alert);
|
|
218
323
|
color: white;
|
|
219
|
-
border:
|
|
220
|
-
padding: 0.
|
|
221
|
-
border-radius:
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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:
|
|
305
|
-
|
|
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
|
-
|
|
491
|
+
color: var(--ol-muted);
|
|
313
492
|
font-variant-numeric: tabular-nums;
|
|
314
|
-
|
|
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:
|
|
380
|
-
color:
|
|
381
|
-
border: 1px solid
|
|
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:
|
|
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:
|
|
575
|
+
background-color: var(--ol-hover);
|
|
394
576
|
}
|
|
395
577
|
|
|
396
578
|
&:active {
|
|
397
|
-
background-color: #
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
45
|
+
<output data-range-slider-target="startOutput"></output>
|
|
42
46
|
<span>–</span>
|
|
43
|
-
<output data-
|
|
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
|
data/lib/onlylogs/formatter.rb
CHANGED
|
@@ -10,11 +10,12 @@ module Onlylogs
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def call(severity, time, progname, msg)
|
|
13
|
-
|
|
14
|
-
return nil if
|
|
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
|