onlylogs 0.5.2 → 0.7.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,18 @@
1
- <%# locals: (log_file_path:, tail: 100, filter: "", autoscroll: true, regexp_mode: false) %>
1
+ <%# locals: (log_file_path:) %>
2
2
  <script nonce="<%= request.content_security_policy_nonce %>"
3
- src="https://cdn.jsdelivr.net/npm/clusterize.js@0.18.1/clusterize.min.js"></script>
3
+ src="<%= asset_path('onlylogs/clusterize.js') %>"></script>
4
+ <link rel="stylesheet" href="<%= asset_path('onlylogs/clusterize.css') %>" />
4
5
  <%= render "onlylogs/shared/log_container_styles" %>
5
6
 
6
7
  <%
7
- mode = filter.blank? ? "live" : "search"
8
- cursor_position = mode == "search" ? 0 : [File.size(log_file_path) - (tail * 100), 0].max
8
+ autoscroll = local_assigns[:autoscroll] != false
9
+ filter = local_assigns[:filter] || ""
10
+ regexp_mode = local_assigns[:regexp_mode] == true
11
+ mode = local_assigns[:mode] || "live"
12
+ file_size = File.size(log_file_path)
13
+ start_position = local_assigns[:start_position]
14
+ end_position = local_assigns[:end_position]
15
+ byte_offset = request.params[:byte_offset]
9
16
 
10
17
  raise SecurityError, "File path not allowed" unless Onlylogs.file_path_permitted?(log_file_path)
11
18
 
@@ -14,11 +21,13 @@
14
21
 
15
22
  <div data-controller="log-streamer text-selection keyboard-shortcuts"
16
23
  data-log-streamer-file-path-value="<%= encrypted_log_file_path %>"
17
- data-log-streamer-cursor-position-value="<%= cursor_position %>"
24
+ <% if start_position.present? %>data-log-streamer-start-position-value="<%= start_position %>"<% end %>
25
+ <% if end_position.present? %>data-log-streamer-end-position-value="<%= end_position %>"<% end %>
18
26
  data-log-streamer-filter-value="<%= filter %>"
19
27
  data-log-streamer-auto-scroll-value="<%= autoscroll %>"
20
28
  data-log-streamer-regexp-mode-value="<%= regexp_mode %>"
21
29
  data-log-streamer-mode-value="<%= mode %>"
30
+ data-log-streamer-file-size-value="<%= file_size %>"
22
31
  data-action="text-selection:start->log-streamer#pauseForSelection"
23
32
  class="onlylogs-log-container" >
24
33
  <div data-log-streamer-target="logLines" data-text-selection-target="logLines" id="scrollArea" class="onlylogs-log-lines clusterize-scroll">
@@ -28,44 +37,42 @@
28
37
 
29
38
  <button type="button"
30
39
  data-text-selection-target="button"
31
- class="onlylogs-context-menu"
40
+ class="onlylogs-context-menu hidden-button"
32
41
  data-action="click->text-selection#searchSelectedText"
33
- title="Search selected text"
34
- style="display: none;">
42
+ title="Search selected text">
35
43
  🔍 Search
36
44
  </button>
37
45
 
38
46
  <div class="onlylogs-log-toolbar">
39
47
  <div>
40
- <label style="margin-bottom: 0;">
48
+ <label class="toolbar-label">
41
49
  <input id="liveMode" type="checkbox" <%= mode == "live" ? "checked" : "" %> name="liveMode" data-log-streamer-target="liveMode" data-keyboard-shortcuts-target="liveMode" data-action="change->log-streamer#toggleLiveMode">
42
50
  <u>L</u>ive Mode
43
51
  </label>
44
52
  </div>
45
53
  <div>
46
- <label style="margin-bottom: 0;">
54
+ <label class="toolbar-label">
47
55
  <input id="autoscroll" type="checkbox" <%= autoscroll ? "checked" : "" %> name="autoscroll" data-log-streamer-target="autoscroll" data-keyboard-shortcuts-target="autoscroll" data-action="change->log-streamer#toggleAutoScroll">
