legion-logging 1.4.3 → 1.5.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: 707d2460ca4e016ca62cc8ef83bf2d646819439ed67ef5549a164518ba2d3bf5
4
- data.tar.gz: a013c59d975f901f25b153cbbfe4ed288dbedb262098627aed6d71baeb70031a
3
+ metadata.gz: 62c3c14ff8e6f67a11941a869f4f4ea2b682686c85c41bb1568368b448f23d0c
4
+ data.tar.gz: af9c1588f601a09f6b986d9dc47cc07b1c8e9b966e482b45d1a7242342e8d500
5
5
  SHA512:
6
- metadata.gz: f5f3d87c609bca88c501e32d241fabc796e48b8aae8912fed39ac3615e0565f2a7d4672b66f12b0fdf606c6d3e0b40abb2b04b9b38d554edca4e9fa4b063a40a
7
- data.tar.gz: 741697e72078cb1cbede684a8729d8a9a0b79c25a8beb4ed465aa53ae89711e11f482b9e93f434f51dae0778dfdf3bf49855282e80a7df17f98054d980d3e037
6
+ metadata.gz: 66a26f844b44432886b1d703d93dbb9547134ede3542610861e123e240b8ec89746d7bdc522ae8e831c93e212f9e1a9bff5fc9b2fa19a78dfb47007373c5e0ab
7
+ data.tar.gz: add0a140cbe3c101dc83dcfb4b1c3eef385e4c778a4b6957be6d71b2ec95a9316c3b36d7b687906867046d482b2246ad0a44e8fd4cc98472ad1157994207146a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Legion::Logging Changelog
2
2
 
3
+ ## [1.5.0] - 2026-04-02
4
+
5
+ ### Added
6
+ - `Legion::Logging.current_settings` and `.configuration_generation` so helper mixins can refresh memoized tagged loggers after runtime reconfiguration
7
+ - Component logger overrides from local `settings`, top-level `Legion::Settings[component]`, and `Legion::Settings.dig(:extensions, component)` for `log_level`, `trace`, `trace_size`, and `extended`
8
+ - `Methods#emit_tagged` / `TaggedLogger#dispatch` path so component-level loggers can emit with their own level while preserving tagged context
9
+ - Fallback exception event construction in `Helper#handle_exception` when structured exception support is unavailable
10
+
11
+ ### Changed
12
+ - `setup` and `Builder#log_level` now default to `debug`
13
+ - Default helper/tagged logger behavior enables trace and extended metadata
14
+ - `Helper#log` rebuilds memoized `TaggedLogger` instances when logging configuration changes
15
+ - Runtime logger settings take precedence over loaded global settings for helper-mixed components
16
+
17
+ ### Fixed
18
+ - `setup(async: true)` now tolerates boolean `logging.async` settings without probing for `buffer_size`
19
+ - Exception stdout/file output now falls back safely when singleton logger helpers are unavailable
20
+ - Structured exception publishing is skipped when the exception writer/EventBuilder path is unavailable
21
+ - `TaggedLogger#unknown` falls back to `debug` output when `Legion::Logging.unknown` is unavailable
22
+
3
23
  ## [1.4.3] - 2026-04-01
4
24
 
5
25
  ### Added
@@ -176,4 +196,4 @@
176
196
  - `format_for_elk` produces ELK-compatible event hashes
177
197
 
178
198
  ## v1.2.0
179
- Moving from BitBucket to GitHub. All git history is reset from this point on
199
+ Moving from BitBucket to GitHub. All git history is reset from this point on
@@ -5,40 +5,58 @@ require_relative 'methods'
5
5
  module Legion
6
6
  module Logging
7
7
  class AsyncWriter
8
- LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx)
8
+ LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx, :caller_trace)
9
9
  SHUTDOWN = :shutdown
10
10
 
11
+ attr_reader :logger
12
+
11
13
  def initialize(logger, buffer_size: 10_000)
12
14
  @logger = logger
15
+ @buffer_size = buffer_size
13
16
  @queue = SizedQueue.new(buffer_size)
14
17
  @thread = nil
18
+ @state_mutex = Mutex.new
19
+ @accepting = true
15
20
  end
16
21
 
17
22
  def start
18
23
  return if @thread&.alive?
19
24
 
25
+ @state_mutex.synchronize { @accepting = true }
20
26
  drain
27
+ @queue = SizedQueue.new(@buffer_size)
21
28
  @thread = Thread.new { consume }
22
29
  @thread.name = 'legion-log-writer'
23
30
  @thread.abort_on_exception = false
24
31
  end
25
32
 
33
+ # rubocop:disable Naming/PredicateMethod
26
34
  def stop(timeout: 2)
27
- return unless @thread&.alive?
35
+ @state_mutex.synchronize { @accepting = false }
28
36
 
29
- begin
30
- @queue.push(SHUTDOWN, true)
31
- rescue ThreadError
32
- # Queue full — fall through to join/kill + drain
37
+ unless @thread&.alive?
38
+ drain
39
+ @thread = nil
40
+ return true
33
41
  end
34
- @thread.join(timeout)
35
- @thread.kill if @thread&.alive?
36
- drain
42
+
43
+ @queue.close
44
+ timeout ? @thread.join(timeout) : @thread.join
45
+ return false if @thread&.alive?
46
+
47
+ @thread = nil
48
+ true
37
49
  end
38
50
 
39
51
  def push(entry)
52
+ return false unless accepting?
53
+
40
54
  @queue.push(entry)
55
+ true
56
+ rescue ClosedQueueError
57
+ false
41
58
  end
59
+ # rubocop:enable Naming/PredicateMethod
42
60
 
43
61
  def alive?
44
62
  @thread&.alive? || false
@@ -49,7 +67,7 @@ module Legion
49
67
  def consume
50
68
  loop do
51
69
  entry = @queue.pop
52
- break if entry == SHUTDOWN
70
+ break if entry.nil? || entry == SHUTDOWN
53
71
 
54
72
  write_entry(entry)
55
73
  end
@@ -58,8 +76,10 @@ module Legion
58
76
  def write_entry(entry)
59
77
  prev_segments = Thread.current[:legion_log_segments]
60
78
  prev_method_ctx = Thread.current[:legion_log_method]
