onlylogs 0.4.5 → 0.5.1
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 +73 -18
- data/app/controllers/onlylogs/logs_controller.rb +21 -3
- data/app/helpers/onlylogs/application_helper.rb +3 -0
- data/app/javascript/onlylogs/controllers/auto_submit_controller.js +1 -5
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +51 -11
- data/app/javascript/onlylogs/controllers/text_selection_controller.js +8 -1
- data/app/models/onlylogs/secure_file_path.rb +2 -4
- data/app/views/onlylogs/logs/index.html.erb +10 -5
- data/app/views/onlylogs/shared/_log_container.html.erb +5 -3
- data/bin/super_grep +91 -0
- data/bin/super_ripgrep +88 -0
- data/config/routes.rb +1 -0
- data/lib/onlylogs/version.rb +1 -1
- data/lib/onlylogs.rb +2 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7340d11d402202a753fa7a6426c6e7c3338af2b53779951aced223420e250e8c
|
|
4
|
+
data.tar.gz: 6be0ff24e762d27d8929a8cd177b53c0140a181bd5717c803858fcc72436074f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90a6de7fd76c95f4958955f2542a31caaeaafa970ff09b99452a25abb14cb0e801728139d0551056c4d371bb0d53e39a6b80b000e22eea17527a26dc68e2d054
|
|
7
|
+
data.tar.gz: 57e3b9e01bcc10fabeef0a3c11cdccf75f01f6b05843922e678c407db7239cce7d0095408df63017459ec929fa8670f60dae38dff614726f5e47c67ce6d65a43
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ If you already have a disk, you can just keep there also your log files (as well
|
|
|
18
18
|
|
|
19
19
|
This section explains how to setup onlylogs to self host your logs and access them directly from your Rails app.
|
|
20
20
|
|
|
21
|
-
If instead you want to stream your logs to https://onlylogs.io, head to the onlylogs.io instructions page.
|
|
21
|
+
If instead you want to stream your logs to https://onlylogs.io, head to [the onlylogs.io instructions page](https://onlylogs.io/instructions).
|
|
22
22
|
|
|
23
23
|
Add this line to your application's Gemfile:
|
|
24
24
|
|
|
@@ -290,38 +290,93 @@ You are more than welcome to help and contribute to this package.
|
|
|
290
290
|
|
|
291
291
|
The app uses minitest and includes a dummy app, so getting started should be straightforward.
|
|
292
292
|
|
|
293
|
+
### Getting Started
|
|
294
|
+
|
|
295
|
+
First, install dependencies:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
bundle install
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Then start the dummy Rails app:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
cd test/dummy
|
|
305
|
+
bundle exec rails s
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The dummy app will be available at `http://localhost:3000` and onlylogs at `http://localhost:3000/onlylogs`.
|
|
309
|
+
|
|
310
|
+
### Generating Test Logs
|
|
311
|
+
|
|
312
|
+
To test onlylogs with live log data, use the continuous log writer script. This is especially useful for testing real-time log streaming and UI behavior.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Generate 1 log entry every 2 seconds (default)
|
|
316
|
+
bin/continuous_log_writer
|
|
317
|
+
|
|
318
|
+
# Generate 5 log entries every 1 second
|
|
319
|
+
bin/continuous_log_writer 5 1
|
|
320
|
+
|
|
321
|
+
# Generate 10 log entries every 3 seconds
|
|
322
|
+
bin/continuous_log_writer 10 3
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The script will write logs to `test/dummy/log/development.log`, which will appear in real-time in the onlylogs UI at `http://localhost:3000/onlylogs`.
|
|
326
|
+
|
|
327
|
+
**Example workflow:**
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Terminal 1: Start the Rails app
|
|
331
|
+
cd test/dummy && bundle exec rails s
|
|
332
|
+
|
|
333
|
+
# Terminal 2: Generate test logs while viewing in the UI
|
|
334
|
+
bin/continuous_log_writer 3 1
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Open `http://localhost:3000/onlylogs` in your browser and watch logs appear as the script writes them.
|
|
338
|
+
|
|
293
339
|
### Latency Simulation
|
|
294
340
|
|
|
295
341
|
For testing how onlylogs behaves under production-like network conditions, you can simulate latency for HTTP requests and WebSocket connections using the included latency simulation tool.
|
|
296
342
|
|
|
297
|
-
|
|
343
|
+
**Parameters:**
|
|
344
|
+
- **Latency** (default: 120ms): The base network delay added to all traffic
|
|
345
|
+
- **Jitter** (default: 30ms): Random variation (±) applied to the latency (changes every 2 seconds to simulate real network conditions)
|
|
346
|
+
- **Port** (default: 3000): The port to apply latency simulation to
|
|
347
|
+
|
|
348
|
+
**Common scenarios:**
|
|
298
349
|
|
|
299
350
|
```bash
|
|
300
|
-
#
|
|
301
|
-
|
|
351
|
+
# Default: 120ms ±30ms jitter (simulates typical 4G/LTE conditions)
|
|
352
|
+
bin/simulate_latency enable
|
|
302
353
|
|
|
303
|
-
#
|
|
304
|
-
|
|
354
|
+
# High latency: 500ms (simulates poor connectivity)
|
|
355
|
+
bin/simulate_latency enable 500
|
|
305
356
|
|
|
306
|
-
#
|
|
307
|
-
|
|
357
|
+
# High latency with high variation: 300ms ±100ms (simulates unstable connections)
|
|
358
|
+
bin/simulate_latency enable 300/100
|
|
308
359
|
|
|
309
|
-
#
|
|
310
|
-
|
|
360
|
+
# Custom port: 120ms ±30ms on port 8080
|
|
361
|
+
bin/simulate_latency enable -p 8080
|
|
362
|
+
```
|
|
311
363
|
|
|
312
|
-
# Enable custom latency and jitter on custom port (150±50ms jitter on port 8080)
|
|
313
|
-
./bin/simulate_latency enable 150/50 -p 8080
|
|
314
364
|
|
|
315
|
-
|
|
316
|
-
./bin/simulate_latency test
|
|
365
|
+
**Testing and monitoring:**
|
|
317
366
|
|
|
318
|
-
|
|
319
|
-
|
|
367
|
+
```bash
|
|
368
|
+
# Test the current latency configuration
|
|
369
|
+
bin/simulate_latency test
|
|
370
|
+
|
|
371
|
+
# Check current status and active pipes
|
|
372
|
+
bin/simulate_latency status
|
|
320
373
|
|
|
321
|
-
# Disable and
|
|
322
|
-
|
|
374
|
+
# Disable and restore normal network conditions
|
|
375
|
+
bin/simulate_latency disable
|
|
323
376
|
```
|
|
324
377
|
|
|
378
|
+
The `test` command will run 10 HTTP requests and 10 WebSocket connections, showing you the actual round-trip times and helping you verify the latency is working as expected.
|
|
379
|
+
|
|
325
380
|
### Performance Testing
|
|
326
381
|
|
|
327
382
|
Performance tests require large log files that are not included in the repository. You can download them using the provided script:
|
|
@@ -10,20 +10,38 @@ module Onlylogs
|
|
|
10
10
|
|
|
11
11
|
@filter = params[:filter]
|
|
12
12
|
@autoscroll = params[:autoscroll] != "false"
|
|
13
|
+
@regexp_mode = params[:regexp_mode] == "true"
|
|
13
14
|
@mode = @filter.blank? ? (params[:mode] || "live") : "search" # "live" or "search"
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
def download
|
|
18
|
+
if params[:log_file_path].blank?
|
|
19
|
+
return render(plain: "Bad Request: missing required parameter 'log_file_path' (encrypted log file path).", status: :bad_request)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
file_path = authorized_log_file_path(params[:log_file_path])
|
|
23
|
+
send_file file_path, filename: ::File.basename(file_path), disposition: :attachment
|
|
24
|
+
rescue Onlylogs::SecureFilePath::SecurityError
|
|
25
|
+
render plain: "Bad Request: 'log_file_path' could not be decrypted (tampered or malformed token).", status: :bad_request
|
|
26
|
+
rescue Onlylogs::ForbiddenPathError
|
|
27
|
+
render plain: "Forbidden: requested log file path is not in the allowed list.", status: :forbidden
|
|
28
|
+
rescue ActionController::MissingFile, Errno::ENOENT
|
|
29
|
+
render plain: "Not Found: log file is currently unreadable (it may have been rotated, moved, or temporarily unavailable).", status: :not_found
|
|
30
|
+
end
|
|
31
|
+
|
|
16
32
|
private
|
|
17
33
|
|
|
18
34
|
def selected_log_file_path
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
return default_log_file_path if params[:log_file_path].blank?
|
|
36
|
+
authorized_log_file_path(params[:log_file_path])
|
|
37
|
+
end
|
|
21
38
|
|
|
39
|
+
def authorized_log_file_path(encrypted_path)
|
|
22
40
|
decrypted_path = Onlylogs::SecureFilePath.decrypt(encrypted_path)
|
|
23
41
|
if Onlylogs.file_path_permitted?(decrypted_path)
|
|
24
42
|
decrypted_path
|
|
25
43
|
else
|
|
26
|
-
raise
|
|
44
|
+
raise Onlylogs::ForbiddenPathError, "File path not allowed"
|
|
27
45
|
end
|
|
28
46
|
end
|
|
29
47
|
|
|
@@ -3,10 +3,6 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
export default class AutoSubmitController extends Controller {
|
|
4
4
|
submit(event) {
|
|
5
5
|
const form = event.target.form
|
|
6
|
-
|
|
7
|
-
form.requestSubmit()
|
|
8
|
-
} else {
|
|
9
|
-
form.submit()
|
|
10
|
-
}
|
|
6
|
+
form.requestSubmit ? form.requestSubmit() : form.submit();
|
|
11
7
|
}
|
|
12
8
|
}
|
|
@@ -12,7 +12,7 @@ export default class LogStreamerController extends Controller {
|
|
|
12
12
|
regexpMode: { type: Boolean, default: false }
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
static targets = ["logLines", "filterInput", "results", "liveMode", "message", "regexpMode", "websocketStatus", "stopButton", "clearButton"];
|
|
15
|
+
static targets = ["logLines", "filterInput", "results", "liveMode", "message", "regexpMode", "websocketStatus", "stopButton", "clearButton", "autoscroll"];
|
|
16
16
|
|
|
17
17
|
connect() {
|
|
18
18
|
this.consumer = createConsumer();
|
|
@@ -86,11 +86,29 @@ export default class LogStreamerController extends Controller {
|
|
|
86
86
|
|
|
87
87
|
toggleAutoScroll() {
|
|
88
88
|
this.autoScrollValue = !this.autoScrollValue;
|
|
89
|
+
this.#updateUrlParam('autoscroll', this.autoScrollValue ? null : 'false');
|
|
89
90
|
this.scroll();
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
pauseForSelection() {
|
|
94
|
+
// Triggered by TextSelectionController#handleMouseDown via text-selection:start event
|
|
95
|
+
// Enter "highlighting mode" - disable both autoscroll and live mode
|
|
96
|
+
if (this.autoScrollValue) {
|
|
97
|
+
this.autoScrollValue = false;
|
|
98
|
+
this.autoscrollTarget.checked = false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this.isLiveMode()) {
|
|
102
|
+
this.liveModeTarget.checked = false;
|
|
103
|
+
this.modeValue = 'search';
|
|
104
|
+
this.updateLiveModeState();
|
|
105
|
+
this.stop();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
toggleRegexpMode() {
|
|
93
110
|
this.regexpModeValue = this.regexpModeTarget.checked;
|
|
111
|
+
this.#updateUrlParam('regexp_mode', this.regexpModeValue ? 'true' : null);
|
|
94
112
|
// If we have a filter applied, reconnect to apply the new regexp mode
|
|
95
113
|
if (this.filterInputTarget.value && this.filterInputTarget.value.trim() !== '') {
|
|
96
114
|
this.reconnectWithNewMode();
|
|
@@ -100,20 +118,22 @@ export default class LogStreamerController extends Controller {
|
|
|
100
118
|
toggleLiveMode() {
|
|
101
119
|
// this condition looks revered, but the value here has been changed already. so the live mode has been enabled.
|
|
102
120
|
if (this.isLiveMode()) {
|
|
103
|
-
this.clearFilter();
|
|
104
121
|
this.modeValue = 'live';
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
this.updateLiveModeState();
|
|
123
|
+
if (!this.isRunning) {
|
|
124
|
+
this.start();
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Prevent unchecking - live mode can only be disabled by applying a filter
|
|
109
128
|
this.liveModeTarget.checked = true;
|
|
110
|
-
return false;
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
applyFilter() {
|
|
133
|
+
const filterValue = this.filterInputTarget.value;
|
|
134
|
+
|
|
115
135
|
// If filter is applied, disable live mode
|
|
116
|
-
if (
|
|
136
|
+
if (filterValue && filterValue.trim() !== '') {
|
|
117
137
|
this.liveModeTarget.checked = false;
|
|
118
138
|
this.modeValue = 'search';
|
|
119
139
|
} else {
|
|
@@ -125,6 +145,7 @@ export default class LogStreamerController extends Controller {
|
|
|
125
145
|
// Update visual state
|
|
126
146
|
this.updateLiveModeState();
|
|
127
147
|
this.updateStopButtonVisibility();
|
|
148
|
+
this.#updateUrlParam('filter', filterValue || null);
|
|
128
149
|
|
|
129
150
|
// Use the global debounced reconnection (300ms delay)
|
|
130
151
|
this.reconnectWithNewMode();
|
|
@@ -168,6 +189,9 @@ export default class LogStreamerController extends Controller {
|
|
|
168
189
|
this.updateLiveModeState();
|
|
169
190
|
this.updateStopButtonVisibility();
|
|
170
191
|
|
|
192
|
+
// Update URL with cleared filter
|
|
193
|
+
this.#updateUrlParam('filter');
|
|
194
|
+
|
|
171
195
|
// Reconnect with cleared filter and live mode
|
|
172
196
|
this.reconnectWithNewMode();
|
|
173
197
|
}
|
|
@@ -184,11 +208,14 @@ export default class LogStreamerController extends Controller {
|
|
|
184
208
|
|
|
185
209
|
updateLiveModeState() {
|
|
186
210
|
const liveModeLabel = this.liveModeTarget.closest('label');
|
|
211
|
+
const hasFilter = this.filterInputTarget.value && this.filterInputTarget.value.trim() !== '';
|
|
187
212
|
|
|
188
|
-
if (
|
|
189
|
-
liveModeLabel.classList.remove('live-mode-sticky');
|
|
190
|
-
} else {
|
|
213
|
+
if (hasFilter) {
|
|
191
214
|
liveModeLabel.classList.add('live-mode-sticky');
|
|
215
|
+
this.liveModeTarget.disabled = true;
|
|
216
|
+
} else {
|
|
217
|
+
liveModeLabel.classList.remove('live-mode-sticky');
|
|
218
|
+
this.liveModeTarget.disabled = false;
|
|
192
219
|
}
|
|
193
220
|
}
|
|
194
221
|
|
|
@@ -390,4 +417,17 @@ export default class LogStreamerController extends Controller {
|
|
|
390
417
|
this.clusterize = null;
|
|
391
418
|
this.#initializeClusterize();
|
|
392
419
|
}
|
|
420
|
+
|
|
421
|
+
#updateUrlParam(param, value = null) {
|
|
422
|
+
const params = new URLSearchParams(window.location.search);
|
|
423
|
+
|
|
424
|
+
if (value != null) {
|
|
425
|
+
params.set(param, value);
|
|
426
|
+
} else {
|
|
427
|
+
params.delete(param);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
|
431
|
+
window.history.replaceState(null, '', newUrl);
|
|
432
|
+
}
|
|
393
433
|
}
|
|
@@ -7,19 +7,26 @@ export default class TextSelectionController extends Controller {
|
|
|
7
7
|
this.boundHandleTextSelection = this.handleTextSelection.bind(this)
|
|
8
8
|
this.boundHandleDocumentClick = this.handleDocumentClick.bind(this)
|
|
9
9
|
this.boundHandleSelectionChange = this.handleSelectionChange.bind(this)
|
|
10
|
-
|
|
10
|
+
this.boundHandleMouseDown = this.handleMouseDown.bind(this)
|
|
11
|
+
|
|
11
12
|
// Only listen for text selection on the log lines, not the toolbar
|
|
13
|
+
this.logLinesTarget.addEventListener('mousedown', this.boundHandleMouseDown)
|
|
12
14
|
this.logLinesTarget.addEventListener('mouseup', this.boundHandleTextSelection)
|
|
13
15
|
document.addEventListener('click', this.boundHandleDocumentClick)
|
|
14
16
|
document.addEventListener('selectionchange', this.boundHandleSelectionChange)
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
disconnect() {
|
|
20
|
+
this.logLinesTarget.removeEventListener('mousedown', this.boundHandleMouseDown)
|
|
18
21
|
this.logLinesTarget.removeEventListener('mouseup', this.boundHandleTextSelection)
|
|
19
22
|
document.removeEventListener('click', this.boundHandleDocumentClick)
|
|
20
23
|
document.removeEventListener('selectionchange', this.boundHandleSelectionChange)
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
handleMouseDown() {
|
|
27
|
+
this.dispatch("start")
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
handleTextSelection(event) {
|
|
24
31
|
const selection = window.getSelection()
|
|
25
32
|
const selectedText = selection.toString().trim()
|
|
@@ -5,7 +5,6 @@ module Onlylogs
|
|
|
5
5
|
class SecurityError < StandardError; end
|
|
6
6
|
|
|
7
7
|
def self.encrypt(file_path)
|
|
8
|
-
encryptor = ActiveSupport::MessageEncryptor.new(encryption_key)
|
|
9
8
|
encrypted = encryptor.encrypt_and_sign(file_path.to_s)
|
|
10
9
|
Base64.urlsafe_encode64(encrypted).tr("=", "")
|
|
11
10
|
rescue => e
|
|
@@ -15,15 +14,14 @@ module Onlylogs
|
|
|
15
14
|
|
|
16
15
|
def self.decrypt(encrypted_path)
|
|
17
16
|
decoded = Base64.urlsafe_decode64(encrypted_path)
|
|
18
|
-
encryptor = ActiveSupport::MessageEncryptor.new(encryption_key)
|
|
19
17
|
encryptor.decrypt_and_verify(decoded)
|
|
20
18
|
rescue => e
|
|
21
19
|
Rails.logger.error "Onlylogs: Decryption failed: #{e.message}"
|
|
22
20
|
raise SecurityError, "Invalid encrypted file path"
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
private_class_method def self.
|
|
26
|
-
Rails.application.secret_key_base[0..31]
|
|
23
|
+
private_class_method def self.encryptor
|
|
24
|
+
@encryptor ||= ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
|
|
27
25
|
end
|
|
28
26
|
end
|
|
29
27
|
end
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
.log-file-dropdown {
|
|
15
15
|
margin-left: 0.5rem;
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
.download-link {
|
|
19
|
+
margin-left: 0.5rem;
|
|
20
|
+
}
|
|
17
21
|
</style>
|
|
18
22
|
<div class="grid">
|
|
19
23
|
<div class="grid-header">
|
|
@@ -21,15 +25,16 @@
|
|
|
21
25
|
<%= form_with url: root_path, method: :get, local: true, class: "log-file-dropdown", data: { controller: "auto-submit" } do %>
|
|
22
26
|
<select id="log_file_path" name="log_file_path" data-action="change->auto-submit#submit">
|
|
23
27
|
<% @available_log_files.each do |file| %>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<option value="<%= encrypted_path %>" <%= "selected" if file.to_s == @log_file_path.to_s %>>
|
|
27
|
-
<%= relative_path %>
|
|
28
|
+
<option value="<%= Onlylogs::SecureFilePath.encrypt(file) %>" <%= "selected" if file.to_s == @log_file_path.to_s %>>
|
|
29
|
+
<%= log_file_label(file) %>
|
|
28
30
|
</option>
|
|
29
31
|
<% end %>
|
|
30
32
|
</select>
|
|
31
33
|
<% end %>
|
|
34
|
+
<% if @log_file_path %>
|
|
35
|
+
<%= link_to "Download", download_log_path(log_file_path: Onlylogs::SecureFilePath.encrypt(@log_file_path)), class: "download-link" %>
|
|
36
|
+
<% end %>
|
|
32
37
|
<% end %>
|
|
33
38
|
</div>
|
|
34
|
-
<%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll } %>
|
|
39
|
+
<%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll, regexp_mode: @regexp_mode } %>
|
|
35
40
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# locals: (log_file_path:, tail: 100, filter: "", autoscroll: true) %>
|
|
1
|
+
<%# locals: (log_file_path:, tail: 100, filter: "", autoscroll: true, regexp_mode: false) %>
|
|
2
2
|
<script nonce="<%= request.content_security_policy_nonce %>"
|
|
3
3
|
src="https://cdn.jsdelivr.net/npm/clusterize.js@0.18.1/clusterize.min.js"></script>
|
|
4
4
|
<%= render "onlylogs/shared/log_container_styles" %>
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
data-log-streamer-cursor-position-value="<%= cursor_position %>"
|
|
18
18
|
data-log-streamer-filter-value="<%= filter %>"
|
|
19
19
|
data-log-streamer-auto-scroll-value="<%= autoscroll %>"
|
|
20
|
+
data-log-streamer-regexp-mode-value="<%= regexp_mode %>"
|
|
20
21
|
data-log-streamer-mode-value="<%= mode %>"
|
|
22
|
+
data-action="text-selection:start->log-streamer#pauseForSelection"
|
|
21
23
|
class="onlylogs-log-container" >
|
|
22
24
|
<div data-log-streamer-target="logLines" data-text-selection-target="logLines" id="scrollArea" class="onlylogs-log-lines clusterize-scroll">
|
|
23
25
|
<div id="contentArea" class="clusterize-content">
|
|
@@ -42,13 +44,13 @@
|
|
|
42
44
|
</div>
|
|
43
45
|
<div>
|
|
44
46
|
<label style="margin-bottom: 0;">
|
|
45
|
-
<input id="autoscroll" type="checkbox" <%= autoscroll ? "checked" : "" %> name="autoscroll" data-keyboard-shortcuts-target="autoscroll" data-action="change->log-streamer#toggleAutoScroll">
|
|
47
|
+
<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">
|
|
46
48
|
<u>A</u>utoscroll
|
|
47
49
|
</label>
|
|
48
50
|
</div>
|
|
49
51
|
<div>
|
|
50
52
|
<label style="margin-bottom: 0;">
|
|
51
|
-
<input id="regexpMode" type="checkbox" name="regexpMode" data-log-streamer-target="regexpMode" data-text-selection-target="regexpMode" data-action="change->log-streamer#toggleRegexpMode">
|
|
53
|
+
<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">
|
|
52
54
|
<u>R</u>egexp Mode
|
|
53
55
|
</label>
|
|
54
56
|
</div>
|
data/bin/super_grep
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
export LC_ALL=C
|
|
3
|
+
|
|
4
|
+
# Parse arguments
|
|
5
|
+
max_matches=""
|
|
6
|
+
start_position=""
|
|
7
|
+
end_position=""
|
|
8
|
+
block_size="${BLOCK_SIZE:-8M}"
|
|
9
|
+
|
|
10
|
+
while [[ $# -gt 0 ]]; do
|
|
11
|
+
case "$1" in
|
|
12
|
+
--max-matches)
|
|
13
|
+
max_matches="$2"
|
|
14
|
+
shift 2
|
|
15
|
+
;;
|
|
16
|
+
--start-position)
|
|
17
|
+
start_position="$2"
|
|
18
|
+
shift 2
|
|
19
|
+
;;
|
|
20
|
+
--end-position)
|
|
21
|
+
end_position="$2"
|
|
22
|
+
shift 2
|
|
23
|
+
;;
|
|
24
|
+
--regexp)
|
|
25
|
+
regexp_mode=true
|
|
26
|
+
shift
|
|
27
|
+
;;
|
|
28
|
+
*)
|
|
29
|
+
break
|
|
30
|
+
;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
query="$1"
|
|
35
|
+
file="$2"
|
|
36
|
+
|
|
37
|
+
# Validate file
|
|
38
|
+
[ -f "$file" ] || exit 1
|
|
39
|
+
|
|
40
|
+
# Prepare query regex based on mode
|
|
41
|
+
if [ "$regexp_mode" = true ]; then
|
|
42
|
+
# Translate \d to [0-9] for grep -E compatibility
|
|
43
|
+
query_regex=$(printf "%s" "$query" | sed 's/\\d/[0-9]/g')
|
|
44
|
+
else
|
|
45
|
+
# Escape special characters
|
|
46
|
+
query_regex=$(printf "%s" "$query" | sed -E 's/([][{}()*+?.\\^$|])/\\\1/g')
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Replace spaces with placeholder to avoid sed escaping hell for color regex
|
|
50
|
+
placeholder="__COLOR_REGEX__"
|
|
51
|
+
query_regex=$(printf "%s" "$query_regex" | sed -E "s/ +/([[:space:]]|$placeholder)+/g")
|
|
52
|
+
|
|
53
|
+
# Use bash variable substitution to insert the actual color regex
|
|
54
|
+
ESC=$(printf '\033')
|
|
55
|
+
actual_color_regex="${ESC}\\[[0-9;]*m"
|
|
56
|
+
query_regex="${query_regex//$placeholder/$actual_color_regex}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Build grep command
|
|
60
|
+
grep_cmd=(grep -E -h) # -h = no filename (portable)
|
|
61
|
+
if [ -n "$max_matches" ]; then
|
|
62
|
+
grep_cmd+=(-m "$max_matches") # -m = max count (portable)
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Handle byte range if specified
|
|
66
|
+
if [ -n "$start_position" ] || [ -n "$end_position" ]; then
|
|
67
|
+
file_size=$(wc -c < "$file")
|
|
68
|
+
range_start=${start_position:-0}
|
|
69
|
+
range_end=${end_position:-$file_size}
|
|
70
|
+
range_size=$((range_end - range_start))
|
|
71
|
+
|
|
72
|
+
# Validate range
|
|
73
|
+
if [ $range_start -lt 0 ] || [ $range_size -le 0 ] || [ $range_start -ge $file_size ]; then
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Adjust if exceeds file size
|
|
78
|
+
[ $range_end -gt $file_size ] && range_end=$file_size && range_size=$((range_end - range_start))
|
|
79
|
+
|
|
80
|
+
# Extract byte range using dd
|
|
81
|
+
start_mb=$((range_start / 1048576))
|
|
82
|
+
start_offset=$((range_start % 1048576))
|
|
83
|
+
count_mb=$(((range_size + 1048576 - 1) / 1048576))
|
|
84
|
+
|
|
85
|
+
dd if="$file" bs="$block_size" skip=$start_mb count=$count_mb 2>/dev/null | \
|
|
86
|
+
dd bs=1 skip=$start_offset count=$range_size 2>/dev/null | \
|
|
87
|
+
"${grep_cmd[@]}" "$query_regex"
|
|
88
|
+
else
|
|
89
|
+
# Search entire file
|
|
90
|
+
"${grep_cmd[@]}" "$query_regex" "$file"
|
|
91
|
+
fi
|
data/bin/super_ripgrep
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
export LC_ALL=C
|
|
3
|
+
|
|
4
|
+
# Parse arguments
|
|
5
|
+
max_matches=""
|
|
6
|
+
start_position=""
|
|
7
|
+
end_position=""
|
|
8
|
+
block_size="${BLOCK_SIZE:-8M}"
|
|
9
|
+
|
|
10
|
+
while [[ $# -gt 0 ]]; do
|
|
11
|
+
case "$1" in
|
|
12
|
+
--max-matches)
|
|
13
|
+
max_matches="$2"
|
|
14
|
+
shift 2
|
|
15
|
+
;;
|
|
16
|
+
--start-position)
|
|
17
|
+
start_position="$2"
|
|
18
|
+
shift 2
|
|
19
|
+
;;
|
|
20
|
+
--end-position)
|
|
21
|
+
end_position="$2"
|
|
22
|
+
shift 2
|
|
23
|
+
;;
|
|
24
|
+
--regexp)
|
|
25
|
+
regexp_mode=true
|
|
26
|
+
shift
|
|
27
|
+
;;
|
|
28
|
+
*)
|
|
29
|
+
break
|
|
30
|
+
;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
query="$1"
|
|
35
|
+
file="$2"
|
|
36
|
+
|
|
37
|
+
# Validate file
|
|
38
|
+
[ -f "$file" ] || exit 1
|
|
39
|
+
|
|
40
|
+
# Prepare query regex based on mode
|
|
41
|
+
if [ "$regexp_mode" = true ]; then
|
|
42
|
+
# Translate \d to [0-9] for consistency
|
|
43
|
+
query_regex=$(printf "%s" "$query" | sed 's/\\d/[0-9]/g')
|
|
44
|
+
else
|
|
45
|
+
# Escape special characters
|
|
46
|
+
query_regex=$(printf "%s" "$query" | sed -E 's/([][{}()*+?.\\^$|])/\\\1/g')
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Replace spaces with placeholder to avoid sed escaping hell for color regex
|
|
50
|
+
placeholder="__COLOR_REGEX__"
|
|
51
|
+
query_regex=$(printf "%s" "$query_regex" | sed -E "s/ +/([[:space:]]|$placeholder)+/g")
|
|
52
|
+
|
|
53
|
+
# Use bash variable substitution to insert the actual color regex
|
|
54
|
+
# Ripgrep supports \x1b directly
|
|
55
|
+
actual_color_regex='\x1b\[[0-9;]*m'
|
|
56
|
+
query_regex="${query_regex//$placeholder/$actual_color_regex}"
|
|
57
|
+
|
|
58
|
+
# Build ripgrep command
|
|
59
|
+
rg_cmd="rg --color=never --no-filename"
|
|
60
|
+
[ -n "$max_matches" ] && rg_cmd="$rg_cmd --max-count=$max_matches"
|
|
61
|
+
|
|
62
|
+
# Handle byte range if specified
|
|
63
|
+
if [ -n "$start_position" ] || [ -n "$end_position" ]; then
|
|
64
|
+
file_size=$(wc -c < "$file")
|
|
65
|
+
range_start=${start_position:-0}
|
|
66
|
+
range_end=${end_position:-$file_size}
|
|
67
|
+
range_size=$((range_end - range_start))
|
|
68
|
+
|
|
69
|
+
# Validate range
|
|
70
|
+
if [ $range_start -lt 0 ] || [ $range_size -le 0 ] || [ $range_start -ge $file_size ]; then
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Adjust if exceeds file size
|
|
75
|
+
[ $range_end -gt $file_size ] && range_end=$file_size && range_size=$((range_end - range_start))
|
|
76
|
+
|
|
77
|
+
# Extract byte range using dd
|
|
78
|
+
start_mb=$((range_start / 1048576))
|
|
79
|
+
start_offset=$((range_start % 1048576))
|
|
80
|
+
count_mb=$(((range_size + 1048576 - 1) / 1048576))
|
|
81
|
+
|
|
82
|
+
dd if="$file" bs="$block_size" skip=$start_mb count=$count_mb 2>/dev/null | \
|
|
83
|
+
dd bs=1 skip=$start_offset count=$range_size 2>/dev/null | \
|
|
84
|
+
$rg_cmd -e "$query_regex"
|
|
85
|
+
else
|
|
86
|
+
# Search entire file
|
|
87
|
+
$rg_cmd -e "$query_regex" "$file"
|
|
88
|
+
fi
|
data/config/routes.rb
CHANGED
data/lib/onlylogs/version.rb
CHANGED
data/lib/onlylogs.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: onlylogs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessandro Rodi
|
|
@@ -29,6 +29,8 @@ email:
|
|
|
29
29
|
- alessandro.rodi@renuo.ch
|
|
30
30
|
executables:
|
|
31
31
|
- onlylogs_sidecar
|
|
32
|
+
- super_grep
|
|
33
|
+
- super_ripgrep
|
|
32
34
|
extensions: []
|
|
33
35
|
extra_rdoc_files: []
|
|
34
36
|
files:
|
|
@@ -68,6 +70,8 @@ files:
|
|
|
68
70
|
- app/views/onlylogs/shared/_log_container.html.erb
|
|
69
71
|
- app/views/onlylogs/shared/_log_container_styles.html.erb
|
|
70
72
|
- bin/onlylogs_sidecar
|
|
73
|
+
- bin/super_grep
|
|
74
|
+
- bin/super_ripgrep
|
|
71
75
|
- config/importmap.rb
|
|
72
76
|
- config/routes.rb
|
|
73
77
|
- db/migrate/20250902112548_create_books.rb
|