legion-logging 1.5.5 → 1.5.6

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: cd5e1bffbdc433aedb24653ea515080063cb456de0c5a0fd5a57cdbff91d371d
4
- data.tar.gz: 0b015f1b0c5d12511f231bfa32325e2ebcc41ca902b92d8293bdcf6359d8a7ac
3
+ metadata.gz: ff7f461a7f614eb2480b2ef960992d463c7e26ee8cae0283d9f381e859e28c8d
4
+ data.tar.gz: a5bc8f5e7fd81d72ef8cc4ae98225e677645581340611103ac99807bb2f6c691
5
5
  SHA512:
6
- metadata.gz: 0bad8e3a31fb86de2b1a2749457b506e7042606e7e060813a0adcaf81f2d0afdf8998e3ba57692fabe5814ab7efcb3dcadfb9c8e985a77b6f3c4e44aedcccf35
7
- data.tar.gz: 83475ff76aae03a25c24e1b464695f68ef6b4514309e6290485e513066f3d9f4ff9367e2b87e087f89dd1f1b6b3fafa4176ca5fcae226224859356d23189a80f
6
+ metadata.gz: 006d30ed434deec409edd42ca5b5ac9245a4657dc8fc6280fc8b982c704d9ef1b1aee299bf6f905698a382f84cbc5ca3205adfd4691f2010d6aec0dea818a986
7
+ data.tar.gz: f6288442156789fd8bbb9858910a3deb01fffea277341c2c0e5b8288ee3cab7150b07b4c348faad78b3645f808823f536a856140fc1c5ce4935b4b805d13caea
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Legion::Logging Changelog
2
2
 
3
+ ## [1.5.6] - 2026-06-01
4
+
5
+ ### Added
6
+ - `task_id:`, `conv_id:`, `request_id:` optional kwargs on all log level methods (`.debug`, `.info`, `.warn`, `.error`, `.fatal`, `.unknown`) in both `Methods` and `TaggedLogger`
7
+ - Text formatter appends context ID (`conv_id` or `task_id` fallback) as `{conv_12345}` after the method segment in the prefix
8
+ - Text formatter inserts `request_id=<value>` between severity and message when present
9
+ - JSON formatter includes `conversation_id` and `request_id` fields when present
10
+ - `AsyncWriter::LogEntry` carries `conv_id` and `request_id` for correct propagation to the writer thread
11
+ - Thread-local `legion_log_conv_id` and `legion_log_request_id` for block-scoped context without per-call kwargs
12
+
3
13
  ## [1.5.5] - 2026-05-27
4
14
 
5
15
  ### Fixed
@@ -5,8 +5,13 @@ 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, :caller_trace)
8
+ LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx, :caller_trace,
9
+ :conv_id, :request_id, :exchange_id, :chain_id)
9
10
  SHUTDOWN = :shutdown
11
+ THREAD_KEYS = %i[
12
+ legion_log_segments legion_log_method legion_log_caller
13
+ legion_log_conv_id legion_log_request_id legion_log_exchange_id legion_log_chain_id
14
+ ].freeze
10
15
 
11
16
  attr_reader :logger
12
17
 
@@ -74,20 +79,26 @@ module Legion
74
79
  end
75
80
 
76
81
  def write_entry(entry)
77
- prev_segments = Thread.current[:legion_log_segments]
78
- prev_method_ctx = Thread.current[:legion_log_method]
79
- prev_caller = Thread.current[:legion_log_caller]
80
- Thread.current[:legion_log_segments] = entry.segments
81
- Thread.current[:legion_log_method] = entry.method_ctx
82
- Thread.current[:legion_log_caller] = entry.caller_trace
83
- @logger.send(entry.level, entry.message)
84
- fire_writer(entry) if entry.writer_context
82
+ with_entry_context(entry) do
83
+ @logger.send(entry.level, entry.message)
84
+ fire_writer(entry) if entry.writer_context
85
+ end
85
86
  rescue StandardError => e
86
87
  warn("legion-log-writer error: #{e.message} (#{e.backtrace&.first})")
88
+ end
89
+
90
+ def with_entry_context(entry)
91
+ prev = THREAD_KEYS.map { |k| [k, Thread.current[k]] }
92
+ Thread.current[:legion_log_segments] = entry.segments
93
+ Thread.current[:legion_log_method] = entry.method_ctx
94
+ Thread.current[:legion_log_caller] = entry.caller_trace
95
+ Thread.current[:legion_log_conv_id] = entry.conv_id
96
+ Thread.current[:legion_log_request_id] = entry.request_id
97
+ Thread.current[:legion_log_exchange_id] = entry.exchange_id
98
+ Thread.current[:legion_log_chain_id] = entry.chain_id
99
+ yield
87
100
  ensure