79
+ prev_caller = Thread.current[:legion_log_caller]
61
80
  Thread.current[:legion_log_segments] = entry.segments
62
81
  Thread.current[:legion_log_method] = entry.method_ctx
82
+ Thread.current[:legion_log_caller] = entry.caller_trace
63
83
  @logger.send(entry.level, entry.message)
64
84
  fire_writer(entry) if entry.writer_context
65
85
  rescue StandardError => e
@@ -67,6 +87,7 @@ module Legion
67
87
  ensure
68
88
  Thread.current[:legion_log_segments] = prev_segments
69
89
  Thread.current[:legion_log_method] = prev_method_ctx
90
+ Thread.current[:legion_log_caller] = prev_caller
70
91
  end
71
92
 
72
93
  def drain
@@ -78,6 +99,10 @@ module Legion
78
99
  nil
79
100
  end
80
101
 
102
+ def accepting?
103
+ @state_mutex.synchronize { @accepting }
104
+ end
105
+
81
106
  def fire_writer(entry)
82
107
  ctx = entry.writer_context
83
108
  event = ctx[:event]
@@ -41,7 +41,7 @@ module Legion
41
41
  def text_format(include_pid: false, **options)
42
42
  log.formatter = proc do |severity, datetime, _progname, msg|
43
43
  lex_name = resolve_lex_tag(options)
44
- runner_trace = build_runner_trace if lex_name
44
+ runner_trace = Thread.current[:legion_log_caller] || build_runner_trace if lex_name
45
45
 
46
46
  string = "[#{datetime}]"
47
47
  string.concat("[#{::Process.pid}]") if include_pid
@@ -69,8 +69,7 @@ module Legion
69
69
  tag
70
70
  end
71
71
 
72
- def build_runner_trace
73
- loc = caller_locations(6, 1)&.first
72
+ def build_runner_trace(loc = caller_locations(6, 1)&.first)
74
73
  return unless loc
75
74
 
76
75
  path = loc.to_s.split('/').last(2)
@@ -91,16 +90,25 @@ module Legion
91
90
  end
92
91
 
93
92
  def set_log(logfile: nil, log_stdout: nil, **)
93
+ previous_log = @log
94
+
94
95
  if logfile && log_stdout != false
95
96
  path = prepare_log_path(logfile)
96
97
  require_relative 'multi_io'
97
- io = MultiIO.new($stdout, File.open(path, 'a'))
98
+ file = File.new(path, 'a')
99
+ file.sync = true
100
+ io = MultiIO.new($stdout, file)
98
101
  @log = ::Logger.new(io)
99
102
  elsif logfile
100
- @log = ::Logger.new(prepare_log_path(logfile))
103
+ file = File.new(prepare_log_path(logfile), 'a')
104
+ file.sync = true
105
+ @log = ::Logger.new(file)
101
106
  else
102
107
  @log = ::Logger.new($stdout)
103
108
  end
109
+
110
+ close_replaced_log(previous_log)
111
+ @log
104
112
  end
105
113
 
106
114
  def prepare_log_path(path)
@@ -113,7 +121,7 @@ module Legion
113
121
  log.level
114
122
  end
115
123
 
116
- def log_level(level = 'info')
124
+ def log_level(level = 'debug')
117
125
  log.level = case level
118
126
  when 'trace', 'debug'
119
127
  ::Logger::DEBUG
@@ -140,19 +148,41 @@ module Legion
140
148
  (@async == true && @async_writer&.alive?) || false
141
149
  end
142
150
 
151
+ # rubocop:disable Naming/PredicateMethod
143
152
  def start_async_writer(buffer_size: 10_000)
144
153
  require_relative 'async_writer'
145
- stop_async_writer if @async_writer&.alive?
154
+ return false if @async_writer&.alive? && stop_async_writer == false
155
+
146
156
  @async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
147
157
  @async_writer.start
148
158
  @async = true
159
+ true
149
160
  end
150
161
 
151
162
  def stop_async_writer
152
163
  writer = @async_writer
153
- @async_writer = nil
164
+ stopped = writer&.stop
165
+ return false if stopped == false
166
+
167
+ close_replaced_log(writer.logger) if writer.respond_to?(:logger)
168
+ @async_writer = nil if @async_writer.equal?(writer)
154
169
  @async = false
155
- writer&.stop
170
+ true
171
+ end
172
+ # rubocop:enable Naming/PredicateMethod
173
+
174
+ private
175
+
176
+ def close_replaced_log(logger)
177
+ return unless logger
178
+ return if logger.equal?(@log)
179
+ return if @async_writer&.alive? && @async_writer.respond_to?(:logger) && @async_writer.logger.equal?(logger)
180
+
181
+ log_device = logger.instance_variable_get(:@logdev)
182
+ dev = log_device&.dev
183
+ return if dev.nil? || [$stdout, $stderr].include?(dev)
184
+
185
+ dev.close if dev.respond_to?(:close)
156
186
  end
157
187
  end
158
188
  end
@@ -11,6 +11,29 @@ module Legion
11
11
  MAX_PAYLOAD_BYTES = 8192
12
12
  MAX_TOTAL_BYTES = 65_536
13
13
  BACKTRACE_FALLBACK_FRAMES = 20
14
+ MIN_TRUNCATED_FIELD_BYTES = 256
15
+
16
+ CORE_EXCEPTION_FIELDS = %i[
17
+ timestamp
18
+ level
19
+ exception_class
20
+ message
21
+ caller_file
22
+ caller_line
23
+ caller_function
24
+ lex
25
+ component_type
26
+ gem_name
27
+ lex_version
28
+ handled
29
+ pid
30
+ thread
31
+ task_id
32
+ conversation_id
33
+ user
34
+ error_fingerprint
35
+ node
36
+ ].freeze
14
37
 
15
38
  GEM_SPEC_CACHE_MUTEX = Mutex.new
16
39
  private_constant :GEM_SPEC_CACHE_MUTEX
@@ -310,6 +333,56 @@ module Legion
310
333
  return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
311
334
 
312
335
  event[:message] = truncate_bytes(event[:message].to_s, 1024)
