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.
@@ -1,73 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
-
5
- # This logger sends messages to onlylogs.io via a UNIX socket connected to the onlylogs sidecar process.
6
- # You need to have the onlylogs sidecar running for this to work.
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
- DEFAULT_SOCKET = "tmp/sockets/onlylogs-sidecar.sock"
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
- @socket
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
- def reconnect_socket
63
- @socket_mutex.synchronize do
64
- begin
65
- @socket&.close
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
@@ -1,3 +1,3 @@
1
1
  module Onlylogs
2
- VERSION = "0.5.2"
2
+ VERSION = "0.7.0"
3
3
  end
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.5.2
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