48
56
  <u>A</u>utoscroll
49
57
  </label>
50
58
  </div>
51
59
  <div>
52
- <label style="margin-bottom: 0;">
60
+ <label class="toolbar-label">
53
61
  <input id="regexpMode" type="checkbox" <%= regexp_mode ? "checked" : "" %> name="regexpMode" data-log-streamer-target="regexpMode" data-text-selection-target="regexpMode" data-action="change->log-streamer#toggleRegexpMode">
54
62
  <u>R</u>egexp Mode
55
63
  </label>
56
64
  </div>
57
65
  <div>
58
- <label style="margin-bottom: 0;">
66
+ <label class="toolbar-label">
59
67
  Filter:
60
- <div style="display: inline-flex; align-items: center; position: relative;">
68
+ <div class="filter-input-wrapper">
61
69
  <input type="text"
62
70
  name="filter"
63
71
  value="<%= filter %>"
64
72
  placeholder="Enter filter text..."
65
73
  data-log-streamer-target="filterInput"
66
74
  data-text-selection-target="filterInput"
67
- data-action="input->log-streamer#applyFilter"
68
- style="padding-right: 1.5rem;">
75
+ data-action="input->log-streamer#applyFilter">
69
76
  <button type="button"
70
77
  data-action="click->log-streamer#clearFilter"
71
78
  class="clear-filter-button"
@@ -75,18 +82,34 @@
75
82
  </div>
76
83
  </label>
77
84
  </div>
85
+ <div class="range-slider-wrapper">
86
+ <%= render "onlylogs/shared/range_slider",
87
+ start_name: "start_position",
88
+ end_name: "end_position",
89
+ start_value: 0,
90
+ end_value: file_size,
91
+ min: 0,
92
+ max: file_size,
93
+ step: 1000,
94
+ label: "Range" %>
95
+ <button type="button"
96
+ data-action="click->log-streamer#resetRange"
97
+ class="reset-range-button"
98
+ title="Reset to full file">
99
+
100
+ </button>
101
+ </div>
78
102
  <div>
79
103
  <button type="button"
80
104
  data-log-streamer-target="stopButton"
105
+ class="stop-search-button hidden-button"
81
106
  data-action="click->log-streamer#stopSearch"
82
- class="stop-search-button"
83
- title="Stop current search"
84
- style="display: none;">
107
+ title="Stop current search">
85
108
  ⏹️ Stop
86
109
  </button>
87
110
  </div>
88
111
  <div>
89
- <span data-log-streamer-target="results" style="color: #666;">Results: 0</span>
112
+ <span data-log-streamer-target="results" class="results-text">Results: 0</span>
90
113
  </div>
91
114
  <div data-log-streamer-target="message"></div>
92
115
  <% unless Onlylogs.ripgrep_enabled? %>
@@ -95,11 +118,11 @@
95
118
  <div class="file-size" title="File size: <%= number_to_human_size(File.size(log_file_path)) %>">
96
119
  <%= number_to_human_size(File.size(log_file_path)) %>
97
120
  </div>
98
- <div style="width:67px">
121
+ <div class="clear-button-wrapper">
99
122
  <button type="button"
100
123
  data-log-streamer-target="clearButton"
101
- data-action="click->log-streamer#clearLogs"
102
124
  class="clear-logs-button"
125
+ data-action="click->log-streamer#clearLogs"
103
126
  title="Clear all log lines">
104
127
  🗑️ Clear
105
128
  </button>
@@ -5,7 +5,7 @@
5
5
  height: var(--onlylogs-height, 100%); /* Take full height of parent container */
6
6
  min-height: var(--onlylogs-min-height, 400px);
7
7
  display: grid;