336
+ trim_optional_fields!(event)
337
+ hard_cap_message!(event)
338
+ end
339
+
340
+ def trim_optional_fields!(event)
341
+ while safe_json_bytesize(event) > MAX_TOTAL_BYTES
342
+ key = largest_optional_field(event)
343
+ break unless key
344
+
345
+ reduced = reduce_field(event[key])
346
+ if reduced.nil?
347
+ event.delete(key)
348
+ else
349
+ event[key] = reduced
350
+ end
351
+ end
352
+ end
353
+
354
+ def largest_optional_field(event)
355
+ event.each_key
356
+ .reject { |key| CORE_EXCEPTION_FIELDS.include?(key) }
357
+ .max_by { |key| safe_json_bytesize(event[key]) }
358
+ end
359
+
360
+ def reduce_field(value)
361
+ case value
362
+ when String
363
+ return nil if value.bytesize <= MIN_TRUNCATED_FIELD_BYTES
364
+
365
+ truncate_bytes(value, [value.bytesize / 2, MIN_TRUNCATED_FIELD_BYTES].max)
366
+ when Array
367
+ return nil if value.size <= 1
368
+
369
+ value.first([value.size / 2, 1].max)
370
+ when Hash
371
+ return nil if value.size <= 1
372
+
373
+ value.first([value.size / 2, 1].max).to_h
374
+ end
375
+ end
376
+
377
+ def hard_cap_message!(event)
378
+ return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
379
+
380
+ event[:message] = truncate_bytes(event[:message].to_s, MIN_TRUNCATED_FIELD_BYTES)
381
+ return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
382
+
383
+ message_overhead = safe_json_bytesize(event.merge(message: ''))
384
+ available = MAX_TOTAL_BYTES - message_overhead
385
+ event[:message] = truncate_bytes(event[:message].to_s, [available, 0].max)
313
386
  end
314
387
  end
315
388
  end
@@ -54,7 +54,19 @@ module Legion
54
54
  end
55
55
 
56
56
  def log
57
- @log ||= Legion::Logging::TaggedLogger.new(segments: derive_log_segments, **resolve_logger_settings)
57
+ current_generation =
58
+ if defined?(Legion::Logging) && Legion::Logging.respond_to?(:configuration_generation)
59
+ Legion::Logging.configuration_generation
60
+ else
61
+ 0
62
+ end
63
+
64
+ if !defined?(@log) || @log.nil? || @log_generation != current_generation
65
+ @log = Legion::Logging::TaggedLogger.new(segments: derive_log_segments, **tagged_logger_settings)
66
+ @log_generation = current_generation
67
+ end
68
+
69
+ @log
58
70
  end
59
71
 
60
72
  def with_log_context(method_name)
@@ -70,19 +82,13 @@ module Legion
70
82
  spec = gem_spec
71
83
  ctx = Thread.current[:legion_context] || {}
72
84
 
