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,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'monitor'
5
+ require_relative 'output_logger'
4
6
 
5
7
  module Legion
6
8
  module Logging
7
9
  module Builder
10
+ LIFECYCLE_MUTEX = Mutex.new
11
+ private_constant :LIFECYCLE_MUTEX
12
+
8
13
  def log_format(format: :text, include_pid: false, **)
9
14
  @format = format.to_sym
10
15
  if @format == :json
@@ -20,17 +25,33 @@ module Legion
20
25
 
21
26
  def json_format(include_pid: false)
22
27
  log.formatter = proc do |severity, datetime, _progname, msg|
28
+ origin = Thread.current[:legion_log_origin]
29
+ if origin
30
+ timestamp = origin[:timestamp]
31
+ thread_id = origin[:thread]
32
+ else
33
+ timestamp = datetime.utc
34
+ thread_id = Thread.current.object_id
35
+ end
23
36
  entry = {
24
- timestamp: datetime.utc.iso8601(3),
37
+ timestamp: timestamp.iso8601(3),
25
38
  level: severity.downcase,
26
39
  message: msg.is_a?(String) ? msg.gsub(/\e\[[0-9;]*m/, '') : msg.to_s,
27
- thread: Thread.current.object_id
40
+ thread: thread_id
28
41
  }
29
42
  entry[:pid] = ::Process.pid if include_pid
30
43
  segments = Thread.current[:legion_log_segments]
31
44
  entry[:segments] = segments if segments
32
45
  method_ctx = Thread.current[:legion_log_method]
33
46
  entry[:method] = method_ctx if method_ctx
47
+ conv_id = Thread.current[:legion_log_conv_id]
48
+ entry[:conversation_id] = conv_id if conv_id.is_a?(String) && !conv_id.empty?
49
+ request_id = Thread.current[:legion_log_request_id]
50
+ entry[:request_id] = request_id if request_id.is_a?(String) && !request_id.empty?
51
+ exchange_id = Thread.current[:legion_log_exchange_id]
52
+ entry[:exchange_id] = exchange_id if exchange_id.is_a?(String) && !exchange_id.empty?
53
+ chain_id = Thread.current[:legion_log_chain_id]
54
+ entry[:chain_id] = chain_id if chain_id.is_a?(String) && !chain_id.empty?
34
55
  "#{::JSON.generate(entry)}\n"
35
56
  rescue StandardError => e
36
57
  warn("Legion::Logging::Builder#json_format formatter failed: #{e.message}")
@@ -40,6 +61,7 @@ module Legion
40
61
 
41
62
  def text_format(include_pid: false, **options)
42
63
  log.formatter = proc do |severity, datetime, _progname, msg|
64
+ datetime = Thread.current[:legion_log_origin]&.[](:timestamp)&.getlocal || datetime
43
65
  lex_name = resolve_lex_tag(options)
44
66
  runner_trace = Thread.current[:legion_log_caller] || build_runner_trace if lex_name
45
67
 
@@ -49,7 +71,12 @@ module Legion
49
71
  if runner_trace.is_a?(Hash) && (options[:extended] || severity == 'debug')
50
72
  string.concat("[#{runner_trace[:type]}:#{runner_trace[:file]}:#{runner_trace[:function]}:#{runner_trace[:line_number]}]")
51
73
  end
52
- string.concat(" #{severity} #{msg}\n")
74
+ ctx_pairs = build_context_kv_pairs
75
+ if ctx_pairs.empty?
76
+ string.concat(" #{severity} #{msg}\n")
77
+ else
78
+ string.concat(" #{severity} #{msg} #{ctx_pairs}\n")
79
+ end
53
80
  string
54
81
  end
55
82
  end
@@ -66,9 +93,23 @@ module Legion
66
93
 
67
94
  method_ctx = Thread.current[:legion_log_method]
68
95
  tag = "#{tag}{#{method_ctx}}" if tag && method_ctx
96
+
97
+ context_id = Thread.current[:legion_log_conv_id]
98
+ tag = "#{tag}{#{context_id}}" if tag && context_id.is_a?(String) && !context_id.empty?
69
99
  tag
70
100
  end
71
101
 
102
+ def build_context_kv_pairs
103
+ pairs = []
104
+ request_id = Thread.current[:legion_log_request_id]
105
+ pairs << "request_id=#{request_id}" if request_id.is_a?(String) && !request_id.empty?
106
+ exchange_id = Thread.current[:legion_log_exchange_id]
107
+ pairs << "exchange_id=#{exchange_id}" if exchange_id.is_a?(String) && !exchange_id.empty?
108
+ chain_id = Thread.current[:legion_log_chain_id]
109
+ pairs << "chain_id=#{chain_id}" if chain_id.is_a?(String) && !chain_id.empty?
110
+ pairs.join(' ')
111
+ end
112
+
72
113
  def build_runner_trace(loc = caller_locations(6, 1)&.first)
73
114
  return unless loc
74
115
 
@@ -90,25 +131,31 @@ module Legion
90
131
  end
91
132
 
92
133
  def set_log(logfile: nil, log_stdout: nil, **)
93
- previous_log = @log
94
-
95
- if logfile && log_stdout != false
96
- path = prepare_log_path(logfile)
97
- require_relative 'multi_io'
98
- file = File.new(path, 'a')
99
- file.sync = true
100
- io = MultiIO.new($stdout, file)
101
- @log = ::Logger.new(io)
102
- elsif logfile
103
- file = File.new(prepare_log_path(logfile), 'a')
104
- file.sync = true
105
- @log = ::Logger.new(file)
106
- else
107
- @log = ::Logger.new($stdout)
108
- end
134
+ with_lifecycle_lock do
135
+ return false if @async_writer && stop_async_writer == false
136
+
137
+ previous_log = @log
138
+
139
+ if logfile && log_stdout != false
140
+ path = prepare_log_path(logfile)
141
+ require_relative 'multi_io'
142
+ file = File.new(path, 'a')
143
+ file.sync = true
144
+ io = MultiIO.new($stdout, file)
145
+ @log = Legion::Logging::OutputLogger.new(io)
146
+ elsif logfile
147
+ file = File.new(prepare_log_path(logfile), 'a')
148
+ file.sync = true
149
+ @log = Legion::Logging::OutputLogger.new(file)
150
+ else
151
+ @log = Legion::Logging::OutputLogger.new($stdout)
152
+ end
153
+
154
+ @log.async_writer = @async_writer
109
155
 
110
- close_replaced_log(previous_log)
111
- @log
156
+ close_replaced_log(previous_log)
157
+ @log
158
+ end
112
159
  end
113
160
 
114
161
  def prepare_log_path(path)
@@ -148,31 +195,39 @@ module Legion
148
195
  (@async == true && @async_writer&.alive?) || false
149
196
  end
150
197
 
151
- # rubocop:disable Naming/PredicateMethod
152
- def start_async_writer(buffer_size: 10_000)
153
- require_relative 'async_writer'
154
- return false if @async_writer&.alive? && stop_async_writer == false
198
+ def start_async_writer(buffer_size: 10_000, timeout: 2)
199
+ with_lifecycle_lock do
200
+ require_relative 'async_writer'
201
+ return false if @async_writer && stop_async_writer(timeout: timeout) == false
155
202
 
156
- @async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
157
- @async_writer.start
158
- @async = true
159
- true
203
+ @async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
204
+ log.async_writer = @async_writer
205
+ @async_writer.start
206
+ @async = true
207
+ true
208
+ end
160
209
  end
161
210
 
162
- def stop_async_writer
163
- writer = @async_writer
164
- stopped = writer&.stop
165
- return false if stopped == false
211
+ def stop_async_writer(timeout: 2)
212
+ with_lifecycle_lock do
213
+ return true unless @async_writer
166
214
 
167
- close_replaced_log(writer.logger) if writer.respond_to?(:logger)
168
- @async_writer = nil if @async_writer.equal?(writer)
169
- @async = false
170
- true
215
+ @async_writer.stop(timeout: timeout)
216
+ end
171
217
  end
172
- # rubocop:enable Naming/PredicateMethod
173
218
 
174
219
  private
175
220
 
221
+ def with_lifecycle_lock
222
+ lock = LIFECYCLE_MUTEX.synchronize { @lifecycle_lock ||= Monitor.new }
223
+ entered = lock.try_enter
224
+ return false unless entered
225
+
226
+ yield
227
+ ensure
228
+ lock.exit if entered
229
+ end
230
+
176
231
  def close_replaced_log(logger)
177
232
  return unless logger
178
233
  return if logger.equal?(@log)
@@ -69,9 +69,10 @@ module Legion
69
69
  bt = Array(exception.backtrace)
70
70
  cf_file, cf_line, cf_func = parse_backtrace_location(bt.first) ||
71
71
  caller_location(caller_offset)
72
+ origin = Thread.current[:legion_log_origin]
72
73
 
73
74
  event = {
74
- timestamp: Time.now.utc.iso8601(3),
75
+ timestamp: (origin&.[](:timestamp) || Time.now.utc).iso8601(3),
75
76
  level: level,
76
77
  exception_class: exception.class.name,
77
78
  message: truncate_bytes(exception.message.to_s, MAX_MESSAGE_BYTES),
@@ -89,7 +90,7 @@ module Legion
89
90
  ruby_version: "#{RUBY_VERSION} #{RUBY_PLATFORM}",
90
91
  handled: handled,
91
92
  pid: ::Process.pid,
92
- thread: Thread.current.object_id
93
+ thread: origin&.[](:thread) || Thread.current.object_id
93
94
  }
94
95
 
95
96
  event[:task_id] = task_id if task_id
@@ -147,12 +148,13 @@ module Legion
147
148
  private
148
149
 
149
150
  def base_fields(level, message)
151
+ origin = Thread.current[:legion_log_origin]
150
152
  {
151
- timestamp: Time.now.utc.iso8601(3),
153
+ timestamp: (origin&.[](:timestamp) || Time.now.utc).iso8601(3),
152
154
  level: level,
153
155
  message: message.is_a?(Exception) ? message.message : strip_ansi(message.to_s),
154
156
  pid: ::Process.pid,
155
- thread: Thread.current.object_id
157
+ thread: origin&.[](:thread) || Thread.current.object_id
156
158
  }
157
159
  end
158
160
 
@@ -166,6 +168,7 @@ module Legion
166
168
 
167
169
  def add_node(event)
168
170
  return unless defined?(Legion::Settings)
171
+ return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
169
172
 
170
173
  name = begin
171
174
  Legion::Settings[:client][:name]
@@ -177,7 +180,7 @@ module Legion
177
180
  end
178
181
 
179
182
  def add_caller_info(event, offset)
180
- loc = caller_locations(offset + 1, 1)&.first
183
+ loc = Thread.current[:legion_log_origin]&.[](:location) || caller_locations(offset + 1, 1)&.first
181
184
  return unless loc
182
185
 
183
186
  event[:caller] = {
@@ -268,7 +271,7 @@ module Legion
268
271
  end
269
272
 
270
273
  def caller_location(offset)
271
- loc = caller_locations(offset + 2, 1)&.first
274
+ loc = Thread.current[:legion_log_origin]&.[](:location) || caller_locations(offset + 2, 1)&.first
272
275
  return [nil, nil, nil] unless loc
273
276
 
274
277
  [loc.absolute_path || loc.path, loc.lineno, loc.base_label]
@@ -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,
@@ -78,6 +80,13 @@ module Legion
78
80
  end
79
81
 
80
82
  def handle_exception(exception, task_id: nil, level: :error, handled: true, **opts)
83
+ if Legion::Logging.async_producer?
84
+ return Legion::Logging.defer_log_call(
85
+ receiver: self, method_name: :handle_exception, arguments: [exception],
86
+ keywords: { task_id: task_id, level: level, handled: handled, **opts }
87
+ )
88
+ end
89
+
81
90
  segments = derive_log_segments
82
91
  spec = gem_spec
83
92
  ctx = Thread.current[:legion_context] || {}
@@ -99,7 +108,7 @@ module Legion
99
108
  event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
100
109
 
101
110
  write_exception_to_log(exception, event, level, segments)
102
- publish_exception(event, level) if structured_exception_support?
111
+ Legion::Logging.publish_exception(event: event, level: level) if structured_exception_support?
103
112
  end
104
113
 
105
114
  def self.included(base)
@@ -359,27 +368,64 @@ module Legion
359
368
  return unless defined?(Legion::Settings)
360
369
  return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
361
370
 
362
- key = derive_component_settings_key
363
- return unless key
371
+ keys = derive_component_settings_keys
372
+ return unless keys&.any?
364
373
 
365
- top_level = Legion::Settings[key]
374
+ if keys.length > 1
375
+ result = dig_settings(Legion::Settings[:extensions], keys)
376
+ return result if result.is_a?(Hash)
377
+ end
378
+
379
+ single_key = keys.length == 1 ? keys.first : keys.join('_').to_sym
380
+ top_level = Legion::Settings[single_key]
366
381
  return top_level if top_level.is_a?(Hash)
367
382
 
368
- extension_settings = Legion::Settings.dig(:extensions, key)
383
+ extension_settings = if keys.length > 1
384
+ dig_settings(Legion::Settings[:extensions], keys)
385
+ else
386
+ Legion::Settings.dig(:extensions, single_key)
387
+ end
369
388
  extension_settings if extension_settings.is_a?(Hash)
370
389
  rescue StandardError
371
390
  nil
372
391
  end
373
392
 
374
- def derive_component_settings_key
393
+ def derive_component_settings_keys
394
+ key = respond_to?(:ancestors) ? ancestors.first : self.class
395
+ parts = key.to_s.split('::')
396
+ parts.shift if parts.first == 'Legion'
397
+
398
+ if parts.first == 'Extensions'
399
+ parts.shift
400
+ ext_parts = parts.take_while { |p| !COMPONENT_MAP.key?(p.downcase) }
401
+ return ext_parts.map { |p| camelize_to_snake_key(p).to_sym } if ext_parts.any?
402
+ elsif parts.first && !parts.first.start_with?('#')
403
+ return [camelize_to_snake_key(parts.first).to_sym]
404
+ end
405
+
375
406
  base = log_name
376
407
  return unless base
377
408
 
378
- base.to_s.tr('-', '_').to_sym
409
+ base.to_s.split('-').map { |s| s.tr('-', '_').to_sym }
379
410
  rescue StandardError
380
411
  nil
381
412
  end
382
413
 
414
+ def camelize_to_snake_key(str)
415
+ str.to_s
416
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
417
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
418
+ .downcase
419
+ end
420
+
421
+ def dig_settings(hash, keys)
422
+ keys.reduce(hash) do |current, key|
423
+ return nil unless current.is_a?(Hash)
424
+
425
+ current[key] || current[key.to_s]
426
+ end
427
+ end
428
+
383
429
  def global_log_level
384
430
  runtime_level = if defined?(Legion::Logging) &&
385
431
  Legion::Logging.respond_to?(:current_settings)
@@ -468,15 +514,7 @@ module Legion
468
514
  Thread.current[:legion_log_segments] = segments
469
515
 
470
516
  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
517
+ Legion::Logging.public_send(level, message) if Legion::Logging.respond_to?(level)
480
518
  ensure
481
519
  Thread.current[:legion_log_segments] = prev_segs
482
520
  end
@@ -488,7 +526,9 @@ module Legion
488
526
  lines << " #{context_line}" unless context_line.empty?
489
527
 
490
528
  max_frames = if event[:backtrace_limit].nil?
491
- defined?(Legion::Settings) ? Legion::Settings[:logging][:backtrace_limit] : nil
529
+ if defined?(Legion::Settings) && (!Legion::Settings.respond_to?(:loaded?) || Legion::Settings.loaded?)
530
+ Legion::Settings.dig(:logging, :backtrace_limit)
531
+ end
492
532
  else
493
533
  event[:backtrace_limit]
494
534
  end
@@ -524,117 +564,10 @@ module Legion
524
564
  parts.join(' | ')
525
565
  end
526
566
 
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
- # -- Exception structured publish --
539
-
540
- def publish_exception(event, level)
541
- return unless structured_exception_support?
542
-
543
- lex_name = event[:lex] || 'core'
544
- comp = event[:component_type] || :unknown
545
- routing_key = "legion.logging.exception.#{level}.#{lex_name}.#{comp}"
546
-
547
- headers = build_exception_headers(event, comp, level)
548
- properties = build_exception_properties(event, level)
549
-
550
- Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
551
- rescue StandardError => e
552
- Legion::Logging.warn("Failed to publish exception event: #{e.class}: #{e.message}") if Legion::Logging.respond_to?(:warn)
553
- end
554
-
555
567
  def structured_exception_support?
556
568
  defined?(Legion::Logging::EventBuilder) &&
557
569
  Legion::Logging.respond_to?(:exception_writer)
558
570
  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
571
  end
639
572
  end
640
573
  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}")
@@ -16,6 +16,8 @@ module Legion
16
16
 
17
17
  base_name = base.to_s
18
18
  tp = TracePoint.new(:call, :return) do |trace|
19
+ next if Thread.current[:legion_trace_emitting]
20
+
19
21
  next unless trace.defined_class == base || (match_singleton && trace.defined_class == base.singleton_class)
20
22
 
21
23
  stack = (Thread.current[:_legion_trace_stack] ||= [])
@@ -25,12 +27,12 @@ module Legion
25
27
  params = format_params(trace)
26
28
  params_segment = params.empty? ? '' : ", #{params.join(', ')}"
27
29
  indent = ' ' * stack.size
28
- puts "#{indent}-> #{trace.method_id}, #{base_name}#{params_segment}"
30
+ emit_trace(message: "#{indent}-> #{trace.method_id}, #{base_name}#{params_segment}")
29
31
  stack.push(trace.method_id)
30
32
  when :return
31
33
  stack.pop
32
34
  indent = ' ' * stack.size
33
- puts "#{indent}<- #{trace.method_id}, #{base_name}"
35
+ emit_trace(message: "#{indent}<- #{trace.method_id}, #{base_name}")
34
36
  end
35
37
  end
36
38
  tp.enable
@@ -52,6 +54,15 @@ module Legion
52
54
  end
53
55
  end
54
56
 
57
+ def self.emit_trace(message:)
58
+ previous = Thread.current[:legion_trace_emitting]
59
+ Thread.current[:legion_trace_emitting] = true
60
+ Legion::Logging.unknown(message)
61
+ ensure
62
+ Thread.current[:legion_trace_emitting] = previous
63
+ end
64
+ private_class_method :emit_trace
65
+
55
66
  def self.format_params(trace_point)
56
67
  trace_point.parameters.filter_map do |type, name|
57
68
  next unless name