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.
- checksums.yaml +4 -4
- data/README.md +39 -2
- data/app/channels/onlylogs/logs_channel.rb +90 -70
- data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +15 -11
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +177 -203
- 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/file.rb +13 -2
- 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 +84 -37
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +247 -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,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-
|
|
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
|
+
.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.
|
|
262
|
+
right: 0.2rem;
|
|
157
263
|
background: none;
|
|
158
264
|
border: none;
|
|
159
|
-
color:
|
|
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:
|
|
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:
|
|
290
|
+
color: var(--ol-muted);
|
|
186
291
|
cursor: help;
|
|
187
|
-
|
|
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
|
-
|
|
300
|
+
&.websocket-status--connected {
|
|
196
301
|
background-color: rgba(34, 197, 94, 0.2);
|
|
197
302
|
animation: pulse 2s infinite;
|
|
198
303
|
}
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
font: inherit;
|
|
323
|
+
background-color: var(--ol-alert);
|
|
218
324
|
color: white;
|
|
219
|
-
border:
|
|
220
|
-
padding: 0.
|
|
221
|
-
border-radius:
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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:
|
|
305
|
-
|
|
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
|
-
|
|
492
|
+
color: var(--ol-muted);
|
|
313
493
|
font-variant-numeric: tabular-nums;
|
|
314
|
-
|
|
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:
|
|
380
|
-
color:
|
|
381
|
-
border: 1px solid
|
|
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:
|
|
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:
|
|
576
|
+
background-color: var(--ol-hover);
|
|
394
577
|
}
|
|
395
578
|
|
|
396
579
|
&:active {
|
|
397
|
-
background-color: #
|
|
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
|
|
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
|