73
- event = Legion::Logging::EventBuilder.build_exception(
85
+ event = build_exception_event(
74
86
  exception: exception,
75
87
  level: level,
76
- lex: log_name,
77
- component_type: derive_component_type,
78
- gem_name: gem_name,
79
- lex_version: spec&.version&.to_s,
80
- gem_path: spec&.full_gem_path,
81
- source_code_uri: spec&.metadata&.[]('source_code_uri'),
88
+ spec: spec,
82
89
  handled: handled,
83
90
  task_id: task_id || ctx[:task_id],
84
- payload_summary: opts.empty? ? nil : opts,
85
- caller_offset: 3
91
+ payload_summary: opts.empty? ? nil : opts
86
92
  )
87
93
 
88
94
  event[:conversation_id] ||= ctx[:conversation_id]
@@ -93,11 +99,57 @@ module Legion
93
99
  event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
94
100
 
95
101
  write_exception_to_log(exception, event, level, segments)
96
- publish_exception(event, level)
102
+ publish_exception(event, level) if structured_exception_support?
97
103
  end
98
104
 
99
105
  private
100
106
 
107
+ def build_exception_event(exception:, level:, spec:, handled:, task_id:, payload_summary:)
108
+ unless structured_exception_support?
109
+ return fallback_exception_event(
110
+ exception: exception,
111
+ level: level,
112
+ spec: spec,
113
+ handled: handled,
114
+ task_id: task_id,
115
+ payload_summary: payload_summary
116
+ )
117
+ end
118
+
119
+ Legion::Logging::EventBuilder.build_exception(
120
+ exception: exception,
121
+ level: level,
122
+ lex: log_name,
123
+ component_type: derive_component_type,
124
+ gem_name: gem_name,
125
+ lex_version: spec&.version&.to_s,
126
+ gem_path: spec&.full_gem_path,
127
+ source_code_uri: spec&.metadata&.[]('source_code_uri'),
128
+ handled: handled,
129
+ task_id: task_id,
130
+ payload_summary: payload_summary,
131
+ caller_offset: 3
132
+ )
133
+ end
134
+
135
+ def fallback_exception_event(exception:, level:, spec:, handled:, task_id:, payload_summary:)
136
+ {
137
+ exception_class: exception.class.to_s,
138
+ message: exception.message,
139
+ level: level,
140
+ lex: log_name,
141
+ component_type: derive_component_type,
142
+ gem_name: gem_name,
143
+ lex_version: spec&.version&.to_s,
144
+ gem_path: spec&.full_gem_path,
145
+ source_code_uri: spec&.metadata&.[]('source_code_uri'),
146
+ handled: handled,
147
+ task_id: task_id,
148
+ payload_summary: payload_summary,
149
+ error_fingerprint: SecureRandom.uuid
150
+ }
151
+ end
152
+
101
153
  def derive_log_segments
102
154
  key = respond_to?(:ancestors) ? ancestors.first : self.class
103
155
  return SEGMENT_CACHE[key] if SEGMENT_CACHE.key?(key)
@@ -167,22 +219,238 @@ module Legion
167
219
  @gem_spec_value = nil
168
220
  end
169
221
 
170
- def settings
171
- { logger: logger_settings }
222
+ def instance_log_level(default = Legion::Logging::Settings.default[:level] || :info)
223
+ component_level = component_log_level
224
+ return component_level if present_log_level?(component_level)
225
+
226
+ global_level = global_log_level
227
+ return global_level if present_log_level?(global_level)
228
+
229
+ Legion::Logging::Settings.default[:level] || default
230
+ rescue StandardError => e
231
+ Legion::Logging.warn("Legion::Logging::Helper.instance_log_level(#{default}) failed: #{e.class}: #{e.message}")
232
+ Legion::Logging::Settings.default[:level] || default
172
233
  end
173
234
 
174
- def logger_settings
175
- return Legion::Settings[:logging] if defined?(Legion::Settings) && Legion::Settings[:logging].is_a?(Hash)
235
+ def global_logger_settings
236
+ defaults = defined?(Legion::Logging::Settings) ? Legion::Logging::Settings.default.dup : {}
237
+ settings_logging = if defined?(Legion::Settings) &&
238
+ Legion::Settings.respond_to?(:loaded?) &&
239
+ Legion::Settings.loaded?
240
+ raw = Legion::Settings[:logging]
241
+ raw.is_a?(Hash) ? raw : {}
242
+ else
243
+ {}
244
+ end
245
+ runtime_logging = if defined?(Legion::Logging) &&
246
+ Legion::Logging.respond_to?(:current_settings)
247
+ current = Legion::Logging.current_settings
248
+ current.is_a?(Hash) ? current : {}
249
+ else
250
+ {}
251
+ end
176
252
 
177
- Legion::Logging::Settings.default
253
+ defaults.merge(settings_logging).merge(runtime_logging)
178
254
  end
179
255
 
180
256
  def resolve_logger_settings
181
- s = settings
182
- return Legion::Logging::Settings.default unless s.is_a?(Hash)
257
+ base = global_logger_settings
258
+ override = component_logger_settings
259
+ merged = override ? base.merge(override) : base
260
+ merged.merge(
261
+ level: instance_log_level(merged[:level]),
262
+ trace: instance_trace(merged[:trace]),
263
+ trace_size: instance_trace_size(merged[:trace_size]),
264
+ extended: instance_extended(merged[:extended])
265
+ )
266
+ rescue StandardError
267
+ defined?(Legion::Logging::Settings) ? Legion::Logging::Settings.default : {}
268
+ end
269
+
270
+ def tagged_logger_settings
271
+ settings = resolve_logger_settings
272
+ {
273
+ level: settings[:level],
274
+ trace: settings[:trace],
275
+ trace_size: settings[:trace_size],
276
+ extended: settings[:extended]
277
+ }
278
+ end
279
+
280
+ def component_logger_settings
281
+ source = component_settings
282
+ raw = settings_value(source, :logger)
283
+ raw.is_a?(Hash) ? raw : nil
284
+ end
285
+
286
+ def component_log_level
287
+ source = component_settings
288
+ return unless source.is_a?(Hash)
289
+
290
+ settings_value(source, :log_level) ||
291
+ settings_value(source, :logger_level) ||
292
+ settings_value(source, :logger, :level)
293
+ end
294
+
295
+ def instance_trace(default = Legion::Logging::Settings.default[:trace])
296
+ component_trace = component_logger_option(:trace)
297
+ return component_trace unless component_trace.nil?
298
+
299
+ global_trace = global_logger_option(:trace)
300
+ return global_trace unless global_trace.nil?
301
+
302
+ Legion::Logging::Settings.default[:trace].nil? ? default : Legion::Logging::Settings.default[:trace]
303
+ rescue StandardError => e
304
+ Legion::Logging.warn("Legion::Logging::Helper.instance_trace(#{default}) failed: #{e.class}: #{e.message}")
305
+ Legion::Logging::Settings.default[:trace].nil? ? default : Legion::Logging::Settings.default[:trace]
306
+ end
307
+
308
+ def instance_trace_size(default = Legion::Logging::Settings.default[:trace_size] || 4)
309
+ component_trace_size = component_logger_option(:trace_size)
310
+ return component_trace_size unless component_trace_size.nil?
311
+
312
+ global_trace_size = global_logger_option(:trace_size)
313
+ return global_trace_size unless global_trace_size.nil?
183
314
 
184
- raw = s[:logger]
185
- raw.is_a?(Hash) ? raw : Legion::Logging::Settings.default
315
+ Legion::Logging::Settings.default[:trace_size] || default
316
+ rescue StandardError => e
317
+ Legion::Logging.warn("Legion::Logging::Helper.instance_trace_size(#{default}) failed: #{e.class}: #{e.message}")
318
+ Legion::Logging::Settings.default[:trace_size] || default
319
+ end
320
+
321
+ def instance_extended(default = Legion::Logging::Settings.default[:extended])
322
+ component_extended = component_logger_option(:extended)
323
+ return component_extended unless component_extended.nil?
324
+
325
+ global_extended = global_logger_option(:extended)
326
+ return global_extended unless global_extended.nil?
327
+
328
+ Legion::Logging::Settings.default[:extended].nil? ? default : Legion::Logging::Settings.default[:extended]
329
+ rescue StandardError => e
330
+ Legion::Logging.warn("Legion::Logging::Helper.instance_extended(#{default}) failed: #{e.class}: #{e.message}")
331
+ Legion::Logging::Settings.default[:extended].nil? ? default : Legion::Logging::Settings.default[:extended]
332
+ end
333
+
334
+ def component_settings
335
+ local = local_settings_hash
336
+ return local if local.is_a?(Hash)
337
+
338
+ legion_component_settings
339
+ end
340
+
341
+ def local_settings_hash
342
+ return unless respond_to?(:settings, true)
343
+
344
+ source = settings
345
+ source if source.is_a?(Hash)
346
+ rescue StandardError
347
+ nil
348
+ end
349
+
350
+ def legion_component_settings
351
+ return unless defined?(Legion::Settings)
352
+ return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
353
+
354
+ key = derive_component_settings_key
355
+ return unless key
356
+
357
+ top_level = Legion::Settings[key]
358
+ return top_level if top_level.is_a?(Hash)
359
+
360
+ extension_settings = Legion::Settings.dig(:extensions, key)
361
+ extension_settings if extension_settings.is_a?(Hash)
362
+ rescue StandardError
363
+ nil
364
+ end
365
+
366
+ def derive_component_settings_key
367
+ base = log_name
368
+ return unless base
369
+
370
+ base.to_s.tr('-', '_').to_sym
371
+ rescue StandardError
372
+ nil
373
+ end
374
+
375
+ def global_log_level
376
+ runtime_level = if defined?(Legion::Logging) &&
377
+ Legion::Logging.respond_to?(:current_settings)
378
+ settings_value(Legion::Logging.current_settings, :level)
379
+ end
380
+ return runtime_level if present_log_level?(runtime_level)
381
+
382
+ return unless defined?(Legion::Settings)
383
+ return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
384
+
385
+ settings_value(Legion::Settings[:logging], :level) || Legion::Settings[:level]
386
+ rescue StandardError
387
+ nil
388
+ end
389
+
390
+ def component_logger_option(key)
391
+ source = component_settings
392
+ return unless source.is_a?(Hash)
393
+
394
+ return settings_value(source, key) if settings_key?(source, key)
395
+ return settings_value(source, :logger, key) if settings_key?(source, :logger, key)
396
+
397
+ nil
398
+ end
399
+
400
+ def global_logger_option(key)
401
+ runtime_value = if defined?(Legion::Logging) &&
402
+ Legion::Logging.respond_to?(:current_settings)
403
+ settings_value(Legion::Logging.current_settings, key)
404
+ end
405
+ return runtime_value unless runtime_value.nil?
406
+
407
+ return unless defined?(Legion::Settings)
408
+ return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
409
+
410
+ settings_value(Legion::Settings[:logging], key)
411
+ rescue StandardError
412
+ nil
413
+ end
414
+
415
+ def settings_value(source, *keys)
416
+ missing = Object.new
417
+ current = source
418
+ keys.each do |key|
419
+ current =
420
+ if current.is_a?(Hash) && current.key?(key)
421
+ current[key]
422
+ elsif current.is_a?(Hash) && current.key?(key.to_s)
423
+ current[key.to_s]
424
+ else
425
+ missing
426
+ end
427
+
428
+ break if current.equal?(missing)
429
+ end
430
+
431
+ current.equal?(missing) ? nil : current
432
+ end
433
+
434
+ def settings_key?(source, *keys)
435
+ current = source
436
+ keys.each do |key|
437
+ return false unless current.is_a?(Hash)
438
+
439
+ next_key = if current.key?(key)
440
+ key
441
+ elsif current.key?(key.to_s)
442
+ key.to_s
443
+ else
444
+ return false
445
+ end
446
+ current = current[next_key]
447
+ end
448
+
449
+ true
450
+ end
451
+
452
+ def present_log_level?(value)
453
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
186
454
  end
187
455
 
188
456
  # -- Exception stdout/file output --
@@ -193,9 +461,14 @@ module Legion
193
461
 
194
462
  message = format_exception_output(exception, event)
195
463
  message = Legion::Logging::Redactor.redact_string(message) if defined?(Legion::Logging::Redactor) && redaction_enabled?
196
- message = colorize_exception(message, level) if Legion::Logging.color
464
+ message = colorize_exception(message, level) if Legion::Logging.respond_to?(:color) && Legion::Logging.color
197
465
 
198
- Legion::Logging.log.public_send(level, message)
466
+ logger = Legion::Logging.respond_to?(:log) ? Legion::Logging.log : nil
467
+ if logger.respond_to?(level)
468
+ logger.public_send(level, message)
469
+ elsif Legion::Logging.respond_to?(level)
470
+ Legion::Logging.public_send(level, message)
471
+ end
199
472
  ensure
200
473
  Thread.current[:legion_log_segments] = prev_segs
201
474
  end
@@ -251,6 +524,8 @@ module Legion
251
524
  # -- Exception structured publish --
252
525
 
253
526
  def publish_exception(event, level)
527
+ return unless structured_exception_support?
528
+
254
529
  lex_name = event[:lex] || 'core'
255
530
  comp = event[:component_type] || :unknown
256
531
  routing_key = "legion.logging.exception.#{level}.#{lex_name}.#{comp}"
@@ -260,7 +535,12 @@ module Legion
260
535
 
261
536
  Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
262
537
  rescue StandardError => e
263
- Legion::Logging.warn("Failed to publish exception event: #{e.class}: #{e.message}")
538
+ Legion::Logging.warn("Failed to publish exception event: #{e.class}: #{e.message}") if Legion::Logging.respond_to?(:warn)
539
+ end
540
+
541
+ def structured_exception_support?
542
+ defined?(Legion::Logging::EventBuilder) &&
543
+ Legion::Logging.respond_to?(:exception_writer)
264
544
  end
265
545
 
266
546
  def build_exception_headers(event, comp, level)
@@ -30,78 +30,36 @@ module Legion
30
30
  return unless log.level < 1
31
31
 
32
32
  message = yield if message.nil? && block_given?
33
- message = maybe_redact(message)
34
- message = Rainbow(message).blue if @color
35
- writer = @async_writer
36
- if writer&.alive?
37
- writer.push(AsyncWriter::LogEntry.new(
38
- level: :debug, message: message, writer_context: nil,
39
- segments: Thread.current[:legion_log_segments],
40
- method_ctx: Thread.current[:legion_log_method]
41
- ))
42
- else
43
- log.debug(message)
44
- end
33
+ raw = maybe_redact(message)
34
+ formatted = format_message_for_level(:debug, raw)
35
+ write_async_or_sync(:debug, formatted, raw)
45
36
  end
46
37
 
47
38
  def info(message = nil)
48
39
  return unless log.level < 2
49
40
 
50
41
  message = yield if message.nil? && block_given?
51
- message = maybe_redact(message)
52
- message = Rainbow(message).green if @color
53
- writer = @async_writer
54
- if writer&.alive?
55
- writer.push(AsyncWriter::LogEntry.new(
56
- level: :info, message: message, writer_context: nil,
57
- segments: Thread.current[:legion_log_segments],
58
- method_ctx: Thread.current[:legion_log_method]
59
- ))
60
- else
61
- log.info(message)
62
- end
42
+ raw = maybe_redact(message)
43
+ formatted = format_message_for_level(:info, raw)
44
+ write_async_or_sync(:info, formatted, raw)
63
45
  end
64
46
 
65
47
  def warn(message = nil)
66
48
  return unless log.level < 3
67
49
 
68
50
  message = yield if message.nil? && block_given?
69
- message = maybe_redact(message)
70
- raw = message
71
- message = Rainbow(message).yellow if @color
72
- writer = @async_writer
73
- if writer&.alive?
74
- ctx = build_writer_context(:warn, raw)
75
- writer.push(AsyncWriter::LogEntry.new(
76
- level: :warn, message: message, writer_context: ctx,
77
- segments: Thread.current[:legion_log_segments],
78
- method_ctx: Thread.current[:legion_log_method]
79
- ))
80
- else
81
- log.warn(message)
82
- fire_log_writer(:warn, raw)
83
- end
51
+ raw = maybe_redact(message)
52
+ formatted = format_message_for_level(:warn, raw)
53
+ write_async_or_sync(:warn, formatted, raw, writer_context: build_writer_context(:warn, raw))
84
54
  end
85
55
 
86
56
  def error(message = nil)
87
57
  return unless log.level < 4
88
58
 
89
59
  message = yield if message.nil? && block_given?
90
- message = maybe_redact(message)
91
- raw = message
92
- message = Rainbow(message).red if @color
93
- writer = @async_writer
94
- if writer&.alive?
95
- ctx = build_writer_context(:error, raw)
96
- writer.push(AsyncWriter::LogEntry.new(
97
- level: :error, message: message, writer_context: ctx,
98
- segments: Thread.current[:legion_log_segments],
99
- method_ctx: Thread.current[:legion_log_method]
100
- ))
101
- else
102
- log.error(message)
103
- fire_log_writer(:error, raw)
104
- end
60
+ raw = maybe_redact(message)
61
+ formatted = format_message_for_level(:error, raw)
62
+ write_async_or_sync(:error, formatted, raw, writer_context: build_writer_context(:error, raw))
105
63
  end
106
64
 
107
65
  def fatal(message = nil)
@@ -117,17 +75,22 @@ module Legion
117
75
 
118
76
  def unknown(message = nil)
119
77
  message = yield if message.nil? && block_given?
120
- message = maybe_redact(message)
121
- message = Rainbow(message).purple if @color
122
- writer = @async_writer
123
- if writer&.alive?
124
- writer.push(AsyncWriter::LogEntry.new(
125
- level: :unknown, message: message, writer_context: nil,
126
- segments: Thread.current[:legion_log_segments],
127
- method_ctx: Thread.current[:legion_log_method]
128
- ))
129
- else
130
- log.unknown(message)
78
+ raw = maybe_redact(message)
79
+ formatted = format_message_for_level(:unknown, raw)
80
+ write_async_or_sync(:unknown, formatted, raw)
81
+ end
82
+
83
+ def emit_tagged(level, message = nil, segments: nil, method_ctx: nil)
84
+ level = level.to_sym
85
+ message = yield if message.nil? && block_given?
86
+ return if message.nil?
87
+
88
+ raw = maybe_redact(message)
89
+ formatted = format_message_for_level(level, raw)
90
+
91
+ with_tagged_context(segments, method_ctx) do
92
+ write_forced(level, formatted)
93
+ fire_log_writer(level, raw) if %i[warn error fatal].include?(level)
131
94
  end
132
95
  end
133
96
 
@@ -143,6 +106,7 @@ module Legion
143
106
  level = level.to_sym if level.respond_to?(:to_sym)
144
107
  # 1. Log human-readable line to stdout/file (bypass writer callbacks)
145
108
  msg = exception.respond_to?(:message) ? exception.message : exception.to_s
109
+ msg = maybe_redact(msg)
146
110
  log.public_send(level, msg) if respond_to?(:log) && log.respond_to?(level)
147
111
 
148
112
  # 2. Build rich exception event
@@ -195,6 +159,81 @@ module Legion
195
159
  message
196
160
  end
197
161
 
162
+ def format_message_for_level(level, message)
163
+ return Rainbow(message).blue if level == :debug && @color
164
+ return Rainbow(message).green if level == :info && @color
165
+ return Rainbow(message).yellow if level == :warn && @color
166
+ return Rainbow(message).red if level == :error && @color
167
+ return Rainbow(message).darkred if level == :fatal && @color
168
+ return Rainbow(message).purple if level == :unknown && @color
169
+
170
+ message
171
+ end
172
+
173
+ def with_tagged_context(segments, method_ctx)
174
+ prev_segments = Thread.current[:legion_log_segments]
175
+ prev_method_ctx = Thread.current[:legion_log_method]
176
+
177
+ Thread.current[:legion_log_segments] = segments unless segments.nil?
178
+ Thread.current[:legion_log_method] = method_ctx unless method_ctx.nil?
179
+ yield
180
+ ensure
181
+ Thread.current[:legion_log_segments] = prev_segments
182
+ Thread.current[:legion_log_method] = prev_method_ctx
183
+ end
184
+
185
+ def write_forced(level, message)
186
+ logger = log
187
+ formatter = logger.formatter || ::Logger::Formatter.new
188
+ rendered = formatter.call(severity_label_for(level), Time.now, nil, message)
189
+
190
+ log_device = logger.instance_variable_get(:@logdev)
191
+ if log_device.respond_to?(:write)
192
+ log_device.write(rendered)
193
+ else
194
+ $stdout.write(rendered)
195
+ end
196
+ end
197
+
198
+ def severity_label_for(level)
199
+ return 'ANY' if level == :unknown
200
+
201
+ level.to_s.upcase
202
+ end
203
+
204
+ def write_async_or_sync(level, formatted_message, raw_message, writer_context: nil)
205
+ writer = @async_writer
206
+ caller_trace = capture_runner_trace_for_async
207
+ if writer&.alive?
208
+ queued = writer.push(AsyncWriter::LogEntry.new(
209
+ level: level,
210
+ message: formatted_message,
211
+ writer_context: writer_context,
212
+ segments: Thread.current[:legion_log_segments],
213
+ method_ctx: Thread.current[:legion_log_method],
214
+ caller_trace: caller_trace
215
+ ))
216
+ return if queued
217
+ end
218
+
219
+ with_caller_trace(caller_trace) do
220
+ log.public_send(level, formatted_message)
221
+ fire_log_writer(level, raw_message) if writer_context
222
+ end
223
+ end
224
+
225
+ def capture_runner_trace_for_async
226
+ build_runner_trace(caller_locations(4, 1)&.first)
227
+ end
228
+
229
+ def with_caller_trace(caller_trace)
230
+ prev_caller_trace = Thread.current[:legion_log_caller]
231
+ Thread.current[:legion_log_caller] = caller_trace
232
+ yield
233
+ ensure
234
+ Thread.current[:legion_log_caller] = prev_caller_trace
235
+ end
236
+
198
237
  def redaction_enabled?
199
238
  return false unless defined?(Legion::Settings)
200
239
 
@@ -10,7 +10,6 @@ module Legion
10
10
  def write(message)
11
11
  @targets.each do |t|
12
12
  t.write(message)
13
- t.flush if t.respond_to?(:flush)
14
13
  end
15
14
  end
16
15
 
@@ -18,7 +18,18 @@ module Legion
18
18
  bearer_token: %r{Bearer\s+[A-Za-z0-9._~+/=-]{20,}}i
19
19
  }.freeze
