onlylogs 0.5.2 → 0.7.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/app/assets/config/onlylogs_manifest.js +2 -0
- data/app/assets/javascripts/onlylogs/clusterize.js +342 -0
- data/app/assets/stylesheets/onlylogs/clusterize.css +35 -0
- data/app/channels/onlylogs/logs_channel.rb +110 -32
- data/app/controllers/onlylogs/logs_controller.rb +0 -7
- data/app/javascript/onlylogs/controllers/log_streamer_controller.js +431 -53
- data/app/models/onlylogs/grep.rb +31 -4
- data/app/views/onlylogs/logs/index.html.erb +1 -1
- data/app/views/onlylogs/shared/_log_container.html.erb +44 -21
- data/app/views/onlylogs/shared/_log_container_styles.html.erb +193 -13
- data/app/views/onlylogs/shared/_range_slider.html.erb +47 -0
- data/bin/super_grep +4 -1
- data/bin/super_ripgrep +10 -2
- data/lib/onlylogs/engine.rb +1 -1
- data/lib/onlylogs/http_device.rb +317 -0
- data/lib/onlylogs/http_logger.rb +21 -189
- data/lib/onlylogs/multi_device.rb +30 -0
- data/lib/onlylogs/socket_device.rb +60 -0
- data/lib/onlylogs/socket_logger.rb +17 -63
- data/lib/onlylogs/spool.rb +105 -0
- data/lib/onlylogs/version.rb +1 -1
- data/lib/onlylogs.rb +4 -0
- metadata +8 -1
|
@@ -1,73 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
|
|
3
|
+
require_relative "socket_device"
|
|
4
|
+
require_relative "multi_device"
|
|
5
|
+
|
|
6
|
+
# Logs to $stdout (local fallback) and to onlylogs.io via a UNIX socket connected to the onlylogs
|
|
7
|
+
# sidecar process. You need to have the onlylogs sidecar running for the socket sink to work.
|
|
8
|
+
#
|
|
9
|
+
# This is a plain Onlylogs::Logger whose log device is the sidecar socket teed with the local
|
|
10
|
+
# fallback. It deliberately does NOT override #add: the stock Logger#add applies the level and
|
|
11
|
+
# formats each line before writing, so a below-level line reaches neither sink.
|
|
8
12
|
module Onlylogs
|
|
9
13
|
class SocketLogger < Onlylogs::Logger
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def initialize(local_fallback: $stdout, socket_path: ENV.fetch("ONLYLOGS_SIDECAR_SOCKET", DEFAULT_SOCKET))
|
|
13
|
-
super(local_fallback)
|
|
14
|
-
@socket_path = socket_path
|
|
15
|
-
@socket_mutex = Mutex.new
|
|
16
|
-
@socket = nil
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def add(severity, message = nil, progname = nil, &block)
|
|
20
|
-
if message.nil?
|
|
21
|
-
if block_given?
|
|
22
|
-
message = block.call
|
|
23
|
-
else
|
|
24
|
-
message = progname
|
|
25
|
-
progname = nil
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
formatted = format_message(format_severity(severity), Time.now, progname, message.to_s)
|
|
30
|
-
send_to_socket(formatted)
|
|
31
|
-
super
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def send_to_socket(payload)
|
|
37
|
-
return if payload.nil? || payload.empty?
|
|
38
|
-
|
|
39
|
-
socket = ensure_socket
|
|
40
|
-
socket&.puts(payload)
|
|
41
|
-
rescue Errno::EPIPE, Errno::ECONNREFUSED, Errno::ENOENT => e
|
|
42
|
-
$stderr.puts "Onlylogs::SocketLogger error: #{e.message}" # rubocop:disable Style/StderrPuts
|
|
43
|
-
reconnect_socket
|
|
44
|
-
rescue => e
|
|
45
|
-
$stderr.puts "Onlylogs::SocketLogger unexpected error: #{e.class}: #{e.message}" # rubocop:disable Style/StderrPuts
|
|
46
|
-
reconnect_socket
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def ensure_socket
|
|
50
|
-
return @socket if @socket
|
|
51
|
-
|
|
52
|
-
@socket_mutex.synchronize do
|
|
53
|
-
@socket ||= UNIXSocket.new(@socket_path)
|
|
54
|
-
rescue => e
|
|
55
|
-
$stderr.puts "Unable to connect to Onlylogs sidecar (#{@socket_path}): #{e.message}" # rubocop:disable Style/StderrPuts
|
|
56
|
-
@socket = nil
|
|
57
|
-
end
|
|
14
|
+
attr_reader :device
|
|
58
15
|
|
|
59
|
-
|
|
16
|
+
def initialize(local_fallback: $stdout, socket_path: ENV.fetch("ONLYLOGS_SIDECAR_SOCKET", SocketDevice::DEFAULT_SOCKET))
|
|
17
|
+
@device = SocketDevice.new(socket_path: socket_path)
|
|
18
|
+
super(MultiDevice.new(local_fallback, @device))
|
|
60
19
|
end
|
|
61
20
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
rescue
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
@socket = nil
|
|
70
|
-
end
|
|
21
|
+
# Only the remote socket is ours to close; the local fallback ($stdout) belongs to the app, so
|
|
22
|
+
# we deliberately do not call super (which would close the whole log device, fallback included).
|
|
23
|
+
def close
|
|
24
|
+
@device.close
|
|
71
25
|
end
|
|
72
26
|
end
|
|
73
27
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Onlylogs
|
|
7
|
+
# A bounded, on-disk overflow buffer for log batches that could not be delivered.
|
|
8
|
+
#
|
|
9
|
+
# HttpLogger keeps the happy path in memory: only when a send fails or the circuit is open does
|
|
10
|
+
# a batch get written here, to be replayed once the drain recovers (and on the next boot).
|
|
11
|
+
# This turns transient-failure / restart data loss into at-least-once delivery: a batch that was in
|
|
12
|
+
# fact received but whose response was lost will be replayed and show up as a duplicate
|
|
13
|
+
# downstream. Duplicates are an accepted trade for not losing data.
|
|
14
|
+
class Spool
|
|
15
|
+
DEFAULT_MAX_BYTES = 128 * 1024 * 1024 # 128 MB
|
|
16
|
+
|
|
17
|
+
def initialize(dir:, max_bytes: DEFAULT_MAX_BYTES)
|
|
18
|
+
@dir = dir
|
|
19
|
+
@max_bytes = max_bytes
|
|
20
|
+
# Unique per instance so two runs (even with a reused pid) never collide on a filename.
|
|
21
|
+
@token = SecureRandom.hex(4)
|
|
22
|
+
@seq = 0
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
::FileUtils.mkdir_p(@dir)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Persist a batch body. Rolls the oldest batches off first if the byte cap would be exceeded.
|
|
28
|
+
def write(body)
|
|
29
|
+
return if body.nil? || body.empty?
|
|
30
|
+
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
evict(body.bytesize)
|
|
33
|
+
seq = (@seq += 1)
|
|
34
|
+
final = ::File.join(@dir, "#{@token}-#{format("%09d", seq)}.batch")
|
|
35
|
+
tmp = "#{final}.tmp"
|
|
36
|
+
# Write to a temp name then rename: rename is atomic, so replay never reads a
|
|
37
|
+
# half-written file (it only globs *.batch).
|
|
38
|
+
::File.binwrite(tmp, body)
|
|
39
|
+
::File.rename(tmp, final)
|
|
40
|
+
end
|
|
41
|
+
rescue => e
|
|
42
|
+
Kernel.warn "Onlylogs::Spool write error: #{e.class}: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Replay pending batches oldest-first. Yields each body; if the block returns truthy the file
|
|
46
|
+
# is deleted (delivered), otherwise replay stops and the remaining files are kept for later.
|
|
47
|
+
def replay
|
|
48
|
+
pending_files.each do |path|
|
|
49
|
+
body = read(path)
|
|
50
|
+
next if body.nil? # already claimed/deleted by another process
|
|
51
|
+
|
|
52
|
+
break unless yield(body)
|
|
53
|
+
|
|
54
|
+
delete(path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def empty?
|
|
59
|
+
pending_files.empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Oldest-first. mtime is the primary key; the zero-padded sequence in the filename breaks
|
|
65
|
+
# ties (and preserves per-process write order when mtimes collide at coarse FS resolution).
|
|
66
|
+
def pending_files
|
|
67
|
+
::Dir.glob(::File.join(@dir, "*.batch")).sort_by { |path| [mtime(path), path] }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def mtime(path)
|
|
71
|
+
::File.mtime(path)
|
|
72
|
+
rescue Errno::ENOENT
|
|
73
|
+
Time.at(0)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def read(path)
|
|
77
|
+
::File.binread(path)
|
|
78
|
+
rescue Errno::ENOENT
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def delete(path)
|
|
83
|
+
::File.delete(path)
|
|
84
|
+
rescue Errno::ENOENT
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Delete oldest batches until `incoming` more bytes fit under the cap.
|
|
89
|
+
def evict(incoming)
|
|
90
|
+
files = pending_files
|
|
91
|
+
total = files.sum { |path| size(path) }
|
|
92
|
+
|
|
93
|
+
while total + incoming > @max_bytes && (oldest = files.shift)
|
|
94
|
+
total -= size(oldest)
|
|
95
|
+
delete(oldest)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def size(path)
|
|
100
|
+
::File.size(path)
|
|
101
|
+
rescue Errno::ENOENT
|
|
102
|
+
0
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/onlylogs/version.rb
CHANGED
data/lib/onlylogs.rb
CHANGED
|
@@ -3,6 +3,10 @@ require "onlylogs/configuration"
|
|
|
3
3
|
require "onlylogs/engine"
|
|
4
4
|
require "onlylogs/formatter"
|
|
5
5
|
require "onlylogs/logger"
|
|
6
|
+
require "onlylogs/spool"
|
|
7
|
+
require "onlylogs/multi_device"
|
|
8
|
+
require "onlylogs/socket_device"
|
|
9
|
+
require "onlylogs/http_device"
|
|
6
10
|
require "onlylogs/socket_logger"
|
|
7
11
|
require "onlylogs/http_logger"
|
|
8
12
|
|
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.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alessandro Rodi
|
|
@@ -45,6 +45,8 @@ files:
|
|
|
45
45
|
- app/assets/images/onlylogs/favicon/web-app-manifest-192x192.png
|
|
46
46
|
- app/assets/images/onlylogs/favicon/web-app-manifest-512x512.png
|
|
47
47
|
- app/assets/images/onlylogs/logo.png
|
|
48
|
+
- app/assets/javascripts/onlylogs/clusterize.js
|
|
49
|
+
- app/assets/stylesheets/onlylogs/clusterize.css
|
|
48
50
|
- app/channels/onlylogs/application_cable/channel.rb
|
|
49
51
|
- app/channels/onlylogs/logs_channel.rb
|
|
50
52
|
- app/controllers/onlylogs/application_controller.rb
|
|
@@ -69,6 +71,7 @@ files:
|
|
|
69
71
|
- app/views/onlylogs/logs/index.html.erb
|
|
70
72
|
- app/views/onlylogs/shared/_log_container.html.erb
|
|
71
73
|
- app/views/onlylogs/shared/_log_container_styles.html.erb
|
|
74
|
+
- app/views/onlylogs/shared/_range_slider.html.erb
|
|
72
75
|
- bin/onlylogs_sidecar
|
|
73
76
|
- bin/super_grep
|
|
74
77
|
- bin/super_ripgrep
|
|
@@ -79,9 +82,13 @@ files:
|
|
|
79
82
|
- lib/onlylogs/configuration.rb
|
|
80
83
|
- lib/onlylogs/engine.rb
|
|
81
84
|
- lib/onlylogs/formatter.rb
|
|
85
|
+
- lib/onlylogs/http_device.rb
|
|
82
86
|
- lib/onlylogs/http_logger.rb
|
|
83
87
|
- lib/onlylogs/logger.rb
|
|
88
|
+
- lib/onlylogs/multi_device.rb
|
|
89
|
+
- lib/onlylogs/socket_device.rb
|
|
84
90
|
- lib/onlylogs/socket_logger.rb
|
|
91
|
+
- lib/onlylogs/spool.rb
|
|
85
92
|
- lib/onlylogs/version.rb
|
|
86
93
|
- lib/puma/plugin/onlylogs_sidecar.rb
|
|
87
94
|
- lib/tasks/onlylogs_tasks.rake
|