8
- grid-template-rows: auto var(--onlylogs-toolbar-height, 42px);
8
+ grid-template-rows: auto var(--onlylogs-toolbar-height, 80px);
9
9
 
10
10
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
11
11
  font-size: var(--onlylogs-font-size, 0.8rem);
@@ -24,8 +24,25 @@
24
24
  }
25
25
  }
26
26
 
27
+ &[data-log-streamer-mode-value="static"] .onlylogs-expand-btn {
28
+ cursor: pointer;
29
+ padding: 0 0.5rem;
30
+ margin-right: 0.25rem;
31
+ background: none;
32
+ border: none;
33
+ color: #666;
34
+ font-weight: bold;
35
+ font-size: 1.1em;
36
+ line-height: 1;
37
+
38
+ &:hover {
39
+ color: #333;
40
+ }
41
+ }
42
+
27
43
  .clusterize-content {
28
44
  outline: 0;
45
+ min-width: max-content;
29
46
  }
30
47
 
31
48
  .color-success {
@@ -76,6 +93,11 @@
76
93
  margin: 0 !important;
77
94
  padding: 0.2rem;
78
95
  word-break: break-word; /* allow breaking long tokens like UUIDs/SQL */
96
+ white-space: pre;
97
+
98
+ &.highlighted-context-line {
99
+ background-color: rgba(255, 193, 7, 0.3);
100
+ }
79
101
  }
80
102
 
81
103
  a {
@@ -106,7 +128,11 @@
106
128
 
107
129
  &:hover {
108
130
  background-color: #bbdefb;
109
- }
131
+ }
132
+ }
133
+
134
+ .hidden-button {
135
+ display: none;
110
136
  }
111
137
 
112
138
  @keyframes spin {
@@ -116,20 +142,15 @@
116
142
 
117
143
  .onlylogs-log-toolbar {
118
144
  flex-shrink: 0;
119
- padding: 1rem 0.5rem;
120
- display: flex;
121
- align-items: center;
122
- gap: 1.5rem;
145
+ padding: 0.5rem;
146
+ display: flex;
147
+ flex-wrap: wrap;
148
+ align-items: center;
149
+ gap: 0.75rem;
123
150
 
124
- .live-mode-sticky {
125
- opacity: 0.7;
126
- cursor: not-allowed;
127
151
 
128
- input[type="checkbox"] {
129
- cursor: not-allowed;
130
- }
131
- }
132
152
 
153
+ }
133
154
  .clear-filter-button {
134
155
  position: absolute;
135
156
  right: 0.25rem;
@@ -217,6 +238,165 @@
217
238
  border-radius: 0.25rem;
218
239
  display: inline-block;
219
240
  }
241
+
242
+ .range-slider {
243
+ --range-track-height: 0.25rem;
244
+ --range-thumb-size: 0.6rem;
245
+ --range-color: #2563eb;
246
+ --range-bg: #d1d5db;
247
+ --range-start-percent: 0%;
248
+ --range-end-percent: 100%;
249
+
250
+ display: contents;
251
+ }
252
+
253
+ /* Toolbar utilities */
254
+ .toolbar-label {
255
+ margin-bottom: 0;
256
+ }
257
+
258
+ .filter-input-wrapper {
259
+ display: inline-flex;
260
+ align-items: center;
261
+ position: relative;
262
+ }
263
+
264
+ .filter-input-wrapper input {
265
+ padding-right: 1.5rem;
266
+ }
267
+
268
+ .clear-button-wrapper {
269
+ width: 67px;
270
+ }
271
+
272
+ .results-text {
273
+ color: #666;
274
+ }
275
+
276
+ .hidden-button {
277
+ display: none;
278
+ }
279
+
280
+ .range-slider-wrapper {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 0.5rem;
284
+ min-width: 250px;
285
+ flex-shrink: 0;
286
+ }
287
+
288
+ .range-slider__container {
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 0.4rem;
292
+ flex: 1;
293
+ height: 1.5rem;
294
+ }
295
+
296
+ .range-slider__label {
297
+ font-size: 0.75rem;
298
+ font-weight: 500;
299
+ white-space: nowrap;
300
+ flex-shrink: 0;
301
+ }
302
+
303
+ .range-slider__slider {
304
+ display: grid;
305
+ gap: 0.15rem;
306
+ flex: 1;
307
+ }
308
+
309
+ .range-slider__values {
310
+ display: flex;
311
+ gap: 0.15rem;
312
+ font-size: 0.6rem;
313
+ font-variant-numeric: tabular-nums;
314
+ justify-content: center;
315
+ }
316
+
317
+ .range-slider__track {
318
+ position: relative;
319
+ height: var(--range-thumb-size);
320
+ }
321
+
322
+ .range-slider__track::before,
323
+ .range-slider__selected {
324
+ position: absolute;
325
+ top: 50%;
326
+ height: var(--range-track-height);
327
+ transform: translateY(-50%);
328
+ border-radius: 999px;
329
+ }
330
+
331
+ .range-slider__track::before {
332
+ content: "";
333
+ left: 0;
334
+ right: 0;
335
+ background: var(--range-bg);
336
+ }
337
+
338
+ .range-slider__selected {
339
+ left: var(--range-start-percent);
340
+ right: calc(100% - var(--range-end-percent));
341
+ background: var(--range-color);
342
+ }
343
+
344
+ .range-slider__input {
345
+ position: absolute;
346
+ inset: 0;
347
+ width: 100%;
348
+ appearance: none;
349
+ background: none;
350
+ pointer-events: none;
351
+ margin: 0;
352
+ }
353
+
354
+ .range-slider__input::-webkit-slider-thumb {
355
+ appearance: none;
356
+ width: var(--range-thumb-size);
357
+ height: var(--range-thumb-size);
358
+ border-radius: 50%;
359
+ background: var(--range-color);
360
+ cursor: pointer;
361
+ pointer-events: auto;
362
+ position: relative;
363
+ z-index: 2;
364
+ }
365
+
366
+ .range-slider__input::-moz-range-thumb {
367
+ width: var(--range-thumb-size);
368
+ height: var(--range-thumb-size);
369
+ border: 0;
370
+ border-radius: 50%;
371
+ background: var(--range-color);
372
+ cursor: pointer;
373
+ pointer-events: auto;
374
+ position: relative;
375
+ z-index: 2;
376
+ }
377
+
378
+ .reset-range-button {
379
+ background-color: transparent;
380
+ color: #666;
381
+ border: 1px solid #ccc;
382
+ padding: 0.3rem 0.5rem;
383
+ border-radius: 2px;
384
+ cursor: pointer;
385
+ font-size: 0.8rem;
386
+ transition: background-color 0.2s;
387
+ height: 1.5rem;
388
+ display: flex;
389
+ align-items: center;
390
+ flex-shrink: 0;
391
+
392
+ &:hover {
393
+ background-color: #f0f0f0;
394
+ }
395
+
396
+ &:active {
397
+ background-color: #e0e0e0;
398
+ }
399
+ }
220
400
  }
221
401
  }
222
402
  </style>