20
20
 
21
- SENSITIVE_FIELDS = %w[password secret token api_key authorization].freeze
21
+ SENSITIVE_FIELDS = %w[
22
+ password
23
+ secret
24
+ token
25
+ api_key
26
+ access_key
27
+ private_key
28
+ public_key
29
+ authorization
30
+ ].freeze
31
+ SENSITIVE_SUFFIXES = %w[token secret password passphrase credential credentials].freeze
32
+ SAFE_KEY_FIELDS = %w[primary_key foreign_key sort_key partition_key routing_key].freeze
22
33
 
23
34
  REDACTED = '[REDACTED]'
24
35
 
@@ -49,7 +60,17 @@ module Legion
49
60
  private
50
61
 
51
62
  def sensitive_field?(key)
52
- SENSITIVE_FIELDS.include?(key.to_s.downcase)
63
+ normalized = normalize_key(key)
64
+ return false if SAFE_KEY_FIELDS.include?(normalized)
65
+ return true if SENSITIVE_FIELDS.include?(normalized)
66
+ return true if normalized.include?('authorization')
67
+ return true if normalized.start_with?('auth_') || normalized.end_with?('_auth')
68
+ return true if normalized.start_with?('bearer_') || normalized.end_with?('_bearer')
69
+ return true if SENSITIVE_SUFFIXES.any? { |suffix| normalized.end_with?("_#{suffix}") }
70
+
71
+ %w[api access client private public auth secret signing session].any? do |prefix|
72
+ normalized == "#{prefix}_key"
73
+ end
53
74
  end
