legion-logging 1.5.5 → 1.5.19

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,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require_relative 'header_builder'
4
5
 
5
6
  module Legion
6
7
  module Logging
7
8
  module Methods
9
+ include Legion::Logging::HeaderBuilder
10
+
8
11
  COMPONENT_REGEX = %r{
9
12
  /(runners|actors|actor|helpers|hooks|absorbers|matchers|transport|
10
13
  exchanges|queues|messages|data|builders|tools|adapters|engines|
11
14
  formatters|parsers|middleware)/
12
15
  }x
13
- EXCEPTION_PRIORITY = { warn: 0, error: 5, fatal: 9 }.freeze
16
+ LOGGING_PATH = __dir__
14
17
 
15
18
  def trace(raw_message = nil, size: @trace_size, log_caller: true)
16
19
  return unless @trace_enabled
@@ -23,83 +26,148 @@ module Legion
23
26
  elsif log_caller
24
27
  message.concat Rainbow(caller_locations[0..size]).cyan.underline
25
28
  end
26
- log.unknown(message)
29
+ unknown(message)
27
30
  end
28
31
 
29
- def debug(message = nil)
32
+ def debug(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
30
33
  return unless log.level < 1
31
34
 
32
35
  message = yield if message.nil? && block_given?
36
+ if async_producer?
37
+ return defer_log_call(receiver: self, method_name: :debug, arguments: [message],
38
+ keywords: { task_id: task_id, conv_id: conv_id, request_id: request_id,
39
+ exchange_id: exchange_id, chain_id: chain_id })
40
+ end
41
+
33
42
  raw = maybe_redact(message)
34
43
  formatted = format_message_for_level(:debug, raw)
35
- write_async_or_sync(:debug, formatted, raw)
44
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
45
+ exchange_id: exchange_id, chain_id: chain_id) do
46
+ write_async_or_sync(:debug, formatted, raw, writer_context: build_writer_context(:debug, raw))
47
+ end
36
48
  end
37
49
 
38
- def info(message = nil)
50
+ def info(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
39
51
  return unless log.level < 2
40
52
 
41
53
  message = yield if message.nil? && block_given?
54
+ if async_producer?
55
+ return defer_log_call(receiver: self, method_name: :info, arguments: [message],
56
+ keywords: { task_id: task_id, conv_id: conv_id, request_id: request_id,
57
+ exchange_id: exchange_id, chain_id: chain_id })
58
+ end
59
+
42
60
  raw = maybe_redact(message)
43
61
  formatted = format_message_for_level(:info, raw)
44
- write_async_or_sync(:info, formatted, raw)
62
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
63
+ exchange_id: exchange_id, chain_id: chain_id) do
64
+ write_async_or_sync(:info, formatted, raw, writer_context: build_writer_context(:info, raw))
65
+ end
45
66
  end
46
67
 
47
- def warn(message = nil)
68
+ def warn(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
48
69
  return unless log.level < 3
49
70
 
50
71
  message = yield if message.nil? && block_given?
72
+ if async_producer?
73
+ return defer_log_call(receiver: self, method_name: :warn, arguments: [message],
74
+ keywords: { task_id: task_id, conv_id: conv_id, request_id: request_id,
75
+ exchange_id: exchange_id, chain_id: chain_id })
76
+ end
77
+
51
78
  raw = maybe_redact(message)
52
79
  formatted = format_message_for_level(:warn, raw)
53
- write_async_or_sync(:warn, formatted, raw, writer_context: build_writer_context(:warn, raw))
80
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
81
+ exchange_id: exchange_id, chain_id: chain_id) do
82
+ write_async_or_sync(:warn, formatted, raw, writer_context: build_writer_context(:warn, raw))
83
+ end
54
84
  end
55
85
 
56
- def error(message = nil)
86
+ def error(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
57
87
  return unless log.level < 4
58
88
 
59
89
  message = yield if message.nil? && block_given?
90
+ if async_producer?
91
+ return defer_log_call(receiver: self, method_name: :error, arguments: [message],
92
+ keywords: { task_id: task_id, conv_id: conv_id, request_id: request_id,
93
+ exchange_id: exchange_id, chain_id: chain_id })
94
+ end
95
+
60
96
  raw = maybe_redact(message)
61
97
  formatted = format_message_for_level(:error, raw)
62
- write_async_or_sync(:error, formatted, raw, writer_context: build_writer_context(:error, raw))
98
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
99
+ exchange_id: exchange_id, chain_id: chain_id) do
100
+ write_async_or_sync(:error, formatted, raw, writer_context: build_writer_context(:error, raw))
101
+ end
63
102
  end
