legion-logging 1.2.8 → 1.3.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/CHANGELOG.md +15 -0
- data/README.md +18 -1
- data/lib/legion/logging/async_writer.rb +80 -0
- data/lib/legion/logging/builder.rb +19 -0
- data/lib/legion/logging/logger.rb +2 -1
- data/lib/legion/logging/methods.rb +51 -7
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +12 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b61c337aaf2f09bc9cf2ae34db008430f232d7d585044fa4cf813162f1db6b69
|
|
4
|
+
data.tar.gz: 453b54b4571861800273a345dd1c7142b742daaddb05a3a30e8b48d21c5c7575
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a819edf335d1c5c38bb431ac5368af501ffefed138c31a32f55bde4eb2ce155b98c26ef9973c798b98ced32330e48e887be3a711d86db6c011651c5ff7c485af
|
|
7
|
+
data.tar.gz: d1bf28077c9fc1de289cbf936ce0cea292ea6ca380dc4d6ee2294d8db0a30fac22ef571794b9c1881d8f743b32dae783ffc772114250d50dfaf2e54afa046d3b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.3.0] - 2026-03-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `Legion::Logging::AsyncWriter`: non-blocking log writer using `SizedQueue` and a dedicated background thread
|
|
9
|
+
- Async mode enabled by default on `setup(async: true)` — log calls return immediately
|
|
10
|
+
- Configurable buffer size via `Legion::Settings[:logging, :async, :buffer_size]` (default: 10,000)
|
|
11
|
+
- Back-pressure: callers block when buffer is full (preserves log completeness)
|
|
12
|
+
- `fatal` calls always bypass the async queue (synchronous write)
|
|
13
|
+
- `async?`, `start_async_writer`, `stop_async_writer` methods on both singleton and Logger instances
|
|
14
|
+
- Hook callbacks (`on_error`, `on_warn`) fire on the writer thread; event context captured on caller thread
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `setup` method now accepts `async:` keyword (default: `true`)
|
|
18
|
+
- `Logger.new` now accepts `async:` keyword (default: `false` for backward compatibility)
|
|
19
|
+
|
|
5
20
|
## [1.2.8] - 2026-03-22
|
|
6
21
|
|
|
7
22
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Logging module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides colorized console output via Rainbow, structured JSON logging, multi-output IO, and a consistent logging interface across all Legion gems and extensions.
|
|
4
4
|
|
|
5
|
-
**Version**: 1.
|
|
5
|
+
**Version**: 1.3.0
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -31,6 +31,23 @@ Legion::Logging.error('something went wrong')
|
|
|
31
31
|
Legion::Logging.fatal('critical failure')
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
### Async Logging
|
|
35
|
+
|
|
36
|
+
By default, `setup` enables async logging — log calls push to a background writer thread and return immediately. Fatal calls always bypass the queue and write synchronously.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Async is on by default
|
|
40
|
+
Legion::Logging.setup(level: 'info')
|
|
41
|
+
|
|
42
|
+
# Disable async (synchronous mode)
|
|
43
|
+
Legion::Logging.setup(level: 'info', async: false)
|
|
44
|
+
|
|
45
|
+
# Configure buffer size via Legion::Settings
|
|
46
|
+
# Legion::Settings[:logging, :async, :buffer_size] = 20_000
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
When the buffer is full, callers block until the writer drains — this preserves log completeness. Use `Legion::Logging.stop_async_writer` during shutdown to flush and stop the writer thread.
|
|
50
|
+
|
|
34
51
|
### Structured JSON Output
|
|
35
52
|
|
|
36
53
|
Pass `format: :json` to disable colorization and emit machine-parseable JSON log lines:
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Logging
|
|
5
|
+
class AsyncWriter
|
|
6
|
+
LogEntry = ::Data.define(:level, :message, :hook_context)
|
|
7
|
+
SHUTDOWN = :shutdown
|
|
8
|
+
|
|
9
|
+
def initialize(logger, buffer_size: 10_000)
|
|
10
|
+
@logger = logger
|
|
11
|
+
@queue = SizedQueue.new(buffer_size)
|
|
12
|
+
@thread = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def start
|
|
16
|
+
return if @thread&.alive?
|
|
17
|
+
|
|
18
|
+
drain
|
|
19
|
+
@thread = Thread.new { consume }
|
|
20
|
+
@thread.name = 'legion-log-writer'
|
|
21
|
+
@thread.abort_on_exception = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stop(timeout: 2)
|
|
25
|
+
return unless @thread&.alive?
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
@queue.push(SHUTDOWN, true)
|
|
29
|
+
rescue ThreadError
|
|
30
|
+
# Queue full — fall through to join/kill + drain
|
|
31
|
+
end
|
|
32
|
+
@thread.join(timeout)
|
|
33
|
+
@thread.kill if @thread&.alive?
|
|
34
|
+
drain
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def push(entry)
|
|
38
|
+
@queue.push(entry)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def alive?
|
|
42
|
+
@thread&.alive? || false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def consume
|
|
48
|
+
loop do
|
|
49
|
+
entry = @queue.pop
|
|
50
|
+
break if entry == SHUTDOWN
|
|
51
|
+
|
|
52
|
+
write_entry(entry)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def write_entry(entry)
|
|
57
|
+
@logger.send(entry.level, entry.message)
|
|
58
|
+
fire_hooks(entry) if entry.hook_context
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
warn("legion-log-writer error: #{e.message} (#{e.backtrace&.first})")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def drain
|
|
64
|
+
until @queue.empty?
|
|
65
|
+
entry = @queue.pop(true)
|
|
66
|
+
write_entry(entry) unless entry == SHUTDOWN
|
|
67
|
+
end
|
|
68
|
+
rescue ThreadError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fire_hooks(entry)
|
|
73
|
+
ctx = entry.hook_context
|
|
74
|
+
Legion::Logging::Hooks.fire(ctx[:level], ctx[:event])
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
warn("legion-log-writer hook error: #{e.message}")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -125,6 +125,25 @@ module Legion
|
|
|
125
125
|
end
|
|
126
126
|
@log = log
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
def async?
|
|
130
|
+
(@async == true && @async_writer&.alive?) || false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def start_async_writer(buffer_size: 10_000)
|
|
134
|
+
require_relative 'async_writer'
|
|
135
|
+
stop_async_writer if @async_writer&.alive?
|
|
136
|
+
@async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
|
|
137
|
+
@async_writer.start
|
|
138
|
+
@async = true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def stop_async_writer
|
|
142
|
+
writer = @async_writer
|
|
143
|
+
@async_writer = nil
|
|
144
|
+
@async = false
|
|
145
|
+
writer&.stop
|
|
146
|
+
end
|
|
128
147
|
end
|
|
129
148
|
end
|
|
130
149
|
end
|
|
@@ -11,7 +11,7 @@ module Legion
|
|
|
11
11
|
include Legion::Logging::Methods
|
|
12
12
|
include Legion::Logging::Builder
|
|
13
13
|
|
|
14
|
-
def initialize(level: 'info', log_file: nil, log_stdout: nil, lex: nil, trace: false, extended: false, trace_size: 4, format: :text, **opts) # rubocop:disable Metrics/ParameterLists
|
|
14
|
+
def initialize(level: 'info', log_file: nil, log_stdout: nil, lex: nil, trace: false, extended: false, trace_size: 4, format: :text, async: false, **opts) # rubocop:disable Metrics/ParameterLists
|
|
15
15
|
@lex = lex
|
|
16
16
|
set_log(logfile: log_file, log_stdout: log_stdout)
|
|
17
17
|
log_level(level)
|
|
@@ -21,6 +21,7 @@ module Legion
|
|
|
21
21
|
@trace_enabled = trace
|
|
22
22
|
@trace_size = trace_size
|
|
23
23
|
@extended = extended
|
|
24
|
+
start_async_writer if async
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -22,7 +22,12 @@ module Legion
|
|
|
22
22
|
|
|
23
23
|
message = yield if message.nil? && block_given?
|
|
24
24
|
message = Rainbow(message).blue if @color
|
|
25
|
-
|
|
25
|
+
writer = @async_writer
|
|
26
|
+
if writer&.alive?
|
|
27
|
+
writer.push(AsyncWriter::LogEntry.new(level: :debug, message: message, hook_context: nil))
|
|
28
|
+
else
|
|
29
|
+
log.debug(message)
|
|
30
|
+
end
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
def info(message = nil)
|
|
@@ -30,7 +35,12 @@ module Legion
|
|
|
30
35
|
|
|
31
36
|
message = yield if message.nil? && block_given?
|
|
32
37
|
message = Rainbow(message).green if @color
|
|
33
|
-
|
|
38
|
+
writer = @async_writer
|
|
39
|
+
if writer&.alive?
|
|
40
|
+
writer.push(AsyncWriter::LogEntry.new(level: :info, message: message, hook_context: nil))
|
|
41
|
+
else
|
|
42
|
+
log.info(message)
|
|
43
|
+
end
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
def warn(message = nil)
|
|
@@ -39,8 +49,14 @@ module Legion
|
|
|
39
49
|
message = yield if message.nil? && block_given?
|
|
40
50
|
raw = message
|
|
41
51
|
message = Rainbow(message).yellow if @color
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
writer = @async_writer
|
|
53
|
+
if writer&.alive?
|
|
54
|
+
ctx = build_hook_context(:warn, raw)
|
|
55
|
+
writer.push(AsyncWriter::LogEntry.new(level: :warn, message: message, hook_context: ctx))
|
|
56
|
+
else
|
|
57
|
+
log.warn(message)
|
|
58
|
+
fire_hooks(:warn, raw)
|
|
59
|
+
end
|
|
44
60
|
end
|
|
45
61
|
|
|
46
62
|
def error(message = nil)
|
|
@@ -49,8 +65,14 @@ module Legion
|
|
|
49
65
|
message = yield if message.nil? && block_given?
|
|
50
66
|
raw = message
|
|
51
67
|
message = Rainbow(message).red if @color
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
writer = @async_writer
|
|
69
|
+
if writer&.alive?
|
|
70
|
+
ctx = build_hook_context(:error, raw)
|
|
71
|
+
writer.push(AsyncWriter::LogEntry.new(level: :error, message: message, hook_context: ctx))
|
|
72
|
+
else
|
|
73
|
+
log.error(message)
|
|
74
|
+
fire_hooks(:error, raw)
|
|
75
|
+
end
|
|
54
76
|
end
|
|
55
77
|
|
|
56
78
|
def fatal(message = nil)
|
|
@@ -66,7 +88,12 @@ module Legion
|
|
|
66
88
|
def unknown(message = nil)
|
|
67
89
|
message = yield if message.nil? && block_given?
|
|
68
90
|
message = Rainbow(message).purple if @color
|
|
69
|
-
|
|
91
|
+
writer = @async_writer
|
|
92
|
+
if writer&.alive?
|
|
93
|
+
writer.push(AsyncWriter::LogEntry.new(level: :unknown, message: message, hook_context: nil))
|
|
94
|
+
else
|
|
95
|
+
log.unknown(message)
|
|
96
|
+
end
|
|
70
97
|
end
|
|
71
98
|
|
|
72
99
|
def runner_exception(exc, **opts)
|
|
@@ -86,6 +113,23 @@ module Legion
|
|
|
86
113
|
|
|
87
114
|
private
|
|
88
115
|
|
|
116
|
+
def build_hook_context(level, message)
|
|
117
|
+
return nil unless Legion::Logging::Hooks.enabled?
|
|
118
|
+
return nil if Legion::Logging::Hooks.hooks[level].empty?
|
|
119
|
+
|
|
120
|
+
lex_val = instance_variable_defined?(:@lex) ? @lex : nil
|
|
121
|
+
lex_segs = instance_variable_defined?(:@lex_segments) ? @lex_segments : nil
|
|
122
|
+
|
|
123
|
+
event = Legion::Logging::EventBuilder.build(
|
|
124
|
+
level: level,
|
|
125
|
+
message: message,
|
|
126
|
+
lex: lex_val,
|
|
127
|
+
lex_segments: lex_segs,
|
|
128
|
+
caller_offset: 4
|
|
129
|
+
)
|
|
130
|
+
{ level: level, event: event }
|
|
131
|
+
end
|
|
132
|
+
|
|
89
133
|
def fire_hooks(level, message)
|
|
90
134
|
return unless Legion::Logging::Hooks.enabled?
|
|
91
135
|
return if Legion::Logging::Hooks.hooks[level].empty?
|
data/lib/legion/logging.rb
CHANGED
|
@@ -6,6 +6,7 @@ require 'legion/logging/methods'
|
|
|
6
6
|
require 'legion/logging/builder'
|
|
7
7
|
require 'legion/logging/hooks'
|
|
8
8
|
require 'legion/logging/event_builder'
|
|
9
|
+
require 'legion/logging/async_writer'
|
|
9
10
|
|
|
10
11
|
require 'json'
|
|
11
12
|
require 'logger'
|
|
@@ -26,12 +27,22 @@ module Legion
|
|
|
26
27
|
|
|
27
28
|
attr_reader :color
|
|
28
29
|
|
|
29
|
-
def setup(level: 'info', format: :text, **options)
|
|
30
|
+
def setup(level: 'info', format: :text, async: true, **options)
|
|
30
31
|
output(**options)
|
|
31
32
|
log_level(level)
|
|
32
33
|
log_format(format: format, **options)
|
|
33
34
|
@color = options[:color]
|
|
34
35
|
@color = format != :json && (options[:color] || (options[:color].nil? && options[:log_file].nil?))
|
|
36
|
+
if async
|
|
37
|
+
buffer = if defined?(Legion::Settings)
|
|
38
|
+
Legion::Settings[:logging, :async, :buffer_size] || 10_000
|
|
39
|
+
else
|
|
40
|
+
10_000
|
|
41
|
+
end
|
|
42
|
+
start_async_writer(buffer_size: buffer)
|
|
43
|
+
else
|
|
44
|
+
stop_async_writer
|
|
45
|
+
end
|
|
35
46
|
end
|
|
36
47
|
end
|
|
37
48
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-logging
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- README.md
|
|
58
58
|
- legion-logging.gemspec
|
|
59
59
|
- lib/legion/logging.rb
|
|
60
|
+
- lib/legion/logging/async_writer.rb
|
|
60
61
|
- lib/legion/logging/builder.rb
|
|
61
62
|
- lib/legion/logging/event_builder.rb
|
|
62
63
|
- lib/legion/logging/hooks.rb
|