54
75
 
55
76
  def all_patterns
@@ -79,6 +100,16 @@ module Legion
79
100
  def reset_pattern_cache!
80
101
  @all_patterns = nil
81
102
  end
103
+
104
+ def refresh_patterns!
105
+ reset_pattern_cache!
106
+ end
107
+
108
+ public :refresh_patterns!
109
+
110
+ def normalize_key(key)
111
+ key.to_s.downcase.gsub(/[^a-z0-9]+/, '_').gsub(/\A_+|_+\z/, '')
112
+ end
82
113
  end
83
114
  end
84
115
  end
@@ -6,9 +6,9 @@ module Legion
6
6
  def self.default
7
7
  {
8
8
  level: :info,
9
- trace: false,
9
+ trace: true,
10
10
  trace_size: 4,
11
- extended: false
11
+ extended: true
12
12
  }
13
13
  end
14
14
  end
@@ -11,10 +11,18 @@ module Legion
11
11
 
12
12
  class << self
13
13
  def ship(event)
14
+ ship_batch([event])
15
+ end
16
+
17
+ def ship_batch(events)
18
+ batch = Array(events)
19
+ return true if batch.empty?
20
+
14
21
  path = resolve_path
15
22
  FileUtils.mkdir_p(File.dirname(path))
16
23
  File.open(path, 'a') do |f|