88
- Thread.current[:legion_log_segments] = prev_segments
89
- Thread.current[:legion_log_method] = prev_method_ctx
90
- Thread.current[:legion_log_caller] = prev_caller
101
+ prev&.each { |k, v| Thread.current[k] = v }
91
102
  end
92
103
 
93
104
  def drain
@@ -31,6 +31,14 @@ module Legion
31
31
  entry[:segments] = segments if segments
32
32
  method_ctx = Thread.current[:legion_log_method]
33
33
  entry[:method] = method_ctx if method_ctx
34
+ conv_id = Thread.current[:legion_log_conv_id]
35
+ entry[:conversation_id] = conv_id if conv_id.is_a?(String) && !conv_id.empty?
36
+ request_id = Thread.current[:legion_log_request_id]
37
+ entry[:request_id] = request_id if request_id.is_a?(String) && !request_id.empty?
38
+ exchange_id = Thread.current[:legion_log_exchange_id]
39
+ entry[:exchange_id] = exchange_id if exchange_id.is_a?(String) && !exchange_id.empty?
40
+ chain_id = Thread.current[:legion_log_chain_id]
41
+ entry[:chain_id] = chain_id if chain_id.is_a?(String) && !chain_id.empty?
34
42
  "#{::JSON.generate(entry)}\n"
35
43
  rescue StandardError => e
36
44
  warn("Legion::Logging::Builder#json_format formatter failed: #{e.message}")
@@ -49,7 +57,12 @@ module Legion
49
57
  if runner_trace.is_a?(Hash) && (options[:extended] || severity == 'debug')
50
58
  string.concat("[#{runner_trace[:type]}:#{runner_trace[:file]}:#{runner_trace[:function]}:#{runner_trace[:line_number]}]")
51
59
  end
52
- string.concat(" #{severity} #{msg}\n")
60
+ ctx_pairs = build_context_kv_pairs
61
+ if ctx_pairs.empty?
62
+ string.concat(" #{severity} #{msg}\n")
63
+ else
64
+ string.concat(" #{severity} #{msg} #{ctx_pairs}\n")
65
+ end
53
66
  string
54
67
  end
55
68
  end
@@ -66,9 +79,23 @@ module Legion
66
79
 
67
80
  method_ctx = Thread.current[:legion_log_method]
68
81
  tag = "#{tag}{#{method_ctx}}" if tag && method_ctx
82
+
83
+ context_id = Thread.current[:legion_log_conv_id]
84
+ tag = "#{tag}{#{context_id}}" if tag && context_id.is_a?(String) && !context_id.empty?
69
85
  tag
70
86
  end
71
87
 
88
+ def build_context_kv_pairs
89
+ pairs = []
90
+ request_id = Thread.current[:legion_log_request_id]
91
+ pairs << "request_id=#{request_id}" if request_id.is_a?(String) && !request_id.empty?
92
+ exchange_id = Thread.current[:legion_log_exchange_id]
93
+ pairs << "exchange_id=#{exchange_id}" if exchange_id.is_a?(String) && !exchange_id.empty?
94
+ chain_id = Thread.current[:legion_log_chain_id]
95
+ pairs << "chain_id=#{chain_id}" if chain_id.is_a?(String) && !chain_id.empty?
96
+ pairs.join(' ')
97
+ end
98
+
72
99
  def build_runner_trace(loc = caller_locations(6, 1)&.first)
73
100
  return unless loc
