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
data/lib/onlylogs/spool.rb
CHANGED
|
@@ -11,8 +11,13 @@ module Onlylogs
|
|
|
11
11
|
# This turns transient-failure / restart data loss into at-least-once delivery: a batch that was in
|
|
12
12
|
# fact received but whose response was lost will be replayed and show up as a duplicate
|
|
13
13
|
# downstream. Duplicates are an accepted trade for not losing data.
|
|
14
|
+
#
|
|
15
|
+
# The byte cap is enforced from an in-memory ledger of the files in the directory, refreshed by
|
|
16
|
+
# listing the directory at most every LEDGER_TTL seconds (sibling processes such as other Puma
|
|
17
|
+
# workers write to the same directory), so a write costs one file and not a stat of every file.
|
|
14
18
|
class Spool
|
|
15
19
|
DEFAULT_MAX_BYTES = 128 * 1024 * 1024 # 128 MB
|
|
20
|
+
LEDGER_TTL = 5
|
|
16
21
|
|
|
17
22
|
def initialize(dir:, max_bytes: DEFAULT_MAX_BYTES)
|
|
18
23
|
@dir = dir
|
|
@@ -21,6 +26,9 @@ module Onlylogs
|
|
|
21
26
|
@token = SecureRandom.hex(4)
|
|
22
27
|
@seq = 0
|
|
23
28
|
@mutex = Mutex.new
|
|
29
|
+
@ledger = nil
|
|
30
|
+
@ledger_bytes = 0
|
|
31
|
+
@ledger_at = nil
|
|
24
32
|
::FileUtils.mkdir_p(@dir)
|
|
25
33
|
end
|
|
26
34
|
|
|
@@ -29,6 +37,7 @@ module Onlylogs
|
|
|
29
37
|
return if body.nil? || body.empty?
|
|
30
38
|
|
|
31
39
|
@mutex.synchronize do
|
|
40
|
+
refresh_ledger if ledger_stale?
|
|
32
41
|
evict(body.bytesize)
|
|
33
42
|
seq = (@seq += 1)
|
|
34
43
|
final = ::File.join(@dir, "#{@token}-#{format("%09d", seq)}.batch")
|
|
@@ -37,30 +46,59 @@ module Onlylogs
|
|
|
37
46
|
# half-written file (it only globs *.batch).
|
|
38
47
|
::File.binwrite(tmp, body)
|
|
39
48
|
::File.rename(tmp, final)
|
|
49
|
+
@ledger << [final, body.bytesize]
|
|
50
|
+
@ledger_bytes += body.bytesize
|
|
40
51
|
end
|
|
41
52
|
rescue => e
|
|
42
53
|
Kernel.warn "Onlylogs::Spool write error: #{e.class}: #{e.message}"
|
|
43
54
|
end
|
|
44
55
|
|
|
45
|
-
# Replay pending batches oldest-first. Yields each body; if the block
|
|
46
|
-
# is deleted (delivered), otherwise replay stops and the
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
# Replay pending batches oldest-first, at most `limit` of them. Yields each body; if the block
|
|
57
|
+
# returns truthy the file is deleted (delivered, or given up on), otherwise replay stops and the
|
|
58
|
+
# remaining files are kept for later.
|
|
59
|
+
#
|
|
60
|
+
# Works off the ledger rather than listing the directory: with a large backlog replayed one file
|
|
61
|
+
# at a time, a glob per call would cost more than the delivery itself.
|
|
62
|
+
def replay(limit: nil)
|
|
63
|
+
paths = @mutex.synchronize do
|
|
64
|
+
refresh_ledger if ledger_stale?
|
|
65
|
+
@ledger.first(limit || @ledger.size).map(&:first)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
paths.each do |path|
|
|
49
69
|
body = read(path)
|
|
50
|
-
|
|
70
|
+
if body.nil? # already claimed/deleted by another process
|
|
71
|
+
forget(path)
|
|
72
|
+
next
|
|
73
|
+
end
|
|
51
74
|
|
|
52
75
|
break unless yield(body)
|
|
53
76
|
|
|
54
77
|
delete(path)
|
|
78
|
+
forget(path)
|
|
55
79
|
end
|
|
56
80
|
end
|
|
57
81
|
|
|
58
82
|
def empty?
|
|
59
|
-
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
refresh_ledger if ledger_stale?
|
|
85
|
+
@ledger.empty?
|
|
86
|
+
end
|
|
60
87
|
end
|
|
61
88
|
|
|
62
89
|
private
|
|
63
90
|
|
|
91
|
+
def ledger_stale?
|
|
92
|
+
@ledger.nil? || Process.clock_gettime(Process::CLOCK_MONOTONIC) - @ledger_at > LEDGER_TTL
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Caller holds @mutex.
|
|
96
|
+
def refresh_ledger
|
|
97
|
+
@ledger = pending_files.map { |path| [path, size(path)] }
|
|
98
|
+
@ledger_bytes = @ledger.sum { |_, bytes| bytes }
|
|
99
|
+
@ledger_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
100
|
+
end
|
|
101
|
+
|
|
64
102
|
# Oldest-first. mtime is the primary key; the zero-padded sequence in the filename breaks
|
|
65
103
|
# ties (and preserves per-process write order when mtimes collide at coarse FS resolution).
|
|
66
104
|
def pending_files
|
|
@@ -73,6 +111,18 @@ module Onlylogs
|
|
|
73
111
|
Time.at(0)
|
|
74
112
|
end
|
|
75
113
|
|
|
114
|
+
# Replay is oldest-first and so is the ledger, so the path is nearly always the head.
|
|
115
|
+
def forget(path)
|
|
116
|
+
@mutex.synchronize do
|
|
117
|
+
next if @ledger.nil?
|
|
118
|
+
|
|
119
|
+
index = (@ledger.first&.first == path) ? 0 : @ledger.index { |candidate, _| candidate == path }
|
|
120
|
+
next unless index
|
|
121
|
+
|
|
122
|
+
@ledger_bytes -= @ledger.delete_at(index).last
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
76
126
|
def read(path)
|
|
77
127
|
::File.binread(path)
|
|
78
128
|
rescue Errno::ENOENT
|
|
@@ -85,14 +135,12 @@ module Onlylogs
|
|
|
85
135
|
nil
|
|
86
136
|
end
|
|
87
137
|
|
|
88
|
-
# Delete oldest batches until `incoming` more bytes fit under the cap.
|
|
138
|
+
# Delete oldest batches until `incoming` more bytes fit under the cap. Caller holds @mutex.
|
|
89
139
|
def evict(incoming)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
total -= size(oldest)
|
|
95
|
-
delete(oldest)
|
|
140
|
+
while @ledger_bytes + incoming > @max_bytes && (oldest = @ledger.shift)
|
|
141
|
+
path, bytes = oldest
|
|
142
|
+
delete(path)
|
|
143
|
+
@ledger_bytes -= bytes
|
|
96
144
|
end
|
|
97
145
|
end
|
|
98
146
|
|
data/lib/onlylogs/version.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.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessandro Rodi
|
|
@@ -58,6 +58,7 @@ files:
|
|
|
58
58
|
- app/javascript/onlylogs/controllers/index.js
|
|
59
59
|
- app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js
|
|
60
60
|
- app/javascript/onlylogs/controllers/log_streamer_controller.js
|
|
61
|
+
- app/javascript/onlylogs/controllers/range_slider_controller.js
|
|
61
62
|
- app/javascript/onlylogs/controllers/text_selection_controller.js
|
|
62
63
|
- app/jobs/onlylogs/application_job.rb
|
|
63
64
|
- app/models/onlylogs/ansi_color_parser.rb
|
|
@@ -80,6 +81,7 @@ files:
|
|
|
80
81
|
- db/migrate/20250902112548_create_books.rb
|
|
81
82
|
- lib/onlylogs.rb
|
|
82
83
|
- lib/onlylogs/configuration.rb
|
|
84
|
+
- lib/onlylogs/continuous_log_writer.rb
|
|
83
85
|
- lib/onlylogs/engine.rb
|
|
84
86
|
- lib/onlylogs/formatter.rb
|
|
85
87
|
- lib/onlylogs/http_device.rb
|