17
- f.puts(::JSON.generate(event))
24
+ f.write(batch.map { |event| ::JSON.generate(event) }.join("\n"))
25
+ f.write("\n")
18
26
  end
19
27
  true
20
28
  rescue StandardError => e
@@ -10,6 +10,10 @@ module Legion
10
10
  module HttpTransport
11
11
  class << self
12
12
  def ship(events)
13
+ ship_batch(events)
14
+ end
15
+
16
+ def ship_batch(events)
13
17
  endpoint = resolve_endpoint
14
18
  return false unless endpoint
15
19
 
@@ -30,21 +30,25 @@ module Legion
30
30
  transport = TRANSPORTS[transport_type]
31
31
  return unless transport
32
32
 
33
- batch = nil
34
- @mutex.synchronize do
35
- batch = @buffer.dup
36
- @buffer.clear
33
+ @flush_mutex ||= Mutex.new
34
+ @flush_mutex.synchronize do
35
+ batch = nil
36
+ @mutex.synchronize { batch = @buffer.dup }
37
+ return if batch.empty?
38
+
39
+ delivered = deliver(transport, batch)
40
+ @mutex.synchronize { @buffer.shift(batch.size) if delivered }
41
+ delivered
37
42
  end
38
-
39
- deliver(transport, batch)
40
43
  end
41
44
 
42
45
  def start
43
46
  return unless enabled?
44
47
  return if @flush_thread&.alive?
45
48
 
46
- @buffer = []
47
- @mutex = Mutex.new
49
+ @buffer ||= []
50
+ @mutex ||= Mutex.new
51
+ @flush_mutex ||= Mutex.new
48
52
  interval = flush_interval
49
53
  @flush_thread = Thread.new do
50
54
  loop do
@@ -83,14 +87,14 @@ module Legion
83
87
  end
84
88
 
85
89
  def deliver(transport, batch)
86
- if transport.method(:ship).arity == 1
87
- # HttpTransport accepts a batch array
88
- transport.ship(batch)
90
+ if transport.respond_to?(:ship_batch)
91
+ transport.ship_batch(batch)
89
92
  else
90
- batch.each { |e| transport.ship(e) }
93
+ batch.all? { |event| transport.ship(event) }
91
94
  end
92
95
  rescue StandardError => e
93
96
  Legion::Logging.error("Shipper deliver failed: #{e.message}") if defined?(Legion::Logging)
97
+ false
94
98
  end
95
99
 
96
100
  def shippable_level?(level)
@@ -7,13 +7,21 @@ module Legion
7
7
 
8
8
  attr_reader :segments, :trace_enabled, :extended
9
9
 