64
103
 
65
- def fatal(message = nil)
104
+ def fatal(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
66
105
  return unless log.level < 5
67
106
 
68
107
  message = yield if message.nil? && block_given?
108
+ if async_producer?
109
+ return defer_log_call(receiver: self, method_name: :fatal, arguments: [message],
110
+ keywords: { task_id: task_id, conv_id: conv_id, request_id: request_id,
111
+ exchange_id: exchange_id, chain_id: chain_id })
112
+ end
113
+
69
114
  raw = maybe_redact(message)
70
115
  formatted = format_message_for_level(:fatal, raw)
71
- write_async_or_sync(:fatal, formatted, raw, writer_context: build_writer_context(:fatal, raw))
116
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
117
+ exchange_id: exchange_id, chain_id: chain_id) do
118
+ write_async_or_sync(:fatal, formatted, raw, writer_context: build_writer_context(:fatal, raw))
119
+ end
72
120
  end
73
121
 
74
- def unknown(message = nil)
122
+ def unknown(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
75
123
  message = yield if message.nil? && block_given?
124
+ if async_producer?
125
+ return defer_log_call(receiver: self, method_name: :unknown, arguments: [message],
126
+ keywords: { task_id: task_id, conv_id: conv_id, request_id: request_id,
127
+ exchange_id: exchange_id, chain_id: chain_id })
128
+ end
129
+
76
130
  raw = maybe_redact(message)
77
131
  formatted = format_message_for_level(:unknown, raw)
78
- write_async_or_sync(:unknown, formatted, raw)
132
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
133
+ exchange_id: exchange_id, chain_id: chain_id) do
134
+ write_async_or_sync(:unknown, formatted, raw, writer_context: build_writer_context(:unknown, raw))
135
+ end
79
136
  end
80
137
 
81
- def emit_tagged(level, message = nil, segments: nil, method_ctx: nil)
138
+ def emit_tagged(level, message = nil, segments: nil, method_ctx: nil,
139
+ task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
82
140
  level = level.to_sym
83
141
  message = yield if message.nil? && block_given?
84
142
  return if message.nil?
143
+ if async_producer?
144
+ return defer_log_call(receiver: self, method_name: :emit_tagged, arguments: [level, message],
145
+ keywords: { segments: segments, method_ctx: method_ctx, task_id: task_id, conv_id: conv_id,
146
+ request_id: request_id, exchange_id: exchange_id, chain_id: chain_id })
147
+ end
85
148
 
86
149
  raw = maybe_redact(message)
87
150
  formatted = format_message_for_level(level, raw)
88
151
 
89
152
  with_tagged_context(segments, method_ctx) do
90
- ctx = %i[warn error fatal].include?(level) ? build_writer_context(level, raw) : nil
91
- writer = @async_writer
92
- caller_trace = capture_runner_trace_for_async
93
- if writer&.alive?
94
- writer.push(AsyncWriter::LogEntry.new(
95
- level: level, message: formatted, writer_context: ctx,
96
- segments: Thread.current[:legion_log_segments],
97
- method_ctx: Thread.current[:legion_log_method],
98
- caller_trace: caller_trace
99
- ))
100
- else
101
- with_caller_trace(caller_trace) { write_forced(level, formatted) }
102
- fire_log_writer(level, raw) if ctx
153
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
154
+ exchange_id: exchange_id, chain_id: chain_id) do
155
+ ctx = build_writer_context(level, raw)
156
+ writer = @async_writer
157
+ caller_trace = capture_runner_trace_for_async
158
+ if @async || writer
159
+ push_async_entry(entry: AsyncWriter::LogEntry.new(force: true, level: level, message: formatted, writer_context: ctx,
160
+ segments: Thread.current[:legion_log_segments],
161
+ method_ctx: Thread.current[:legion_log_method],
162
+ caller_trace: caller_trace,
163
+ conv_id: Thread.current[:legion_log_conv_id],
164
+ request_id: Thread.current[:legion_log_request_id],
165
+ exchange_id: Thread.current[:legion_log_exchange_id],
166
+ chain_id: Thread.current[:legion_log_chain_id]))
167
+ else
168
+ with_caller_trace(caller_trace) { write_forced(level, formatted) }
169
+ fire_log_writer(level, raw, event: ctx[:event]) if ctx
170
+ end
103
171
  end
