legion-logging 1.5.3 → 1.5.5

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: f751e228eb4c00edae10784b3edf4176afb2f84ec7a8df1525a9c8432145c50f
4
- data.tar.gz: 728db26335f6bffdcef54523760d9b8051615f0bee280ac01a897a3373b3403f
3
+ metadata.gz: cd5e1bffbdc433aedb24653ea515080063cb456de0c5a0fd5a57cdbff91d371d
4
+ data.tar.gz: 0b015f1b0c5d12511f231bfa32325e2ebcc41ca902b92d8293bdcf6359d8a7ac
5
5
  SHA512:
6
- metadata.gz: 3ba593790cf603ab542162e685c97450f7db2d18da76dd288981317ecf627b708a391e2b0f1eecfef43e3a308f0391a89a6bc636ab3216c6450a973d653b7e3e
7
- data.tar.gz: 5c6fb87b90d197c3f70e0d69e1d5a3a13c55083aa1f8ebbd79676ff8926b12686e3fa9b2df91286ac2a216ace987863eb719b21df8693e3f0981768a2f70f748
6
+ metadata.gz: 0bad8e3a31fb86de2b1a2749457b506e7042606e7e060813a0adcaf81f2d0afdf8998e3ba57692fabe5814ab7efcb3dcadfb9c8e985a77b6f3c4e44aedcccf35
7
+ data.tar.gz: 83475ff76aae03a25c24e1b464695f68ef6b4514309e6290485e513066f3d9f4ff9367e2b87e087f89dd1f1b6b3fafa4176ca5fcae226224859356d23189a80f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Legion::Logging Changelog
2
2
 
3
+ ## [1.5.5] - 2026-05-27
4
+
5
+ ### Fixed
6
+ - `emit_tagged` (used by all Legion::Logging::Helper consumers) now routes through the async writer instead of doing synchronous IO#write — eliminates IO mutex contention across all consumer threads
7
+ - `fatal` level now routes through async writer consistently with all other levels
8
+ - `log_exception` now writes through async writer instead of direct `log.public_send`
9
+ - `Legion::Logging::Logger` defaults to `async: true`, ensuring all per-extension logger instances use non-blocking writes
10
+
11
+ ## [1.5.4] - 2026-05-22
12
+
13
+ ### Added
14
+ - Structured log and exception AMQP writer headers now include `legion_protocol_version`, best-effort `x-legion-version`, and transport-standard `x-legion-identity-*` headers when process identity is resolved.
15
+ - `log_writer` now receives the same `headers:` and `properties:` envelope metadata shape as `exception_writer`.
16
+
3
17
  ## [1.5.3] - 2026-05-13
4
18
 
5
19
  ### Added
@@ -110,7 +110,9 @@ module Legion
110
110
  lex_name = event[:lex] || 'core'
111
111
  component = event.dig(:caller, :file).to_s[Legion::Logging::Methods::COMPONENT_REGEX, 1] || 'unknown'
112
112
  routing_key = "legion.logging.log.#{level}.#{lex_name}.#{component}"
113
- Legion::Logging.log_writer.call(event, routing_key: routing_key)
113
+ headers = Legion::Logging.send(:build_log_headers, event, component, level)
114
+ properties = Legion::Logging.send(:build_log_properties, level)
115
+ Legion::Logging.log_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
114
116
  Legion::Logging::Hooks.fire(level, entry.message, event) if defined?(Legion::Logging::Hooks)
115
117
  rescue StandardError => e
116
118
  warn("legion-log-writer writer error: #{e.message}")
@@ -559,21 +559,70 @@ module Legion
559
559
 
560
560
  def build_exception_headers(event, comp, level)
561
561
  headers = {
562
- 'x-error-fingerprint' => event[:error_fingerprint],
563
- 'x-exception-class' => event[:exception_class],
564
- 'x-handled' => event[:handled].to_s,
565
- 'x-gem-name' => event[:gem_name].to_s,
566
- 'x-lex-version' => event[:lex_version].to_s,
567
- 'x-component-type' => comp.to_s,
568
- 'x-level' => level.to_s
562
+ 'legion_protocol_version' => '2.0',
563
+ 'x-error-fingerprint' => event[:error_fingerprint],
564
+ 'x-exception-class' => event[:exception_class],
565
+ 'x-handled' => event[:handled].to_s,
566
+ 'x-gem-name' => event[:gem_name].to_s,
567
+ 'x-lex-version' => event[:lex_version].to_s,
568
+ 'x-component-type' => comp.to_s,
569
+ 'x-level' => level.to_s
569
570
  }
571
+ append_legion_version_header(headers)
570
572
  headers['x-task-id'] = event[:task_id].to_s if event[:task_id]
571
573
  headers['x-conversation-id'] = event[:conversation_id].to_s if event[:conversation_id]
572
574
  headers['x-chain-id'] = event[:chain_id].to_s if event[:chain_id]
573
575
  headers['x-user'] = event[:user].to_s if event[:user]
576
+ append_identity_headers(headers)
574
577
  headers
575
578
  end
576
579
 
580
+ def append_identity_headers(headers)
581
+ return unless defined?(Legion::Identity::Process)
582
+ return if Legion::Identity::Process.respond_to?(:resolved?) && !Legion::Identity::Process.resolved?
583
+
584
+ id = identity_hash
585
+ append_optional_header(headers, 'x-legion-identity-canonical-name', id[:canonical_name])
586
+ append_optional_header(headers, 'x-legion-identity-trust', id[:trust])
587
+ append_optional_header(headers, 'x-legion-identity-id', id[:id])
588
+ append_optional_header(headers, 'x-legion-identity-kind', id[:kind])
589
+ append_optional_header(headers, 'x-legion-identity-mode', id[:mode])
590
+ append_optional_header(headers, 'x-legion-identity-source', id[:source])
591
+ headers['x-legion-identity-db-principal-id'] = id[:db_principal_id] if id[:db_principal_id]
592
+ headers['x-legion-identity-db-identity-id'] = id[:db_identity_id] if id[:db_identity_id]
593
+ rescue StandardError
594
+ nil
595
+ end
596
+
597
+ def append_optional_header(headers, key, value)
598
+ return if value.nil?
599
+ return if value.respond_to?(:empty?) && value.empty?
600
+
601
+ headers[key] = value.to_s
602
+ end
603
+
604
+ def append_legion_version_header(headers)
605
+ append_optional_header(headers, 'x-legion-version', Legion::VERSION) if defined?(Legion::VERSION)
606
+ end
607
+
608
+ def identity_hash
609
+ process = Legion::Identity::Process
610
+ return process.identity_hash if process.respond_to?(:identity_hash)
611
+
612
+ {
613
+ canonical_name: identity_value(process, :canonical_name),
614
+ id: identity_value(process, :id),
615
+ kind: identity_value(process, :kind),
616
+ mode: identity_value(process, :mode),
617
+ source: identity_value(process, :source),
618
+ trust: identity_value(process, :trust)
619
+ }
620
+ end
621
+
622
+ def identity_value(process, method_name)
623
+ process.public_send(method_name) if process.respond_to?(method_name)
624
+ end
625
+
577
626
  def build_exception_properties(event, level)
578
627
  {
579
628
  content_type: 'application/json',
@@ -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, async: false, **opts)
14
+ def initialize(level: 'info', log_file: nil, log_stdout: nil, lex: nil, trace: false, extended: false, trace_size: 4, format: :text, async: true, **opts)
15
15
  @lex = lex
16
16
  set_log(logfile: log_file, log_stdout: log_stdout)
17
17
  log_level(level)
@@ -66,11 +66,9 @@ module Legion
66
66
  return unless log.level < 5
67
67
 
68
68
  message = yield if message.nil? && block_given?
69
- message = maybe_redact(message)
70
- raw = message
71
- message = Rainbow(message).darkred if @color
72
- log.fatal(message)
73
- fire_log_writer(:fatal, raw)
69
+ raw = maybe_redact(message)
70
+ formatted = format_message_for_level(:fatal, raw)
71
+ write_async_or_sync(:fatal, formatted, raw, writer_context: build_writer_context(:fatal, raw))
74
72
  end
75
73
 
76
74
  def unknown(message = nil)
@@ -89,8 +87,20 @@ module Legion
89
87
  formatted = format_message_for_level(level, raw)
90
88
 
91
89
  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)
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
103
+ end
94
104
  end
95
105
  end
96
106
 
@@ -104,11 +114,12 @@ module Legion
104
114
  source_code_uri: nil, handled: false, payload_summary: nil,
105
115
  task_id: nil, backtrace_limit: nil, **extra)
106
116
  level = level.to_sym if level.respond_to?(:to_sym)
107
- # 1. Log human-readable line + backtrace to stdout/file (bypass writer callbacks)
117
+ # 1. Log human-readable line + backtrace via async writer
108
118
  msg = exception.respond_to?(:message) ? exception.message : exception.to_s
109
119
  msg = maybe_redact(msg)
110
120
  msg = build_exception_log_message(exception, msg, backtrace_limit)
111
- log.public_send(level, msg) if respond_to?(:log) && log.respond_to?(level)
121
+ formatted = format_message_for_level(level, msg)
122
+ write_async_or_sync(level, formatted, msg)
112
123
 
113
124
  # 2. Build rich exception event
114
125
  event = Legion::Logging::EventBuilder.build_exception(
@@ -270,6 +281,48 @@ module Legion
270
281
  false
271
282
  end
272
283
 
284
+ def build_log_headers(event, component, level)
285
+ headers = {
286
+ 'legion_protocol_version' => '2.0',
287
+ 'x-component-type' => component.to_s,
288
+ 'x-level' => level.to_s
289
+ }
290
+ append_legion_version_header(headers)
291
+ append_optional_header(headers, 'x-lex', event[:lex])
292
+ append_optional_header(headers, 'x-node', event[:node])
293
+ append_identity_headers(headers)
294
+ headers
295
+ end
296
+
297
+ def build_log_properties(level)
298
+ {
299
+ content_type: 'application/json',
300
+ message_id: SecureRandom.uuid,
301
+ timestamp: Time.now.to_i,
302
+ app_id: 'legionio',
303
+ type: 'log_event',
304
+ priority: EXCEPTION_PRIORITY[level] || 0,
305
+ delivery_mode: 2
306
+ }
307
+ end
308
+
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
+
273
326
  def publish_exception_event(event, level)
274
327
  lex_name = event[:lex] || 'core'
275
328
  comp = event[:component_type] || :unknown
@@ -281,17 +334,20 @@ module Legion
281
334
 
282
335
  def build_exception_headers(event, comp, level)
283
336
  headers = {
284
- 'x-error-fingerprint' => event[:error_fingerprint],
285
- 'x-exception-class' => event[:exception_class],
286
- 'x-handled' => event[:handled].to_s,
287
- 'x-gem-name' => event[:gem_name].to_s,
288
- 'x-lex-version' => event[:lex_version].to_s,
289
- 'x-component-type' => comp.to_s,
290
- 'x-level' => level.to_s
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
291
345
  }
346
+ append_legion_version_header(headers)
292
347
  append_optional_header(headers, 'x-task-id', event[:task_id])
293
348
  append_optional_header(headers, 'x-conversation-id', event[:conversation_id])
294
349
  append_optional_header(headers, 'x-user', event[:user])
350
+ append_identity_headers(headers)
295
351
  headers
296
352
  end
297
353
 
@@ -302,6 +358,28 @@ module Legion
302
358
  headers[key] = value.to_s
303
359
  end
304
360
 
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
+
305
383
  def build_exception_properties(event, level)
306
384
  {
307
385
  content_type: 'application/json',
@@ -347,7 +425,9 @@ module Legion
347
425
  lex_name = event[:lex] || 'core'
348
426
  component = event.dig(:caller, :file).to_s[COMPONENT_REGEX, 1] || 'unknown'
349
427
  routing_key = "legion.logging.log.#{level}.#{lex_name}.#{component}"
350
- Legion::Logging.log_writer.call(event, routing_key: routing_key)
428
+ headers = build_log_headers(event, component, level)
429
+ properties = build_log_properties(level)
430
+ Legion::Logging.log_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
351
431
  Legion::Logging::Hooks.fire(level, message, event) if defined?(Legion::Logging::Hooks)
352
432
  rescue StandardError => e
353
433
  rk = defined?(routing_key) ? routing_key : 'unknown'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Logging
5
- VERSION = '1.5.3'
5
+ VERSION = '1.5.5'
6
6
  end
7
7
  end
@@ -25,7 +25,7 @@ module Legion
25
25
  attr_reader :color
26
26
  attr_writer :log_writer, :exception_writer
27
27
 
28
- DEFAULT_LOG_WRITER = ->(_event, routing_key:) {}
28
+ DEFAULT_LOG_WRITER = ->(_event, routing_key:, headers: nil, properties: nil) {}
29
29
  DEFAULT_EXCEPTION_WRITER = ->(_event, routing_key:, headers:, properties:) {}
30
30
 
31
31
  def log_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.5.3
4
+ version: 1.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity