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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 518026f9d08e58ddd2a6c1f0727a92be011321202d5cd788db339678e65338af
4
- data.tar.gz: 0d08f35ebdc02e31399e5ab4579c9e71a7aa1691bb549cd79eeda355715f45c8
3
+ metadata.gz: b61c337aaf2f09bc9cf2ae34db008430f232d7d585044fa4cf813162f1db6b69
4
+ data.tar.gz: 453b54b4571861800273a345dd1c7142b742daaddb05a3a30e8b48d21c5c7575
5
5
  SHA512:
6
- metadata.gz: 7dd496ca2596dfd8a5a4e35093752f88c1e37924ce1595e5c53c4c7c8ab4c53f3e872104dcc9fd35fb1c3fa268ed72d1619c38ed0878336bc6a2fadfa76e08be
7
- data.tar.gz: d39e9736a4738b87bb07204008d7c46b21a504022421b4612e480fcd7b74998fe946e10748f0cc9653e60d77c56a6d9fadef811d11e51fc9c4852d3b2a9beeee
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.2.5
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
- log.debug(message)
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
- log.info(message)
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
- log.warn(message)
43
- fire_hooks(:warn, raw)
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
- log.error(message)
53
- fire_hooks(:error, raw)
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
- log.unknown(message)
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?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Logging
5
- VERSION = '1.2.8'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
@@ -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.2.8
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