@@ -0,0 +1,47 @@
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) %>
3
+
4
+ <div class="range-slider range-slider--double"
5
+ data-log-streamer-target="rangeSliderContainer"
6
+ style="--range-min: <%= min %>; --range-max: <%= max %>; --range-start: <%= start_value %>; --range-end: <%= end_value %>;">
7
+
8
+ <div class="range-slider__container">
9
+ <% if label.present? %>
10
+ <label class="range-slider__label">
11
+ <%= label %>
12
+ </label>
13
+ <% end %>
14
+
15
+ <div class="range-slider__slider">
16
+ <div class="range-slider__track">
17
+ <div class="range-slider__selected"></div>
18
+
19
+ <input class="range-slider__input range-slider__input--start"
20
+ type="range"
21
+ name="<%= start_name %>"
22
+ min="<%= min %>"
23
+ max="<%= max %>"
24
+ step="<%= step %>"
25
+ value="<%= start_value %>"
26
+ data-log-streamer-target="startSlider"
27
+ data-action="input->log-streamer#updateRangeVisuals input->range-slider#updateVisuals change->log-streamer#updateRangeVisuals change->range-slider#updateVisuals">
28
+
29
+ <input class="range-slider__input range-slider__input--end"
30
+ type="range"
31
+ name="<%= end_name %>"
32
+ min="<%= min %>"
33
+ max="<%= max %>"
34
+ step="<%= step %>"
35
+ value="<%= end_value %>"
36
+ data-log-streamer-target="endSlider"
37
+ data-action="input->log-streamer#updateRangeVisuals input->range-slider#updateVisuals change->log-streamer#updateRangeVisuals change->range-slider#updateVisuals">
38
+ </div>
39
+
40
+ <div class="range-slider__values">
41
+ <output data-log-streamer-target="startOutput"><%= start_value %></output>
42
+ <span>–</span>
43
+ <output data-log-streamer-target="endOutput"><%= end_value %></output>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
data/bin/super_grep CHANGED
@@ -5,7 +5,10 @@ export LC_ALL=C
5
5
  max_matches=""
6
6
  start_position=""
7
7
  end_position=""
8
- block_size="${BLOCK_SIZE:-8M}"
8
+ # Block size for dd operations. Performance testing shows no significant
9
+ # difference across block sizes (4K-8M) on typical log files. 1M balances
10
+ # memory usage and I/O efficiency.
11
+ block_size="${BLOCK_SIZE:-1M}"
9
12
 
10
13
  while [[ $# -gt 0 ]]; do
11
14
  case "$1" in
data/bin/super_ripgrep CHANGED
@@ -5,7 +5,10 @@ export LC_ALL=C
5
5
  max_matches=""
6
6
  start_position=""
7
7
  end_position=""
8
- block_size="${BLOCK_SIZE:-8M}"
8
+ # Block size for dd operations. Performance testing shows no significant
9
+ # difference across block sizes (4K-8M) on typical log files. 1M balances
10
+ # memory usage and I/O efficiency.
11
+ block_size="${BLOCK_SIZE:-1M}"
9
12
 
10
13
  while [[ $# -gt 0 ]]; do
11
14
  case "$1" in
@@ -56,7 +59,12 @@ actual_color_regex='\x1b\[[0-9;]*m'
56
59
  query_regex="${query_regex//$placeholder/$actual_color_regex}"
57
60
 
58
61
  # Build ripgrep command
59
- rg_cmd="rg --color=never --no-filename"
62
+ # --no-mmap: read the file with read() instead of memory-mapping it. mmap'ing a
63
+ # multi-GB log file maps the whole file into the process and, in a container,
64
+ # charges those pages to the cgroup as active memory, which can trigger an OOM
65
+ # kill. Plain read() pages are ordinary, readily-reclaimable page cache.
66
+ # --byte-offset: include byte offset in output for the logs-around-line feature
67
+ rg_cmd="rg --color=never --no-filename --byte-offset --no-mmap"
60
68
  [ -n "$max_matches" ] && rg_cmd="$rg_cmd --max-count=$max_matches"
61
69
 
62
70
  # Handle byte range if specified
@@ -9,7 +9,7 @@ module Onlylogs
9
9
  isolate_namespace Onlylogs
10
10
 
11
11
  initializer "onlylogs.assets" do |app|
12
- %w[images stylesheets builds fonts].each do |subdir|
12
+ %w[images javascript stylesheets builds fonts].each do |subdir|
13
13
  path = root.join("app/assets", subdir)
14
14
  app.config.assets.paths << path if path.exist?
15
15
  end