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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b9969627b74355a1b0c31a2314961e45ed78558c10b2c181c83c8303c4128a5
4
- data.tar.gz: b01ad9a6faa8d94e982a938d8c1b7afa4e25fe277ddbd6d0468546d9c9272f64
3
+ metadata.gz: 7340d11d402202a753fa7a6426c6e7c3338af2b53779951aced223420e250e8c
4
+ data.tar.gz: 6be0ff24e762d27d8929a8cd177b53c0140a181bd5717c803858fcc72436074f
5
5
  SHA512:
6
- metadata.gz: d40db595e6c21fdff61f5f0f0c0eef96a1be49b35bb79a0e57fc0676b9fdde401d7e644a42a9523f54a79f00150646e0413c352a774437c33e6c20f60b6ed46c
7
- data.tar.gz: 433b9d90c250d86e88117f4706304a571b6f34fc72992209aab90427ebb29cc1162dcf9d09a435f23244c55f1c79f3b4fb15985c6c7e189673512587feac7940
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
- ### Usage
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
- # Enable latency simulation (120±30ms jitter on port 3000)
301
- ./bin/simulate_latency enable
351
+ # Default: 120ms ±30ms jitter (simulates typical 4G/LTE conditions)
352
+ bin/simulate_latency enable
302
353
 
303
- # Enable custom latency simulation (150±30ms jitter on port 3000)
304
- ./bin/simulate_latency enable 150
354
+ # High latency: 500ms (simulates poor connectivity)
355
+ bin/simulate_latency enable 500
305
356
 
306
- # Enable custom latency and jitter (200±50ms jitter on port 3000)
307
- ./bin/simulate_latency enable 200/50
357
+ # High latency with high variation: 300ms ±100ms (simulates unstable connections)
358
+ bin/simulate_latency enable 300/100
308
359
 
309
- # Enable latency simulation on custom port (120±30ms jitter on port 8080)
310
- ./bin/simulate_latency enable -p 8080
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
- # Test the latency
316
- ./bin/simulate_latency test
365
+ **Testing and monitoring:**
317
366
 
318
- # Check current status
319
- ./bin/simulate_latency status
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 clean up
322
- ./bin/simulate_latency disable
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
- encrypted_path = params[:log_file_path]
20
- return default_log_file_path if encrypted_path.blank?
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 SecurityError, "File path not allowed"
44
+ raise Onlylogs::ForbiddenPathError, "File path not allowed"
27
45
  end
28
46
  end
29
47
 
@@ -1,4 +1,7 @@
1
1
  module Onlylogs
2
2
  module ApplicationHelper
3
+ def log_file_label(file)
4
+ Pathname.new(file).relative_path_from(Rails.root).to_s
5
+ end
3
6
  end
4
7
  end
@@ -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
- if (form.requestSubmit) {
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.reconnectWithNewMode();
106
- return;
107
- }
108
- else {
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 (this.filterInputTarget.value && this.filterInputTarget.value.trim() !== '') {
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 (this.isLiveMode()) {
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.encryption_key
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
- <% encrypted_path = Onlylogs::SecureFilePath.encrypt(file.to_s) %>
25
- <% relative_path = Pathname.new(file).relative_path_from(Rails.root).to_s %>
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
@@ -1,4 +1,5 @@
1
1
  Onlylogs::Engine.routes.draw do
2
2
  root "logs#index"
3
3
  resources :logs, only: [:index]
4
+ get "download", to: "logs#download", as: :download_log
4
5
  end
@@ -1,3 +1,3 @@
1
1
  module Onlylogs
2
- VERSION = "0.4.5"
2
+ VERSION = "0.5.1"
3
3
  end
data/lib/onlylogs.rb CHANGED
@@ -14,6 +14,8 @@ require "onlylogs/http_logger"
14
14
  # loader.setup
15
15
 
16
16
  module Onlylogs
17
+ class ForbiddenPathError < StandardError; end
18
+
17
19
  if defined?(Importmap)
18
20
  mattr_accessor :importmap, default: Importmap::Map.new
19
21
  end
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.5
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