74
101
 
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Logging
7
+ module HeaderBuilder
8
+ EXCEPTION_PRIORITY = { warn: 0, error: 5, fatal: 9 }.freeze
9
+
10
+ private
11
+
12
+ def build_exception_headers(event, comp, level)
13
+ headers = {
14
+ 'legion_protocol_version' => '2.0',
15
+ 'x-error-fingerprint' => event[:error_fingerprint],
16
+ 'x-exception-class' => event[:exception_class],
17
+ 'x-handled' => event[:handled].to_s,
18
+ 'x-gem-name' => event[:gem_name].to_s,
19
+ 'x-lex-version' => event[:lex_version].to_s,
20
+ 'x-component-type' => comp.to_s,
21
+ 'x-level' => level.to_s
22
+ }
23
+ append_legion_version_header(headers)
24
+ append_optional_header(headers, 'x-task-id', event[:task_id])
25
+ append_optional_header(headers, 'x-conversation-id', event[:conversation_id])
26
+ append_optional_header(headers, 'x-chain-id', event[:chain_id])
27
+ append_optional_header(headers, 'x-user', event[:user])
28
+ append_identity_headers(headers)
29
+ headers
30
+ end
31
+
32
+ def build_exception_properties(event, level)
33
+ {
34
+ content_type: 'application/json',
35
+ message_id: SecureRandom.uuid,
36
+ correlation_id: event[:error_fingerprint],
37
+ timestamp: Time.now.to_i,
38
+ app_id: 'legionio',
39
+ type: 'exception_event',
40
+ priority: EXCEPTION_PRIORITY[level] || 5,
41
+ delivery_mode: 2
42
+ }
43
+ end
44
+
45
+ def append_identity_headers(headers)
46
+ return unless defined?(Legion::Identity::Process)
47
+ return if Legion::Identity::Process.respond_to?(:resolved?) && !Legion::Identity::Process.resolved?
48
+
49
+ id = identity_hash
50
+ append_optional_header(headers, 'x-legion-identity-canonical-name', id[:canonical_name])
51
+ append_optional_header(headers, 'x-legion-identity-trust', id[:trust])
52
+ append_optional_header(headers, 'x-legion-identity-id', id[:id])
53
+ append_optional_header(headers, 'x-legion-identity-kind', id[:kind])
54
+ append_optional_header(headers, 'x-legion-identity-mode', id[:mode])
55
+ append_optional_header(headers, 'x-legion-identity-source', id[:source])
56
+ headers['x-legion-identity-db-principal-id'] = id[:db_principal_id] if id[:db_principal_id]
57
+ headers['x-legion-identity-db-identity-id'] = id[:db_identity_id] if id[:db_identity_id]
58
+ rescue StandardError
59
+ nil
60
+ end
61
+
62
+ def append_optional_header(headers, key, value)
63
+ return if value.nil?
64
+ return if value.respond_to?(:empty?) && value.empty?
65
+
66
+ headers[key] = value.to_s
67
+ end
68
+
69
+ def append_legion_version_header(headers)
70
+ append_optional_header(headers, 'x-legion-version', Legion::VERSION) if defined?(Legion::VERSION)
71
+ end
72
+
73
+ def identity_hash
74
+ process = Legion::Identity::Process
75
+ return process.identity_hash if process.respond_to?(:identity_hash)
76
+
77
+ {
78
+ canonical_name: identity_value(process, :canonical_name),
79
+ id: identity_value(process, :id),
80
+ kind: identity_value(process, :kind),
81
+ mode: identity_value(process, :mode),
82
+ source: identity_value(process, :source),
83
+ trust: identity_value(process, :trust)
84
+ }
85
+ end
86
+
87
+ def identity_value(process, method_name)
88
+ process.public_send(method_name) if process.respond_to?(method_name)
89
+ end
90
+
91
+ def redaction_enabled?
92
+ return false unless defined?(Legion::Settings)
93
+
94
+ loader = Legion::Settings.instance_variable_get(:@loader)
95
+ return false unless loader
96
+
97
+ loader.dig(:logging, :redaction, :enabled) == true
98
+ rescue StandardError
99
+ false
100
+ end
101
+ end
102
+ end
103
+ end
@@ -3,10 +3,13 @@
3
3
  require 'securerandom'
4
4
  require_relative 'tagged_logger'
5
5
  require_relative 'method_tracer'
6
+ require_relative 'header_builder'
6
7
 
7
8
  module Legion
8
9
  module Logging
9
10
  module Helper
11
+ include Legion::Logging::HeaderBuilder
12
+
10
13
  SEGMENT_CACHE = {} # rubocop:disable Style/MutableConstant
11
14
  SEGMENT_CACHE_MUTEX = Mutex.new
12
15
  private_constant :SEGMENT_CACHE_MUTEX
@@ -32,7 +35,6 @@ module Legion
32
35
  'middleware' => :middleware
33
36
  }.freeze
34
37
 
35
- EXCEPTION_PRIORITY = { warn: 0, error: 5, fatal: 9 }.freeze
36
38
  EXCEPTION_COLORS = {
37
39
  fatal: :darkred,
38
40
  error: :red,
@@ -359,27 +361,64 @@ module Legion
359
361
  return unless defined?(Legion::Settings)
360
362
  return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
361
363
 
362
- key = derive_component_settings_key
363
- return unless key
364
+ keys = derive_component_settings_keys
365
+ return unless keys&.any?
366
+
367
+ if keys.length > 1
368
+ result = dig_settings(Legion::Settings[:extensions], keys)
369
+ return result if result.is_a?(Hash)
370
+ end
364
371
 
365
- top_level = Legion::Settings[key]
372
+ single_key = keys.length == 1 ? keys.first : keys.join('_').to_sym
373
+ top_level = Legion::Settings[single_key]
366
374
  return top_level if top_level.is_a?(Hash)
367
375
 
368
- extension_settings = Legion::Settings.dig(:extensions, key)
376
+ extension_settings = if keys.length > 1
377
+ dig_settings(Legion::Settings[:extensions], keys)
378
+ else
379
+ Legion::Settings.dig(:extensions, single_key)
380
+ end
369
381
  extension_settings if extension_settings.is_a?(Hash)
370
382
  rescue StandardError
371
383
  nil
372
384
  end
373
385
 
374
- def derive_component_settings_key
386
+ def derive_component_settings_keys
387
+ key = respond_to?(:ancestors) ? ancestors.first : self.class
388
+ parts = key.to_s.split('::')
389
+ parts.shift if parts.first == 'Legion'
390
+
391
+ if parts.first == 'Extensions'
392
+ parts.shift
393
+ ext_parts = parts.take_while { |p| !COMPONENT_MAP.key?(p.downcase) }
394
+ return ext_parts.map { |p| camelize_to_snake_key(p).to_sym } if ext_parts.any?
395
+ elsif parts.first && !parts.first.start_with?('#')
396
+ return [camelize_to_snake_key(parts.first).to_sym]
397
+ end
398
+
375
399
  base = log_name
376
400
  return unless base
377
401
 
378
- base.to_s.tr('-', '_').to_sym
402
+ base.to_s.split('-').map { |s| s.tr('-', '_').to_sym }
379
403
  rescue StandardError
380
404
  nil
381
405
  end
382
406
 
407
+ def camelize_to_snake_key(str)
408
+ str.to_s
409
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
410
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
411
+ .downcase
412
+ end
413
+
414
+ def dig_settings(hash, keys)
415
+ keys.reduce(hash) do |current, key|
416
+ return nil unless current.is_a?(Hash)
417
+
418
+ current[key] || current[key.to_s]
419
+ end
420
+ end
421
+
383
422
  def global_log_level
384
423
  runtime_level = if defined?(Legion::Logging) &&
385
424
  Legion::Logging.respond_to?(:current_settings)
@@ -468,15 +507,7 @@ module Legion
468
507
  Thread.current[:legion_log_segments] = segments
469
508
 
470
509
  message = format_exception_output(exception, event)
471
- message = Legion::Logging::Redactor.redact_string(message) if defined?(Legion::Logging::Redactor) && redaction_enabled?
472
- message = colorize_exception(message, level) if Legion::Logging.respond_to?(:color) && Legion::Logging.color
473
-
474
- logger = Legion::Logging.respond_to?(:log) ? Legion::Logging.log : nil
475
- if logger.respond_to?(level)
476
- logger.public_send(level, message)
477
- elsif Legion::Logging.respond_to?(level)
478
- Legion::Logging.public_send(level, message)
479
- end
510
+ Legion::Logging.public_send(level, message) if Legion::Logging.respond_to?(level)
480
511
  ensure
481
512
  Thread.current[:legion_log_segments] = prev_segs
482
513
  end
@@ -524,17 +555,6 @@ module Legion
524
555
  parts.join(' | ')
525
556
  end
526
557
 
527
- def redaction_enabled?
528
- return false unless defined?(Legion::Settings)
529
-
530
- loader = Legion::Settings.instance_variable_get(:@loader)
531
- return false unless loader
532
-
533
- loader.dig(:logging, :redaction, :enabled) == true
534
- rescue StandardError
535
- false
536
- end
537
-
538
558
  # -- Exception structured publish --
539
559
 
540
560
  def publish_exception(event, level)
@@ -556,85 +576,6 @@ module Legion
556
576
  defined?(Legion::Logging::EventBuilder) &&
557
577
  Legion::Logging.respond_to?(:exception_writer)
558
578
  end
559
-
560
- def build_exception_headers(event, comp, level)
561
- headers = {
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
570
- }
571
- append_legion_version_header(headers)
572
- headers['x-task-id'] = event[:task_id].to_s if event[:task_id]
573
- headers['x-conversation-id'] = event[:conversation_id].to_s if event[:conversation_id]
574
- headers['x-chain-id'] = event[:chain_id].to_s if event[:chain_id]
575
- headers['x-user'] = event[:user].to_s if event[:user]
576
- append_identity_headers(headers)
577
- headers
578
- end
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
-
626
- def build_exception_properties(event, level)
627
- {
628
- content_type: 'application/json',
629
- message_id: SecureRandom.uuid,
630
- correlation_id: event[:error_fingerprint],
631
- timestamp: Time.now.to_i,
632
- app_id: 'legionio',
633
- type: 'exception_event',
634
- priority: EXCEPTION_PRIORITY[level] || 5,
635
- delivery_mode: 2
636
- }
637
- end
638
579
  end
639
580
  end
640
581
  end
@@ -19,7 +19,7 @@ module Legion
19
19
  def fire(level, message, event)
20
20
  return unless @enabled
21
21
 
22
- hooks_for(level).each do |hook|
22
+ hooks_for(level).dup.each do |hook|
23
23
  hook.call(message, event)
24
24
  rescue StandardError => e
25
25
  warn("Legion::Logging::Hooks#fire callback failed: #{e.message}")
@@ -1,16 +1,18 @@
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
14
16
 
15
17
  def trace(raw_message = nil, size: @trace_size, log_caller: true)
16
18
  return unless @trace_enabled
@@ -26,59 +28,78 @@ module Legion
26
28
  log.unknown(message)
27
29
  end
28
30
 
29
- def debug(message = nil)
31
+ def debug(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
30
32
  return unless log.level < 1
31
33
 
32
34
  message = yield if message.nil? && block_given?
33
35
  raw = maybe_redact(message)
34
36
  formatted = format_message_for_level(:debug, raw)
35
- write_async_or_sync(:debug, formatted, raw)
37
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
38
+ exchange_id: exchange_id, chain_id: chain_id) do
39
+ write_async_or_sync(:debug, formatted, raw)
40
+ end
36
41
  end
37
42
 
38
- def info(message = nil)
43
+ def info(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
39
44
  return unless log.level < 2
40
45
 
41
46
  message = yield if message.nil? && block_given?
42
47
  raw = maybe_redact(message)
43
48
  formatted = format_message_for_level(:info, raw)
44
- write_async_or_sync(:info, formatted, raw)
49
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
50
+ exchange_id: exchange_id, chain_id: chain_id) do
51
+ write_async_or_sync(:info, formatted, raw)
52
+ end
45
53
  end
46
54
 
47
- def warn(message = nil)
55
+ def warn(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
48
56
  return unless log.level < 3
49
57
 
50
58
  message = yield if message.nil? && block_given?
51
59
  raw = maybe_redact(message)
52
60
  formatted = format_message_for_level(:warn, raw)
53
- write_async_or_sync(:warn, formatted, raw, writer_context: build_writer_context(:warn, raw))
61
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
62
+ exchange_id: exchange_id, chain_id: chain_id) do
63
+ write_async_or_sync(:warn, formatted, raw, writer_context: build_writer_context(:warn, raw))
64
+ end
54
65
  end
55
66
 
56
- def error(message = nil)
67
+ def error(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
57
68
  return unless log.level < 4
58
69
 
59
70
  message = yield if message.nil? && block_given?
60
71
  raw = maybe_redact(message)
61
72
  formatted = format_message_for_level(:error, raw)
62
- write_async_or_sync(:error, formatted, raw, writer_context: build_writer_context(:error, raw))
73
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
74
+ exchange_id: exchange_id, chain_id: chain_id) do
75
+ write_async_or_sync(:error, formatted, raw, writer_context: build_writer_context(:error, raw))
76
+ end
63
77
  end
64
78
 
65
- def fatal(message = nil)
79
+ def fatal(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
66
80
  return unless log.level < 5
67
81
 
68
82
  message = yield if message.nil? && block_given?
69
83
  raw = maybe_redact(message)
70
84
  formatted = format_message_for_level(:fatal, raw)
71
- write_async_or_sync(:fatal, formatted, raw, writer_context: build_writer_context(:fatal, raw))
85
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
86
+ exchange_id: exchange_id, chain_id: chain_id) do
87
+ write_async_or_sync(:fatal, formatted, raw, writer_context: build_writer_context(:fatal, raw))
88
+ end
72
89
  end
73
90
 
74
- def unknown(message = nil)
91
+ def unknown(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
75
92
  message = yield if message.nil? && block_given?
76
93
  raw = maybe_redact(message)
77
94
  formatted = format_message_for_level(:unknown, raw)
78
- write_async_or_sync(:unknown, formatted, raw)
95
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
96
+ exchange_id: exchange_id, chain_id: chain_id) do
97
+ write_async_or_sync(:unknown, formatted, raw)
98
+ end
79
99
  end
80
100
 
81
- def emit_tagged(level, message = nil, segments: nil, method_ctx: nil)
101
+ def emit_tagged(level, message = nil, segments: nil, method_ctx: nil,
102
+ task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
82
103
  level = level.to_sym
83
104
  message = yield if message.nil? && block_given?
84
105
  return if message.nil?
@@ -87,19 +108,26 @@ module Legion
87
108
  formatted = format_message_for_level(level, raw)
88
109
 
89
110
  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
111
+ with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
112
+ exchange_id: exchange_id, chain_id: chain_id) do
113
+ ctx = %i[warn error fatal].include?(level) ? build_writer_context(level, raw) : nil
114
+ writer = @async_writer
115
+ caller_trace = capture_runner_trace_for_async
116
+ if writer&.alive?
117
+ writer.push(AsyncWriter::LogEntry.new(
118
+ level: level, message: formatted, writer_context: ctx,
119
+ segments: Thread.current[:legion_log_segments],
120
+ method_ctx: Thread.current[:legion_log_method],
121
+ caller_trace: caller_trace,
122
+ conv_id: Thread.current[:legion_log_conv_id],
123
+ request_id: Thread.current[:legion_log_request_id],
124
+ exchange_id: Thread.current[:legion_log_exchange_id],
125
+ chain_id: Thread.current[:legion_log_chain_id]
126
+ ))
127
+ else
128
+ with_caller_trace(caller_trace) { write_forced(level, formatted) }
129
+ fire_log_writer(level, raw) if ctx
130
+ end
103
131
  end
104
132
  end
105
133
  end
@@ -237,6 +265,29 @@ module Legion
237
265
  level.to_s.upcase
238
266
  end
239
267
 
268
+ def with_context_ids(task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil)
269
+ prev_conv = Thread.current[:legion_log_conv_id]
270
+ prev_req = Thread.current[:legion_log_request_id]
271
+ prev_exchange = Thread.current[:legion_log_exchange_id]
272
+ prev_chain = Thread.current[:legion_log_chain_id]
273
+
274
+ context_id = conv_id || task_id || Thread.current[:legion_log_conv_id]
275
+ req_id = request_id || Thread.current[:legion_log_request_id]
276
+ exch_id = exchange_id || Thread.current[:legion_log_exchange_id]
277
+ ch_id = chain_id || Thread.current[:legion_log_chain_id]
278
+
279
+ Thread.current[:legion_log_conv_id] = context_id if context_id.is_a?(String) && !context_id.empty?
280
+ Thread.current[:legion_log_request_id] = req_id if req_id.is_a?(String) && !req_id.empty?
281
+ Thread.current[:legion_log_exchange_id] = exch_id if exch_id.is_a?(String) && !exch_id.empty?
282
+ Thread.current[:legion_log_chain_id] = ch_id if ch_id.is_a?(String) && !ch_id.empty?
283
+ yield
284
+ ensure
285
+ Thread.current[:legion_log_conv_id] = prev_conv
286
+ Thread.current[:legion_log_request_id] = prev_req
287
+ Thread.current[:legion_log_exchange_id] = prev_exchange
288
+ Thread.current[:legion_log_chain_id] = prev_chain
289
+ end
290
+
240
291
  def write_async_or_sync(level, formatted_message, raw_message, writer_context: nil)
241
292
  writer = @async_writer
242
293
  caller_trace = capture_runner_trace_for_async
@@ -247,7 +298,11 @@ module Legion
247
298
  writer_context: writer_context,
248
299
  segments: Thread.current[:legion_log_segments],
249
300
  method_ctx: Thread.current[:legion_log_method],
250
- caller_trace: caller_trace
301
+ caller_trace: caller_trace,
302
+ conv_id: Thread.current[:legion_log_conv_id],
303
+ request_id: Thread.current[:legion_log_request_id],
304
+ exchange_id: Thread.current[:legion_log_exchange_id],
305
+ chain_id: Thread.current[:legion_log_chain_id]
251
306
  ))