10
- def initialize(segments:, level: :info, trace: false, trace_size: 4, extended: false, **_opts)
10
+ def initialize(
11
+ segments:,
12
+ level: Legion::Logging::Settings.default[:level],
13
+ trace: Legion::Logging::Settings.default[:trace],
14
+ trace_size: Legion::Logging::Settings.default[:trace_size],
15
+ extended: Legion::Logging::Settings.default[:extended],
16
+ **_opts
17
+ )
11
18
  @segments = segments
12
19
  @level_value =
13
20
  if level.is_a?(Integer)
14
21
  level
15
22
  else
16
- LEVELS.fetch(level.to_s.downcase.to_sym, LEVELS[:info])
23
+ default_level = Legion::Logging::Settings.default[:level].to_s.downcase.to_sym
24
+ LEVELS.fetch(level.to_s.downcase.to_sym, LEVELS.fetch(default_level, LEVELS[:info]))
17
25
  end
18
26
  @trace_enabled = trace
19
27
  @trace_size = trace_size
@@ -28,40 +36,40 @@ module Legion
28
36
  return unless @level_value < 1
29
37
 
30
38
  message = yield if message.nil? && block_given?
31
- with_segments { Legion::Logging.debug(message) }
39
+ with_segments { dispatch(:debug, message) }
32
40
  end
33
41
 
34
42
  def info(message = nil)
35
43
  return unless @level_value < 2
36
44
 
37
45
  message = yield if message.nil? && block_given?
38
- with_segments { Legion::Logging.info(message) }
46
+ with_segments { dispatch(:info, message) }
39
47
  end
40
48
 
41
49
  def warn(message = nil)
42
50
  return unless @level_value < 3
43
51
 
44
52
  message = yield if message.nil? && block_given?
45
- with_segments { Legion::Logging.warn(message) }
53
+ with_segments { dispatch(:warn, message) }
46
54
  end
47
55
 
48
56
  def error(message = nil)
49
57
  return unless @level_value < 4
50
58
 
51
59
  message = yield if message.nil? && block_given?
52
- with_segments { Legion::Logging.error(message) }
60
+ with_segments { dispatch(:error, message) }
53
61
  end
54
62
 
55
63
  def fatal(message = nil)
56
64
  return unless @level_value < 5
57
65
 
58
66
  message = yield if message.nil? && block_given?
59
- with_segments { Legion::Logging.fatal(message) }
67
+ with_segments { dispatch(:fatal, message) }
60
68
  end
61
69
 
62
70
  def unknown(message = nil)
63
71
  message = yield if message.nil? && block_given?
64
- with_segments { Legion::Logging.unknown(message) }
72
+ with_segments { dispatch(:unknown, message) }
65
73
  end
66
74
 
67
75
  def trace(raw_message = nil, size: @trace_size, log_caller: true)
@@ -86,6 +94,29 @@ module Legion
86
94
 
87
95
  private
88
96
 
97
+ def dispatch(level, message)
98
+ return unless defined?(Legion::Logging)
99
+
100
+ if Legion::Logging.respond_to?(:emit_tagged)
101
+ Legion::Logging.emit_tagged(level, message, segments: @segments)
102
+ return
103
+ end
104
+
105
+ if Legion::Logging.respond_to?(level)
106
+ Legion::Logging.public_send(level, message)
107
+ return
108
+ end
109
+
110
+ fallback = fallback_level(level)
111
+ Legion::Logging.public_send(fallback, message) if fallback && Legion::Logging.respond_to?(fallback)
112
+ end
113
+
114
+ def fallback_level(level)
115
+ return :debug if level == :unknown
116
+
117
+ nil
118
+ end
119
+
89
120
  def with_segments
90
121
  prev = Thread.current[:legion_log_segments]
91
122
  Thread.current[:legion_log_segments] = @segments
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Logging
5
- VERSION = '1.4.3'
5
+ VERSION = '1.5.0'
6
6
  end
7
7
  end
@@ -36,6 +36,14 @@ module Legion
36
36
  @exception_writer || DEFAULT_EXCEPTION_WRITER
37
37
  end
38
38
 
39
+ def current_settings
40
+ (@current_settings || {}).dup
41
+ end
42
+
43
+ def configuration_generation
44
+ @configuration_generation || 0
45
+ end
46
+
39
47
  def register_category(name, description: nil, expected_fields: [])
40
48
  CategoryRegistry.register_category(name, description: description, expected_fields: expected_fields)
41
49
  end
@@ -68,18 +76,33 @@ module Legion
68
76
  Hooks.clear_hooks!
69
77
  end
70
78
 
71
- def setup(level: 'info', format: :text, async: true, **options)
79
+ def setup(level: 'debug', format: :text, async: true, **options)
72
80
  output(**options)
73
81
  log_level(level)
74
82
  log_format(format: format, **options)
75
83
  @color = options[:color]
76
84
  @color = format != :json && (options[:color] || (options[:color].nil? && options[:log_file].nil?))
85
+ @current_settings = {
86
+ level: level,
87
+ format: format.to_sym,
88
+ async: async,
89
+ trace: options.fetch(:trace, true),
90
+ trace_size: options.fetch(:trace_size, 4),
91
+ extended: options.fetch(:extended, true),
92
+ log_file: options[:log_file],
93
+ log_stdout: options[:log_stdout],
94
+ include_pid: options.fetch(:include_pid, false),
95
+ color: @color
96
+ }.freeze
97
+ @configuration_generation = configuration_generation + 1
98
+ Legion::Logging::Redactor.refresh_patterns! if defined?(Legion::Logging::Redactor)
77
99
  if async
78
- buffer = if defined?(Legion::Settings)
79
- Legion::Settings.dig(:logging, :async, :buffer_size) || 10_000
80
- else
81
- 10_000
100
+ buffer = if defined?(Legion::Settings) && Legion::Settings.respond_to?(:[])
101
+ logging_settings = Legion::Settings[:logging]
102
+ async_settings = logging_settings[:async] if logging_settings.is_a?(Hash)
103
+ async_settings[:buffer_size] if async_settings.is_a?(Hash)
82
104
  end
105
+ buffer ||= 10_000
83
106
  start_async_writer(buffer_size: buffer)
84
107
  else
85
108
  stop_async_writer
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.3
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity