legion-logging 1.2.8 → 1.3.1

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: 54638d1c2311825640cda666c7900b1bdd091ab5d8c7a322fdf72a22f7c1ec2a
4
+ data.tar.gz: 3ed84565ebc83f04d7d785b2581efadcb4e99a64138f0c1e37920ee4fb78bc94
5
5
  SHA512:
6
- metadata.gz: 7dd496ca2596dfd8a5a4e35093752f88c1e37924ce1595e5c53c4c7c8ab4c53f3e872104dcc9fd35fb1c3fa268ed72d1619c38ed0878336bc6a2fadfa76e08be
7
- data.tar.gz: d39e9736a4738b87bb07204008d7c46b21a504022421b4612e480fcd7b74998fe946e10748f0cc9653e60d77c56a6d9fadef811d11e51fc9c4852d3b2a9beeee
6
+ metadata.gz: 45c750d7a564b0ce713abf50e985fd3749d271c86136600738f3648921d375db75564a0809fe861fb80fb3653a92ec2e1e3120858dd019261d1b9da26de9cee4
7
+ data.tar.gz: c28935a6b7992cc8db82df7b2fa75e008ee60b520a3bc82752c5d6caaaa6a407a19e0ac944d2c366b84167a9f2032bad489c3f1bec3636feef171ebffac1b1cf
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.3.1] - 2026-03-22
6
+
7
+ ### Fixed
8
+ - Replace `Legion::Settings[:logging, :shipper, ...]` multi-arg bracket calls with `Legion::Settings.dig(...)` — `Settings#[]` only accepts 1 argument, causing `ArgumentError: wrong number of arguments (given 3, expected 1)` on boot
9
+ - Affected: `logging.rb` (async buffer_size), `shipper.rb` (5 calls), `redactor.rb`, `file_transport.rb`, `http_transport.rb` (2 calls)
10
+
11
+ ## [1.3.0] - 2026-03-22
12
+
13
+ ### Added
14
+ - `Legion::Logging::AsyncWriter`: non-blocking log writer using `SizedQueue` and a dedicated background thread
15
+ - Async mode enabled by default on `setup(async: true)` — log calls return immediately
16
+ - Configurable buffer size via `Legion::Settings[:logging, :async, :buffer_size]` (default: 10,000)
17
+ - Back-pressure: callers block when buffer is full (preserves log completeness)
18
+ - `fatal` calls always bypass the async queue (synchronous write)
19
+ - `async?`, `start_async_writer`, `stop_async_writer` methods on both singleton and Logger instances
20
+ - Hook callbacks (`on_error`, `on_warn`) fire on the writer thread; event context captured on caller thread
21
+
22
+ ### Changed
23
+ - `setup` method now accepts `async:` keyword (default: `true`)
24
+ - `Logger.new` now accepts `async:` keyword (default: `false` for backward compatibility)
25
+
5
26
  ## [1.2.8] - 2026-03-22
6
27
 
7
28
  ### 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?
@@ -60,7 +60,7 @@ module Legion
60
60
  def custom_patterns
61
61
  return {} unless defined?(Legion::Settings)
62
62
 
63
- raw = Legion::Settings[:logging, :redactor, :custom_patterns]
63
+ raw = Legion::Settings.dig(:logging, :redactor, :custom_patterns)
64
64
  return {} unless raw.is_a?(Hash)
65
65
 
66
66
  raw.each_with_object({}) do |(name, pattern_str), acc|
@@ -33,7 +33,7 @@ module Legion
33
33
  def settings_path
34
34
  return nil unless defined?(Legion::Settings)
35
35
 
36
- Legion::Settings[:logging, :shipper, :file, :path]
36
+ Legion::Settings.dig(:logging, :shipper, :file, :path)
37
37
  end
38
38
  end
39
39
  end
@@ -64,13 +64,13 @@ module Legion
64
64
  def auth_token
65
65
  return nil unless defined?(Legion::Settings)
66
66
 
67
- Legion::Settings[:logging, :shipper, :auth_token]
67
+ Legion::Settings.dig(:logging, :shipper, :auth_token)
68
68
  end
69
69
 
70
70
  def resolve_endpoint
71
71
  return nil unless defined?(Legion::Settings)
72
72
 
73
- Legion::Settings[:logging, :shipper, :endpoint]
73
+ Legion::Settings.dig(:logging, :shipper, :endpoint)
74
74
  end
75
75
  end
76
76
  end
@@ -64,7 +64,7 @@ module Legion
64
64
  def enabled?
65
65
  return false unless defined?(Legion::Settings)
66
66
 
67
- Legion::Settings[:logging, :shipper, :enabled] == true
67
+ Legion::Settings.dig(:logging, :shipper, :enabled) == true
68
68
  end
69
69
 
70
70
  private
@@ -103,26 +103,26 @@ module Legion
103
103
  def transport_type
104
104
  return :file unless defined?(Legion::Settings)
105
105
 
106
- key = Legion::Settings[:logging, :shipper, :transport]
106
+ key = Legion::Settings.dig(:logging, :shipper, :transport)
107
107
  key ? key.to_sym : :file
108
108
  end
109
109
 
110
110
  def batch_size
111
111
  return 100 unless defined?(Legion::Settings)
112
112
 
113
- Legion::Settings[:logging, :shipper, :batch_size] || 100
113
+ Legion::Settings.dig(:logging, :shipper, :batch_size) || 100
114
114
  end
115
115
 
116
116
  def flush_interval
117
117
  return 5 unless defined?(Legion::Settings)
118
118
 
119
- Legion::Settings[:logging, :shipper, :flush_interval] || 5
119
+ Legion::Settings.dig(:logging, :shipper, :flush_interval) || 5
120
120
  end
121
121
 
122
122
  def minimum_level
123
123
  return 'warn' unless defined?(Legion::Settings)
124
124
 
125
- levels = Legion::Settings[:logging, :shipper, :levels]
125
+ levels = Legion::Settings.dig(:logging, :shipper, :levels)
126
126
  return 'warn' unless levels.is_a?(Array) && !levels.empty?
127
127
 
128
128
  levels.min_by { |l| LEVEL_ORDER.index(l.to_s) || 99 }.to_s
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Logging
5
- VERSION = '1.2.8'
5
+ VERSION = '1.3.1'
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.dig(: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.1
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