onlylogs 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f166264d89c5d2273751e3377ef236168da610b1d37d5ddf02d33959f2fe318d
4
- data.tar.gz: a612d6738ee600171cd21a78b403d1abef06fdf5df893ebcc8c3401b092f6a51
3
+ metadata.gz: 9303c9ba4c727343d48702332baef85d112068f0639a148ff42afc8693b1f7af
4
+ data.tar.gz: 82fe3f9d70cfc3b2d4a6f661a296974037aca9fd8bd8e97317476bb091d3c320
5
5
  SHA512:
6
- metadata.gz: 6c2d38d16486ea48cd8e6c695e1b9234dbe8e300f45342afa163adfef2e532cdf1cf0cdecdca411c9a13a80c6c4f32aa5c33a82dfc9137c2da35f72cffa0f2c6
7
- data.tar.gz: d4b8b016e5c089712e288d6533b613571f5edee524b2784a3aff80b4d8701913aabb169f72cdedb4220c01a86f3be33d0c52eedc644538db1a3fe3e259e947c9
6
+ metadata.gz: d7bf3497e6013d367776e50e716d113781cdde78b79d302b01a126778092aa36fb0f025c93162301b4f9444d9be83bd30ebd2b832d3e3e5da29d6ed721bd6138
7
+ data.tar.gz: 879705e6eb4dafe29e1bf1698495cfb0731ccefc13f3127547a45173e4fdc6beda4ea53c13b4406ae6c3d1ba10ffe36f3b2f21bf1a7e3ff17adf195b5161170f
data/README.md CHANGED
@@ -235,7 +235,7 @@ end
235
235
 
236
236
  Onlylogs automatically detects file paths in log messages and converts them into clickable links that open in your preferred code editor.
237
237
 
238
- For a complete list of supported editors, see [lib/onlylogs/editor_detector.rb](lib/onlylogs/editor_detector.rb).
238
+ For a complete list of supported editors, see [app/models/onlylogs/file_path_parser.rb](app/models/onlylogs/file_path_parser.rb).
239
239
 
240
240
  ```bash
241
241
  # env variables
@@ -340,6 +340,16 @@ bin/continuous_log_writer 10 3
340
340
 
341
341
  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`.
342
342
 
343
+ The lines are generated by `Onlylogs::ContinuousLogWriter`, in the same format a Rails app logging through `Onlylogs::Logger` produces. The script is a thin wrapper, so applications embedding onlylogs can reuse the writer against any file of their own:
344
+
345
+ ```ruby
346
+ require "onlylogs/continuous_log_writer"
347
+
348
+ Onlylogs::ContinuousLogWriter.new(some_log_path, logs_per_batch: 3, interval: 1).call
349
+ ```
350
+
351
+ It is plain Ruby (it does not need Rails to be booted) and is not loaded by `require "onlylogs"` — require it explicitly where you need it.
352
+
343
353
  **Example workflow:**
344
354
 
345
355
  ```bash
@@ -16,27 +16,8 @@ module Onlylogs
16
16
  @last_initialize_params = data.dup
17
17
  cleanup_existing_operations
18
18
 
19
- # Decrypt and verify the file path
20
- begin
21
- encrypted_file_path = data["file_path"]
22
- if encrypted_file_path.present?
23
- file_path = Onlylogs::SecureFilePath.decrypt(encrypted_file_path)
24
-
25
- # Verify the decrypted path is still allowed
26
- unless Onlylogs.file_path_permitted?(file_path)
27
- Rails.logger.error "Onlylogs: Attempted to access non-allowed file: #{file_path}"
28
- transmit({action: "error", content: "Access denied"})
29
- return
30
- end
31
- else
32
- # Fallback to default if no encrypted path provided
33
- file_path = Onlylogs.default_log_file_path
34
- end
35
- rescue Onlylogs::SecureFilePath::SecurityError => e
36
- Rails.logger.error "Onlylogs: Security violation - #{e.message}"
37
- transmit({action: "error", content: "Access denied"})
38
- return
39
- end
19
+ file_path = authorized_file_path(data["file_path"])
20
+ return unless file_path
40
21
 
41
22
  # Check if the file is a text file
42
23
  unless Onlylogs::File.text_file?(file_path)
@@ -72,6 +53,32 @@ module Onlylogs
72
53
 
73
54
  private
74
55
 
56
+ # Decrypts and authorises the requested file. Returns nil (after transmitting
57
+ # an error) when there is no token, when it cannot be decrypted, or when the
58
+ # decrypted path is not on the configured allow-list — the three ways a client
59
+ # can ask for something it is not entitled to stream.
60
+ def authorized_file_path(encrypted_file_path)
61
+ if encrypted_file_path.blank?
62
+ Rails.logger.error "Onlylogs: initialize_watcher without a file path token; refusing to stream"
63
+ transmit({action: "error", content: "Access denied"})
64
+ return nil
65
+ end
66
+
67
+ file_path = Onlylogs::SecureFilePath.decrypt(encrypted_file_path)
68
+
69
+ unless Onlylogs.file_path_permitted?(file_path)
70
+ Rails.logger.error "Onlylogs: Attempted to access non-allowed file: #{file_path}"
71
+ transmit({action: "error", content: "Access denied"})
72
+ return nil
73
+ end
74
+
75
+ file_path
76
+ rescue Onlylogs::SecureFilePath::SecurityError => e
77
+ Rails.logger.error "Onlylogs: Security violation - #{e.message}"
78
+ transmit({action: "error", content: "Access denied"})
79
+ nil
80
+ end
81
+
75
82
  def cleanup_existing_operations
76
83
  if @batch_sender
77
84
  @batch_sender.stop(send_remaining_lines: false)
@@ -168,56 +175,18 @@ module Onlylogs
168
175
  last_byte_offset = nil
169
176
  line_count = 0
170
177
 
171
- Rails.logger.silence(Logger::ERROR) do
172
- skip_first = start_position > 0
173
-
174
- if filter.present?
175
- # Use grep for filtered search
176
- @log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position,
177
- end_position: end_position, timeout: Onlylogs.search_timeout) do |result|
178
- break if @batch_sender.nil? || @log_watcher_running == false
179
-
180
- # Skip first line if start_position > 0 (line is cut off at byte boundary)
181
- if skip_first
182
- skip_first = false
183
- next
184
- end
185
-
186
- # Result is a hash with {byte_offset, content}
187
- byte_offset = result[:byte_offset]
188
- log_line = result[:content]
178
+ show_expand_button = filter.present?
189
179
 
190
- # Buffer previous line and skip it to avoid cut-off lines at boundaries
191
- if last_line
192
- @batch_sender.add_line(render_log_line(last_line, byte_offset: last_byte_offset, show_expand_button: true))
193
- line_count += 1
194
- end
195
- last_line = log_line
196
- last_byte_offset = byte_offset
197
- end
198
- else
199
- # No filter - read all lines directly (skip grep)
200
- # Still need byte_offset for highlighting when expanding around a line
201
- current_byte_offset = start_position
202
- read_byte_range(file_path, start_position, end_position) do |log_line|
203
- break if @batch_sender.nil? || @log_watcher_running == false
204
-
205
- # Skip first line if start_position > 0 (line is cut off at byte boundary)
206
- if skip_first
207
- skip_first = false
208
- next
209
- end
210
-
211
- # Buffer previous line and skip it to avoid cut-off lines at boundaries
212
- if last_line
213
- @batch_sender.add_line(render_log_line(last_line, byte_offset: last_byte_offset))
214
- line_count += 1
215
- end
216
- last_line = log_line
217
- last_byte_offset = current_byte_offset
218
- # Account for line content plus newline character (2 bytes for \r\n or 1 for \n)
219
- current_byte_offset += log_line.bytesize + 1
180
+ Rails.logger.silence(Logger::ERROR) do
181
+ each_matching_line(file_path, filter, regexp_mode, start_position, end_position) do |log_line, byte_offset|
182
+ # Buffer previous line and skip it to avoid cut-off lines at boundaries
183
+ if last_line
184
+ @batch_sender.add_line(render_log_line(last_line, byte_offset: last_byte_offset,
185
+ show_expand_button: show_expand_button))
186
+ line_count += 1
220
187
  end
188
+ last_line = log_line
189
+ last_byte_offset = byte_offset
221
190
  end
222
191
  end
223
192
 
@@ -249,6 +218,49 @@ module Onlylogs
249
218
  end
250
219
  end
251
220
 
221
+ # Yields [line, byte_offset] over the requested slice: through grep when there
222
+ # is a filter, straight off disk when there is not. Either way the offsets are
223
+ # the real ones, so "show around this line" can re-centre on them.
224
+ #
225
+ # A range that starts mid-line opens with a fragment of a line, which is
226
+ # dropped - but its bytes still count towards every offset after it.
227
+ def each_matching_line(file_path, filter, regexp_mode, start_position, end_position)
228
+ skip_first = start_position > 0
229
+
230
+ if filter.present?
231
+ @log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position,
232
+ end_position: end_position, timeout: Onlylogs.search_timeout) do |result|
233
+ break if reading_stopped?
234
+
235
+ if skip_first
236
+ skip_first = false
237
+ next
238
+ end
239
+
240
+ yield result[:content], result[:byte_offset]
241
+ end
242
+ else
243
+ offset = start_position
244
+
245
+ read_byte_range(file_path, start_position, end_position) do |log_line, raw_bytesize|
246
+ break if reading_stopped?
247
+
248
+ if skip_first
249
+ skip_first = false
250
+ offset += raw_bytesize
251
+ next
252
+ end
253
+
254
+ yield log_line, offset
255
+ offset += raw_bytesize
256
+ end
257
+ end
258
+ end
259
+
260
+ def reading_stopped?
261
+ @batch_sender.nil? || @log_watcher_running == false
262
+ end
263
+
252
264
  def read_byte_range(file_path, start_position, end_position)
253
265
  file_size = ::File.size(file_path)
254
266
  range_size = (end_position || file_size) - start_position
@@ -256,7 +268,7 @@ module Onlylogs
256
268
  return if start_position < 0 || range_size <= 0 || start_position >= file_size
257
269
 
258
270
  ::File.read(file_path, range_size, start_position).each_line do |line|
259
- yield line.chomp
271
+ yield line.chomp, line.bytesize
260
272
  end
261
273
  rescue => e
262
274
  Rails.logger.error "Error reading byte range: #{e.message}"
@@ -1,7 +1,7 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  export default class KeyboardShortcutsController extends Controller {
4
- static targets = ["liveMode", "autoscroll"]
4
+ static targets = ["liveButton", "searchButton", "autoscroll"]
5
5
 
6
6
  connect() {
7
7
  this.boundHandleKeydown = this.handleKeydown.bind(this)
@@ -13,15 +13,25 @@ export default class KeyboardShortcutsController extends Controller {
13
13
  }
14
14
 
15
15
  handleKeydown(event) {
16
+ // These are bare single-key shortcuts. Any modifier means the keystroke belongs
17
+ // to the browser or the OS (cmd+L, cmd+A, cmd+S) and must pass straight through.
18
+ if (event.metaKey || event.ctrlKey || event.altKey) {
19
+ return
20
+ }
21
+
16
22
  // Only handle shortcuts when not typing in input fields
17
- if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
23
+ if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA' || event.target.isContentEditable) {
18
24
  return
19
25
  }
20
26
 
21
27
  switch (event.key.toLowerCase()) {
22
28
  case 'l':
23
29
  event.preventDefault()
24
- this.toggleLiveMode()
30
+ if (this.hasLiveButtonTarget) this.liveButtonTarget.click()
31
+ break
32
+ case 's':
33
+ event.preventDefault()
34
+ if (this.hasSearchButtonTarget) this.searchButtonTarget.click()
25
35
  break
26
36
  case 'a':
27
37
  event.preventDefault()
@@ -30,15 +40,9 @@ export default class KeyboardShortcutsController extends Controller {
30
40
  }
31
41
  }
32
42
 
33
- toggleLiveMode() {
34
- if (this.hasLiveModeTarget) {
35
- this.liveModeTarget.checked = !this.liveModeTarget.checked
36
- this.liveModeTarget.dispatchEvent(new Event('change', { bubbles: true }))
37
- }
38
- }
39
-
40
43
  toggleAutoscroll() {
41
- if (this.hasAutoscrollTarget) {
44
+ // Autoscroll only exists while live; ignore the shortcut when it is not on screen.
45
+ if (this.hasAutoscrollTarget && this.autoscrollTarget.offsetParent !== null) {
42
46
  this.autoscrollTarget.checked = !this.autoscrollTarget.checked
43
47
  this.autoscrollTarget.dispatchEvent(new Event('change', { bubbles: true }))
44
48
  }