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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/CLAUDE.md +18 -10
- data/README.md +44 -10
- data/lib/legion/logging/async_writer.rb +142 -42
- data/lib/legion/logging/builder.rb +93 -38
- data/lib/legion/logging/event_builder.rb +9 -6
- data/lib/legion/logging/header_builder.rb +103 -0
- data/lib/legion/logging/helper.rb +58 -125
- data/lib/legion/logging/hooks.rb +1 -1
- data/lib/legion/logging/method_tracer.rb +13 -2
- data/lib/legion/logging/methods.rb +211 -146
- data/lib/legion/logging/multi_io.rb +2 -0
- data/lib/legion/logging/output_logger.rb +60 -0
- data/lib/legion/logging/redactor.rb +3 -2
- data/lib/legion/logging/settings.rb +21 -5
- data/lib/legion/logging/shipper/file_transport.rb +2 -1
- data/lib/legion/logging/shipper/http_transport.rb +4 -2
- data/lib/legion/logging/shipper.rb +119 -64
- data/lib/legion/logging/siem_exporter.rb +2 -1
- data/lib/legion/logging/tagged_logger.rb +18 -16
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +31 -30
- metadata +3 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
|
310
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
|
|
458
|
+
configured = Legion::Settings.dig(:logging, :transport, :log_levels)
|
|
459
|
+
return default unless configured.is_a?(Array) && !configured.empty?
|
|
360
460
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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}"
|
|
@@ -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:
|
|
9
|
-
trace:
|
|
10
|
-
trace_size:
|
|
11
|
-
extended:
|
|
12
|
-
backtrace_limit:
|
|
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
|
-
|
|
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
|