104
172
  end
105
173
  end
@@ -113,6 +181,14 @@ module Legion
113
181
  gem_name: nil, lex_version: nil, gem_path: nil,
114
182
  source_code_uri: nil, handled: false, payload_summary: nil,
115
183
  task_id: nil, backtrace_limit: nil, **extra)
184
+ if async_producer?
185
+ return defer_log_call(receiver: self, method_name: :log_exception, arguments: [exception],
186
+ keywords: { level: level, lex: lex, component_type: component_type, gem_name: gem_name,
187
+ lex_version: lex_version, gem_path: gem_path, source_code_uri: source_code_uri,
188
+ handled: handled, payload_summary: payload_summary, task_id: task_id,
189
+ backtrace_limit: backtrace_limit, **extra })
190
+ end
191
+
116
192
  level = level.to_sym if level.respond_to?(:to_sym)
117
193
  # 1. Log human-readable line + backtrace via async writer
118
194
  msg = exception.respond_to?(:message) ? exception.message : exception.to_s
@@ -142,7 +218,7 @@ module Legion
142
218
  event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
143
219
 
144
220
  # 4. Publish rich event via exception_writer
145
- publish_exception_event(event, level)
221
+ publish_exception(event: event, level: level)
146
222
  rescue StandardError => e
147
223
  if respond_to?(:log) && log.respond_to?(:warn)
148
224
  log.warn("Failed to publish structured exception event: #{e.class}: #{e.message}")
@@ -151,6 +227,45 @@ module Legion
151
227
  end
152
228
  end
153
229
 
230
+ def publish_exception(event:, level:)
231
+ lex_name = event[:lex] || 'core'
232
+ component = event[:component_type] || :unknown
233
+ options = {
234
+ routing_key: "legion.logging.exception.#{level}.#{lex_name}.#{component}",
235
+ headers: build_exception_headers(event, component, level),
236
+ properties: build_exception_properties(event, level)
237
+ }
238
+ callback = Legion::Logging.exception_writer
239
+ writer = @async_writer
240
+ return push_async_entry(entry: AsyncWriter::EventEntry.new(callback: callback, event: event, options: options)) if @async || writer
241
+
242
+ callback.call(event, **options)
243
+ end
244
+
245
+ def async_producer?
246
+ (@async == true || !@async_writer.nil?) && !@async_writer&.current_thread?
247
+ end
248
+
249
+ def defer_log_call(receiver:, method_name:, arguments:, keywords:)
250
+ return false unless async_producer?
251
+ raise ClosedQueueError, 'async logging writer is unavailable' unless @async_writer&.alive?
252
+
253
+ context = Legion::Logging::AsyncWriter::CALL_THREAD_KEYS.to_h do |key|
254
+ [key, Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[key])]
255
+ end
256
+ location = caller_locations(1, 16).find do |frame|
257
+ path = frame.absolute_path || frame.path
258
+ !path.start_with?(LOGGING_PATH)
259
+ end
260
+ context[:legion_log_origin] = { location: location, timestamp: Time.now.utc, thread: Thread.current.object_id }
261
+ push_async_entry(entry: Legion::Logging::AsyncWriter::CallEntry.new(
262
+ receiver: receiver, method_name: method_name,
263
+ arguments: arguments.map { |value| Legion::Logging::AsyncWriter.snapshot_value(value: value) },
264
+ keywords: keywords.transform_values { |value| Legion::Logging::AsyncWriter.snapshot_value(value: value) },
265
+ context: context
266
+ ))
267
+ end
268
+
154
269
  def thread(kvl: false, **_opts)
155
270
  if kvl
156
271
  "thread=#{Thread.current.object_id}"
@@ -164,8 +279,9 @@ module Legion
164
279
  def resolve_backtrace_limit(explicit_limit)
165
280
  return explicit_limit unless explicit_limit.nil?
166
281
  return nil unless defined?(Legion::Settings)
282
+ return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
167
283
 
168
- Legion::Settings[:logging][:backtrace_limit]
284
+ Legion::Settings.dig(:logging, :backtrace_limit)
169
285
  end
170
286
 
171
287
  def build_exception_log_message(exception, msg, backtrace_limit)
@@ -237,29 +353,61 @@ module Legion
237
353
  level.to_s.upcase
238
354
  end
239
355
 
356
+ def with_context_ids(task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil)
357
+ prev_conv = Thread.current[:legion_log_conv_id]
358
+ prev_req = Thread.current[:legion_log_request_id]
359
+ prev_exchange = Thread.current[:legion_log_exchange_id]
360
+ prev_chain = Thread.current[:legion_log_chain_id]
361
+
362
+ context_id = conv_id || task_id || Thread.current[:legion_log_conv_id]
363
+ req_id = request_id || Thread.current[:legion_log_request_id]
364
+ exch_id = exchange_id || Thread.current[:legion_log_exchange_id]
365
+ ch_id = chain_id || Thread.current[:legion_log_chain_id]
366
+
367
+ Thread.current[:legion_log_conv_id] = context_id if context_id.is_a?(String) && !context_id.empty?
368
+ Thread.current[:legion_log_request_id] = req_id if req_id.is_a?(String) && !req_id.empty?
369
+ Thread.current[:legion_log_exchange_id] = exch_id if exch_id.is_a?(String) && !exch_id.empty?
370
+ Thread.current[:legion_log_chain_id] = ch_id if ch_id.is_a?(String) && !ch_id.empty?
371
+ yield
372
+ ensure
373
+ Thread.current[:legion_log_conv_id] = prev_conv
374
+ Thread.current[:legion_log_request_id] = prev_req
375
+ Thread.current[:legion_log_exchange_id] = prev_exchange
376
+ Thread.current[:legion_log_chain_id] = prev_chain
377
+ end
378
+
240
379
  def write_async_or_sync(level, formatted_message, raw_message, writer_context: nil)
241
380
  writer = @async_writer
242
381
  caller_trace = capture_runner_trace_for_async
243
- if writer&.alive?
244
- queued = writer.push(AsyncWriter::LogEntry.new(
245
- level: level,
246
- message: formatted_message,
247
- writer_context: writer_context,
248
- segments: Thread.current[:legion_log_segments],
249
- method_ctx: Thread.current[:legion_log_method],
250
- caller_trace: caller_trace
251
- ))
252
- return if queued
382
+ if @async || writer
383
+ return push_async_entry(entry: AsyncWriter::LogEntry.new(force: false, level: level,
384
+ message: formatted_message,
385
+ writer_context: writer_context,
386
+ segments: Thread.current[:legion_log_segments],
387
+ method_ctx: Thread.current[:legion_log_method],
388
+ caller_trace: caller_trace,
389
+ conv_id: Thread.current[:legion_log_conv_id],
390
+ request_id: Thread.current[:legion_log_request_id],
391
+ exchange_id: Thread.current[:legion_log_exchange_id],
392
+ chain_id: Thread.current[:legion_log_chain_id]))
253
393
  end
254
394
 
255
395
  with_caller_trace(caller_trace) do
256
396
  log.public_send(level, formatted_message)
257
- fire_log_writer(level, raw_message) if writer_context
397
+ fire_log_writer(level, raw_message, event: writer_context[:event]) if writer_context
258
398
  end
259
399
  end
260
400
 
401
+ def push_async_entry(entry:) # rubocop:disable Naming/PredicateMethod
402
+ writer = @async_writer
403
+ raise ClosedQueueError, 'async logging writer is unavailable' unless writer&.alive? && writer.push(entry)
404
+
405
+ true
406
+ end
407
+
261
408
  def capture_runner_trace_for_async
262
- build_runner_trace(caller_locations(4, 1)&.first)
409
+ location = Thread.current[:legion_log_origin]&.[](:location) || caller_locations(5, 1)&.first
410
+ build_runner_trace(location)
263
411
  end
264
412
 
265
413
  def with_caller_trace(caller_trace)
@@ -270,17 +418,6 @@ module Legion
270
418
  Thread.current[:legion_log_caller] = prev_caller_trace
271
419
  end
272
420
 
273
- def redaction_enabled?
274
- return false unless defined?(Legion::Settings)
275
-
276
- loader = Legion::Settings.instance_variable_get(:@loader)
277
- return false unless loader
278
-
279
- loader.dig(:logging, :redaction, :enabled) == true
280
- rescue StandardError
281
- false
282
- end
283
-
284
421
  def build_log_headers(event, component, level)
285
422
  headers = {
286
423
  'legion_protocol_version' => '2.0',
@@ -301,102 +438,40 @@ module Legion
301
438
  timestamp: Time.now.to_i,
302
439
  app_id: 'legionio',
303
440
  type: 'log_event',
304
- priority: EXCEPTION_PRIORITY[level] || 0,
441
+ priority: HeaderBuilder::EXCEPTION_PRIORITY[level] || 0,
305
442
  delivery_mode: 2
306
443
  }
307
444
  end
308
445
 
309
- def append_identity_headers(headers)
310
- return unless defined?(Legion::Identity::Process)
311
- return if Legion::Identity::Process.respond_to?(:resolved?) && !Legion::Identity::Process.resolved?
312
-
313
- id = identity_hash
314
- append_optional_header(headers, 'x-legion-identity-canonical-name', id[:canonical_name])
315
- append_optional_header(headers, 'x-legion-identity-trust', id[:trust])
316
- append_optional_header(headers, 'x-legion-identity-id', id[:id])
317
- append_optional_header(headers, 'x-legion-identity-kind', id[:kind])
318
- append_optional_header(headers, 'x-legion-identity-mode', id[:mode])
319
- append_optional_header(headers, 'x-legion-identity-source', id[:source])
320
- headers['x-legion-identity-db-principal-id'] = id[:db_principal_id] if id[:db_principal_id]
321
- headers['x-legion-identity-db-identity-id'] = id[:db_identity_id] if id[:db_identity_id]
322
- rescue StandardError
323
- nil
324
- end
325
-
326
- def publish_exception_event(event, level)
327
- lex_name = event[:lex] || 'core'
328
- comp = event[:component_type] || :unknown
329
- routing_key = "legion.logging.exception.#{level}.#{lex_name}.#{comp}"
330
- headers = build_exception_headers(event, comp, level)
331
- properties = build_exception_properties(event, level)
332
- Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
333
- end
334
-
335
- def build_exception_headers(event, comp, level)
336
- headers = {
337
- 'legion_protocol_version' => '2.0',
338
- 'x-error-fingerprint' => event[:error_fingerprint],
339
- 'x-exception-class' => event[:exception_class],
340
- 'x-handled' => event[:handled].to_s,
341
- 'x-gem-name' => event[:gem_name].to_s,
342
- 'x-lex-version' => event[:lex_version].to_s,
343
- 'x-component-type' => comp.to_s,
344
- 'x-level' => level.to_s
345
- }
346
- append_legion_version_header(headers)
347
- append_optional_header(headers, 'x-task-id', event[:task_id])
348
- append_optional_header(headers, 'x-conversation-id', event[:conversation_id])
349
- append_optional_header(headers, 'x-user', event[:user])
350
- append_identity_headers(headers)
351
- headers
446
+ def forward_level?(level)
447
+ forwardable_log_levels.include?(level.to_sym)
352
448
  end
353
449
 
354
- def append_optional_header(headers, key, value)
355
- return if value.nil?
356
- return if value.respond_to?(:empty?) && value.empty?
450
+ # Levels whose logs build a structured event for the transport bus / Hooks.
451
+ # Runtime override: logging.transport.log_levels (Array of level names).
452
+ # Falls back to Settings.default[:forward_log_levels] (warn/error/fatal).
453
+ def forwardable_log_levels
454
+ default = Legion::Logging::Settings::DEFAULT_FORWARD_LOG_LEVELS
455
+ return default unless defined?(Legion::Settings)
456
+ return default unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
357
457
 
358
- headers[key] = value.to_s
359
- end
458
+ configured = Legion::Settings.dig(:logging, :transport, :log_levels)
459
+ return default unless configured.is_a?(Array) && !configured.empty?
360
460
 
361
- def append_legion_version_header(headers)
362
- append_optional_header(headers, 'x-legion-version', Legion::VERSION) if defined?(Legion::VERSION)
363
- end
364
-
365
- def identity_hash
366
- process = Legion::Identity::Process
367
- return process.identity_hash if process.respond_to?(:identity_hash)
368
-
369
- {
370
- canonical_name: identity_value(process, :canonical_name),
371
- id: identity_value(process, :id),
372
- kind: identity_value(process, :kind),
373
- mode: identity_value(process, :mode),
374
- source: identity_value(process, :source),
375
- trust: identity_value(process, :trust)
376
- }
377
- end
378
-
379
- def identity_value(process, method_name)
380
- process.public_send(method_name) if process.respond_to?(method_name)
381
- end
382
-
383
- def build_exception_properties(event, level)
384
- {
385
- content_type: 'application/json',
386
- message_id: SecureRandom.uuid,
387
- correlation_id: event[:error_fingerprint],
388
- timestamp: Time.now.to_i,
389
- app_id: 'legionio',
390
- type: 'exception_event',
391
- priority: EXCEPTION_PRIORITY[level] || 5,
392
- delivery_mode: 2
393
- }
461
+ configured.map { |lvl| lvl.to_s.to_sym }
462
+ rescue StandardError => e
463
+ Kernel.warn("Legion::Logging forwarding configuration failed: #{e.message}")
464
+ default
394
465
  end
395
466
 
396
467
  def build_writer_context(level, message)
468
+ # Cheap ivar/flag checks first: when nothing consumes forwarded events
469
+ # (the common case) we return before touching Settings, keeping the
470
+ # hot debug/info path allocation- and lookup-free.
397
471
  has_writer = !Legion::Logging.instance_variable_get(:@log_writer).nil?
398
472
  has_hooks = defined?(Legion::Logging::Hooks) && Legion::Logging::Hooks.enabled?
399
473
  return nil unless has_writer || has_hooks
474
+ return nil unless forward_level?(level)
400
475
 
401
476
  lex_val = instance_variable_defined?(:@lex) ? @lex : nil
402
477
  lex_segs = Thread.current[:legion_log_segments] || (instance_variable_defined?(:@lex_segments) ? @lex_segments : nil)
@@ -411,17 +486,7 @@ module Legion
411
486
  { level: level, event: event }
412
487
  end
413
488
 
414
- def fire_log_writer(level, message)
415
- lex_val = instance_variable_defined?(:@lex) ? @lex : nil
416
- lex_segs = Thread.current[:legion_log_segments] || (instance_variable_defined?(:@lex_segments) ? @lex_segments : nil)
417
-
418
- event = Legion::Logging::EventBuilder.build(
419
- level: level,
420
- message: message,
421
- lex: lex_val,
422
- lex_segments: lex_segs,
423
- caller_offset: 4
424
- )
489
+ def fire_log_writer(level, message, event:)
425
490
  lex_name = event[:lex] || 'core'
426
491
  component = event.dig(:caller, :file).to_s[COMPONENT_REGEX, 1] || 'unknown'
427
492
  routing_key = "legion.logging.log.#{level}.#{lex_name}.#{component}"
@@ -10,6 +10,8 @@ module Legion
10
10
  def write(message)
11
11
  @targets.each do |t|
12
12
  t.write(message)
13
+ rescue StandardError => e
14
+ warn("Legion::Logging::MultiIO#write failed for #{t.class}: #{e.message}")
13
15
  end
14
16
  end
15
17
 
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Legion
6
+ module Logging
7
+ # Keeps the standard Logger interface used by Sinatra/APM on the async queue.
8
+ class OutputLogger < ::Logger
9
+ attr_accessor :async_writer
10
+
11
+ def add(severity, message = nil, progname = nil)
12
+ writer = @async_writer
13
+ return super unless writer && !writer.current_thread?
14
+
15
+ severity ||= UNKNOWN
16
+ return true if @logdev.nil? || severity < level
17
+
18
+ progname ||= @progname
19
+ if message.nil?
20
+ if block_given?
21
+ message = yield
22
+ else
23
+ message = progname
24
+ progname = @progname
25
+ end
26
+ end
27
+ enqueue_output(severity: severity, message: message, progname: progname)
28
+ end
29
+ alias log add
30
+
31
+ def <<(message)
32
+ writer = @async_writer
33
+ return super unless writer && !writer.current_thread?
34
+
35
+ enqueue_output(severity: nil, message: message, progname: nil)
36
+ end
37
+
38
+ private
39
+
40
+ def enqueue_output(severity:, message:, progname:) # rubocop:disable Naming/PredicateMethod
41
+ entry = Legion::Logging::AsyncWriter::OutputEntry.new(
42
+ severity: severity,
43
+ message: Legion::Logging::AsyncWriter.snapshot_value(value: message),
44
+ progname: Legion::Logging::AsyncWriter.snapshot_value(value: progname),
45
+ segments: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_segments]),
46
+ method_ctx: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_method]),
47
+ caller_trace: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_caller]),
48
+ conv_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_conv_id]),
49
+ request_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_request_id]),
50
+ exchange_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_exchange_id]),
51
+ chain_id: Legion::Logging::AsyncWriter.snapshot_value(value: Thread.current[:legion_log_chain_id]),
52
+ origin: { timestamp: Time.now.utc, thread: Thread.current.object_id }
53
+ )
54
+ raise ClosedQueueError, 'async logging writer is unavailable' unless @async_writer.alive? && @async_writer.push(entry)
55
+
56
+ true
57
+ end
58
+ end
59
+ end
60
+ end
@@ -74,6 +74,9 @@ module Legion
74
74
  end
75
75
 
76
76
  def all_patterns
77
+ return PATTERNS unless defined?(Legion::Settings)
78
+ return PATTERNS unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
79
+
77
80
  @all_patterns ||= build_patterns
78
81
  end
79
82
 
@@ -85,8 +88,6 @@ module Legion
85
88
  end
86
89
 
87
90
  def custom_patterns
88
- return {} unless defined?(Legion::Settings)
89
-
90
91
  raw = Legion::Settings.dig(:logging, :redactor, :custom_patterns)
91
92
  return {} unless raw.is_a?(Hash)
92
93
 
@@ -3,15 +3,31 @@
3
3
  module Legion
4
4
  module Logging
5
5
  module Settings
6
+ # Log levels forwarded to the transport bus (and in-process Hooks) by
7
+ # default. Preserves historic behavior: only warn/error/fatal logs build a
8
+ # structured event. Override at runtime via logging.transport.log_levels
9
+ # (e.g. add :debug/:info) — see Methods#forwardable_log_levels.
10
+ DEFAULT_FORWARD_LOG_LEVELS = %i[warn error fatal].freeze
11
+
6
12
  def self.default
7
13
  {
8
- level: :info,
9
- trace: true,
10
- trace_size: 4,
11
- extended: true,
12
- backtrace_limit: nil
14
+ level: :info,
15
+ trace: true,
16
+ trace_size: 4,
17
+ extended: true,
18
+ backtrace_limit: nil,
19
+ forward_log_levels: DEFAULT_FORWARD_LOG_LEVELS
13
20
  }
14
21
  end
22
+
23
+ def self.async_buffer_size
24
+ return 10_000 unless defined?(Legion::Settings) && Legion::Settings.respond_to?(:[])
25
+ return 10_000 unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
26
+
27
+ logging = Legion::Settings[:logging]
28
+ async = logging[:async] if logging.is_a?(Hash)
29
+ async.is_a?(Hash) ? (async[:buffer_size] || 10_000) : 10_000
30
+ end
15
31
  end
16
32
  end
17
33
  end
@@ -26,7 +26,7 @@ module Legion
26
26
  end
27
27
  true
28
28
  rescue StandardError => e
29
- Legion::Logging.error("FileTransport ship failed: #{e.message}") if defined?(Legion::Logging)
29
+ Kernel.warn("FileTransport ship failed: #{e.message}")
30
30
  false
31
31
  end
32
32
 
@@ -40,6 +40,7 @@ module Legion
40
40
 
41
41
  def settings_path
42
42
  return nil unless defined?(Legion::Settings)
43
+ return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
43
44
 
44
45
  Legion::Settings.dig(:logging, :shipper, :file, :path)
45
46
  end