252
307
  return if queued
253
308
  end
@@ -259,7 +314,7 @@ module Legion
259
314
  end
260
315
 
261
316
  def capture_runner_trace_for_async
262
- build_runner_trace(caller_locations(4, 1)&.first)
317
+ build_runner_trace(caller_locations(5, 1)&.first)
263
318
  end
264
319
 
265
320
  def with_caller_trace(caller_trace)
@@ -270,17 +325,6 @@ module Legion
270
325
  Thread.current[:legion_log_caller] = prev_caller_trace
271
326
  end
272
327
 
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
328
  def build_log_headers(event, component, level)
285
329
  headers = {
286
330
  'legion_protocol_version' => '2.0',
@@ -301,28 +345,11 @@ module Legion
301
345
  timestamp: Time.now.to_i,
302
346
  app_id: 'legionio',
303
347
  type: 'log_event',
304
- priority: EXCEPTION_PRIORITY[level] || 0,
348
+ priority: HeaderBuilder::EXCEPTION_PRIORITY[level] || 0,
305
349
  delivery_mode: 2
306
350
  }
307
351
  end
308
352
 
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
353
  def publish_exception_event(event, level)
327
354
  lex_name = event[:lex] || 'core'
328
355
  comp = event[:component_type] || :unknown
@@ -332,67 +359,6 @@ module Legion
332
359
  Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
333
360
  end
334
361
 
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
352
- end
353
-
354
- def append_optional_header(headers, key, value)
355
- return if value.nil?
356
- return if value.respond_to?(:empty?) && value.empty?
357
-
358
- headers[key] = value.to_s
359
- end
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
-
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
- }
394
- end
395
-
396
362
  def build_writer_context(level, message)
397
363
  has_writer = !Legion::Logging.instance_variable_get(:@log_writer).nil?
398
364
  has_hooks = defined?(Legion::Logging::Hooks) && Legion::Logging::Hooks.enabled?
@@ -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
 
@@ -25,43 +25,49 @@ module Legion
25
25
  end
26
26
 
27
27
  def flush
28
- return if @buffer.nil? || @buffer.empty?
28
+ @mutex ||= Mutex.new
29
+ batch = @mutex.synchronize do
30
+ return true if @buffer.nil? || @buffer.empty?
29
31
 
30
- transport = TRANSPORTS[transport_type]
31
- return unless transport
32
+ flushed = @buffer.dup
33
+ @buffer.clear
34
+ flushed
35
+ end
32
36
 
33
- @flush_mutex ||= Mutex.new
34
- @flush_mutex.synchronize do
35
- batch = nil
36
- @mutex.synchronize { batch = @buffer.dup }
37
- return if batch.empty?
37
+ transport = TRANSPORTS[transport_type]
38
+ return true unless transport
38
39
 
39
- delivered = deliver(transport, batch)
40
- @mutex.synchronize { @buffer.shift(batch.size) if delivered }
41
- delivered
42
- end
40
+ delivered = deliver(transport, batch)
41
+ @mutex.synchronize { @buffer.prepend(*batch) } unless delivered
42
+ delivered
43
43
  end
44
44
 
45
45
  def start
46
- return unless enabled?
47
- return if @flush_thread&.alive?
48
-
49
- @buffer ||= []
50
- @mutex ||= Mutex.new
51
- @flush_mutex ||= Mutex.new
52
- interval = flush_interval
53
- @flush_thread = Thread.new do
54
- loop do
55
- sleep interval
56
- flush
46
+ @start_mutex ||= Mutex.new
47
+ @start_mutex.synchronize do
48
+ return unless enabled?
49
+ return if @flush_thread&.alive?
50
+
51
+ @buffer ||= []
52
+ @mutex ||= Mutex.new
53
+ @running = true
54
+ interval = flush_interval
55
+ @flush_thread = Thread.new do
56
+ while @running
57
+ sleep interval
58
+ flush
59
+ end
57
60
  end
61
+ @flush_thread.name = 'legion-log-shipper'
62
+ @flush_thread.abort_on_exception = false
58
63
  end
59
- @flush_thread.abort_on_exception = false
60
64
  end
61
65
 
62
66
  def stop
63
- @flush_thread&.kill
67
+ @running = false
68
+ thread = @flush_thread
64
69
  @flush_thread = nil
70
+ thread&.join(5)
65
71
  flush
66
72
  end
67
73
 
@@ -74,8 +80,8 @@ module Legion
74
80
  private
75
81
 
76
82
  def buffer_event(event)
77
- @buffer ||= []
78
- @mutex ||= Mutex.new
83
+ @buffer ||= []
84
+ @mutex ||= Mutex.new
79
85
 
80
86
  full = false
81
87
  @mutex.synchronize do
@@ -28,7 +28,8 @@ module Legion
28
28
  req['Content-Type'] = 'application/json'
29
29
  req.body = ::JSON.dump({ event: redact_phi(event.to_s), time: Time.now.to_f })
30
30
 
31
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
31
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
32
+ open_timeout: 5, read_timeout: 10) do |http|
32
33
  http.request(req)
33
34
  end
34
35
  rescue StandardError => e
@@ -32,44 +32,44 @@ module Legion
32
32
  @level_value
33
33
  end
34
34
 
35
- def debug(message = nil)
35
+ def debug(message = nil, **ctx)
36
36
  return unless @level_value < 1
37
37
 
38
38
  message = yield if message.nil? && block_given?
39
- with_segments { dispatch(:debug, message) }
39
+ with_segments { dispatch(:debug, message, **ctx) }
40
40
  end
41
41
 
42
- def info(message = nil)
42
+ def info(message = nil, **ctx)
43
43
  return unless @level_value < 2
44
44
 
45
45
  message = yield if message.nil? && block_given?
46
- with_segments { dispatch(:info, message) }
46
+ with_segments { dispatch(:info, message, **ctx) }
47
47
  end
48
48
 
49
- def warn(message = nil)
49
+ def warn(message = nil, **ctx)
50
50
  return unless @level_value < 3
51
51
 
52
52
  message = yield if message.nil? && block_given?
53
- with_segments { dispatch(:warn, message) }
53
+ with_segments { dispatch(:warn, message, **ctx) }
54
54
  end
55
55
 
56
- def error(message = nil)
56
+ def error(message = nil, **ctx)
57
57
  return unless @level_value < 4
58
58
 
59
59
  message = yield if message.nil? && block_given?
60
- with_segments { dispatch(:error, message) }
60
+ with_segments { dispatch(:error, message, **ctx) }
61
61
  end
62
62
 
63
- def fatal(message = nil)
63
+ def fatal(message = nil, **ctx)
64
64
  return unless @level_value < 5
65
65
 
66
66
  message = yield if message.nil? && block_given?
67
- with_segments { dispatch(:fatal, message) }
67
+ with_segments { dispatch(:fatal, message, **ctx) }
68
68
  end
69
69
 
70
- def unknown(message = nil)
70
+ def unknown(message = nil, **ctx)
71
71
  message = yield if message.nil? && block_given?
72
- with_segments { dispatch(:unknown, message) }
72
+ with_segments { dispatch(:unknown, message, **ctx) }
73
73
  end
74
74
 
75
75
  def trace(raw_message = nil, size: @trace_size, log_caller: true)
@@ -94,21 +94,23 @@ module Legion
94
94
 
95
95
  private
96
96
 
97
- def dispatch(level, message)
97
+ def dispatch(level, message, **ctx)
98
98
  return unless defined?(Legion::Logging)
99
99
 
100
100
  if Legion::Logging.respond_to?(:emit_tagged)
101
- Legion::Logging.emit_tagged(level, message, segments: @segments)
101
+ Legion::Logging.emit_tagged(level, message, segments: @segments, **ctx)
102
102
  return
103
103
  end
104
104
 
105
105
  if Legion::Logging.respond_to?(level)
106
- Legion::Logging.public_send(level, message)
106
+ Legion::Logging.public_send(level, message, **ctx)
107
107
  return
108
108
  end
109
109
 
110
110
  fallback = fallback_level(level)
111
- Legion::Logging.public_send(fallback, message) if fallback && Legion::Logging.respond_to?(fallback)
111
+ return unless fallback && Legion::Logging.respond_to?(fallback)
112
+
113
+ Legion::Logging.public_send(fallback, message, **ctx)
112
114
  end
113
115
 
114
116
  def fallback_level(level)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Logging
5
- VERSION = '1.5.5'
5
+ VERSION = '1.5.6'
6
6
  end
7
7
  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.5.5
4
+ version: 1.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -63,6 +63,7 @@ files:
63
63
  - lib/legion/logging/builder.rb
64
64
  - lib/legion/logging/category_registry.rb
65
65
  - lib/legion/logging/event_builder.rb
66
+ - lib/legion/logging/header_builder.rb
66
67
  - lib/legion/logging/helper.rb
67
68
  - lib/legion/logging/hooks.rb
68
69
  - lib/legion/logging/logger.rb