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 +4 -4
- data/README.md +11 -1
- data/app/channels/onlylogs/logs_channel.rb +82 -70
- data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +15 -11
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +170 -202
- 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/grep.rb +14 -6
- data/app/views/onlylogs/logs/index.html.erb +25 -5
- data/app/views/onlylogs/shared/_log_container.html.erb +71 -35
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +246 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9303c9ba4c727343d48702332baef85d112068f0639a148ff42afc8693b1f7af
|
|
4
|
+
data.tar.gz: 82fe3f9d70cfc3b2d4a6f661a296974037aca9fd8bd8e97317476bb091d3c320
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 [
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 = ["
|
|
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.
|
|
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
|
-
|
|
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
|
}
|