legion-logging 1.5.6 → 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 +84 -0
- data/CLAUDE.md +18 -10
- data/README.md +44 -10
- data/lib/legion/logging/async_writer.rb +125 -36
- data/lib/legion/logging/builder.rb +65 -37
- data/lib/legion/logging/event_builder.rb +9 -6
- data/lib/legion/logging/helper.rb +11 -19
- data/lib/legion/logging/method_tracer.rb +13 -2
- data/lib/legion/logging/methods.rb +152 -53
- 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 +118 -69
- data/lib/legion/logging/siem_exporter.rb +1 -1
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +31 -30
- metadata +2 -1
|
@@ -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,11 +25,19 @@ 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:
|
|
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:
|
|
40
|
+
thread: thread_id
|
|
28
41
|
}
|
|
29
42
|
entry[:pid] = ::Process.pid if include_pid
|
|
30
43
|
segments = Thread.current[:legion_log_segments]
|
|
@@ -48,6 +61,7 @@ module Legion
|
|
|
48
61
|
|
|
49
62
|
def text_format(include_pid: false, **options)
|
|
50
63
|
log.formatter = proc do |severity, datetime, _progname, msg|
|
|
64
|
+
datetime = Thread.current[:legion_log_origin]&.[](:timestamp)&.getlocal || datetime
|
|
51
65
|
lex_name = resolve_lex_tag(options)
|
|
52
66
|
runner_trace = Thread.current[:legion_log_caller] || build_runner_trace if lex_name
|
|
53
67
|
|
|
@@ -117,25 +131,31 @@ module Legion
|
|
|
117
131
|
end
|
|
118
132
|
|
|
119
133
|
def set_log(logfile: nil, log_stdout: nil, **)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
136
153
|
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
@log.async_writer = @async_writer
|
|
155
|
+
|
|
156
|
+
close_replaced_log(previous_log)
|
|
157
|
+
@log
|
|
158
|
+
end
|
|
139
159
|
end
|
|
140
160
|
|
|
141
161
|
def prepare_log_path(path)
|
|
@@ -175,31 +195,39 @@ module Legion
|
|
|
175
195
|
(@async == true && @async_writer&.alive?) || false
|
|
176
196
|
end
|
|
177
197
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
182
202
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
187
209
|
end
|
|
188
210
|
|
|
189
|
-
def stop_async_writer
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return false if stopped == false
|
|
211
|
+
def stop_async_writer(timeout: 2)
|
|
212
|
+
with_lifecycle_lock do
|
|
213
|
+
return true unless @async_writer
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
@async = false
|
|
197
|
-
true
|
|
215
|
+
@async_writer.stop(timeout: timeout)
|
|
216
|
+
end
|
|
198
217
|
end
|
|
199
|
-
# rubocop:enable Naming/PredicateMethod
|
|
200
218
|
|
|
201
219
|
private
|
|
202
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
|
+
|
|
203
231
|
def close_replaced_log(logger)
|
|
204
232
|
return unless logger
|
|
205
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]
|
|
@@ -80,6 +80,13 @@ module Legion
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
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
|
+
|
|
83
90
|
segments = derive_log_segments
|
|
84
91
|
spec = gem_spec
|
|
85
92
|
ctx = Thread.current[:legion_context] || {}
|
|
@@ -101,7 +108,7 @@ module Legion
|
|
|
101
108
|
event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
|
|
102
109
|
|
|
103
110
|
write_exception_to_log(exception, event, level, segments)
|
|
104
|
-
publish_exception(event, level) if structured_exception_support?
|
|
111
|
+
Legion::Logging.publish_exception(event: event, level: level) if structured_exception_support?
|
|
105
112
|
end
|
|
106
113
|
|
|
107
114
|
def self.included(base)
|
|
@@ -519,7 +526,9 @@ module Legion
|
|
|
519
526
|
lines << " #{context_line}" unless context_line.empty?
|
|
520
527
|
|
|
521
528
|
max_frames = if event[:backtrace_limit].nil?
|
|
522
|
-
defined?(Legion::Settings)
|
|
529
|
+
if defined?(Legion::Settings) && (!Legion::Settings.respond_to?(:loaded?) || Legion::Settings.loaded?)
|
|
530
|
+
Legion::Settings.dig(:logging, :backtrace_limit)
|
|
531
|
+
end
|
|
523
532
|
else
|
|
524
533
|
event[:backtrace_limit]
|
|
525
534
|
end
|
|
@@ -555,23 +564,6 @@ module Legion
|
|
|
555
564
|
parts.join(' | ')
|
|
556
565
|
end
|
|
557
566
|
|
|
558
|
-
# -- Exception structured publish --
|
|
559
|
-
|
|
560
|
-
def publish_exception(event, level)
|
|
561
|
-
return unless structured_exception_support?
|
|
562
|
-
|
|
563
|
-
lex_name = event[:lex] || 'core'
|
|
564
|
-
comp = event[:component_type] || :unknown
|
|
565
|
-
routing_key = "legion.logging.exception.#{level}.#{lex_name}.#{comp}"
|
|
566
|
-
|
|
567
|
-
headers = build_exception_headers(event, comp, level)
|
|
568
|
-
properties = build_exception_properties(event, level)
|
|
569
|
-
|
|
570
|
-
Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
|
|
571
|
-
rescue StandardError => e
|
|
572
|
-
Legion::Logging.warn("Failed to publish exception event: #{e.class}: #{e.message}") if Legion::Logging.respond_to?(:warn)
|
|
573
|
-
end
|
|
574
|
-
|
|
575
567
|
def structured_exception_support?
|
|
576
568
|
defined?(Legion::Logging::EventBuilder) &&
|
|
577
569
|
Legion::Logging.respond_to?(:exception_writer)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -13,6 +13,7 @@ module Legion
|
|
|
13
13
|
exchanges|queues|messages|data|builders|tools|adapters|engines|
|
|
14
14
|
formatters|parsers|middleware)/
|
|
15
15
|
}x
|
|
16
|
+
LOGGING_PATH = __dir__
|
|
16
17
|
|
|
17
18
|
def trace(raw_message = nil, size: @trace_size, log_caller: true)
|
|
18
19
|
return unless @trace_enabled
|
|
@@ -25,18 +26,24 @@ module Legion
|
|
|
25
26
|
elsif log_caller
|
|
26
27
|
message.concat Rainbow(caller_locations[0..size]).cyan.underline
|
|
27
28
|
end
|
|
28
|
-
|
|
29
|
+
unknown(message)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def debug(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
|
|
32
33
|
return unless log.level < 1
|
|
33
34
|
|
|
34
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
|
+
|
|
35
42
|
raw = maybe_redact(message)
|
|
36
43
|
formatted = format_message_for_level(:debug, raw)
|
|
37
44
|
with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
|
|
38
45
|
exchange_id: exchange_id, chain_id: chain_id) do
|
|
39
|
-
write_async_or_sync(:debug, formatted, raw)
|
|
46
|
+
write_async_or_sync(:debug, formatted, raw, writer_context: build_writer_context(:debug, raw))
|
|
40
47
|
end
|
|
41
48
|
end
|
|
42
49
|
|
|
@@ -44,11 +51,17 @@ module Legion
|
|
|
44
51
|
return unless log.level < 2
|
|
45
52
|
|
|
46
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
|
+
|
|
47
60
|
raw = maybe_redact(message)
|
|
48
61
|
formatted = format_message_for_level(:info, raw)
|
|
49
62
|
with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
|
|
50
63
|
exchange_id: exchange_id, chain_id: chain_id) do
|
|
51
|
-
write_async_or_sync(:info, formatted, raw)
|
|
64
|
+
write_async_or_sync(:info, formatted, raw, writer_context: build_writer_context(:info, raw))
|
|
52
65
|
end
|
|
53
66
|
end
|
|
54
67
|
|
|
@@ -56,6 +69,12 @@ module Legion
|
|
|
56
69
|
return unless log.level < 3
|
|
57
70
|
|
|
58
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
|
+
|
|
59
78
|
raw = maybe_redact(message)
|
|
60
79
|
formatted = format_message_for_level(:warn, raw)
|
|
61
80
|
with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
|
|
@@ -68,6 +87,12 @@ module Legion
|
|
|
68
87
|
return unless log.level < 4
|
|
69
88
|
|
|
70
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
|
+
|
|
71
96
|
raw = maybe_redact(message)
|
|
72
97
|
formatted = format_message_for_level(:error, raw)
|
|
73
98
|
with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
|
|
@@ -80,6 +105,12 @@ module Legion
|
|
|
80
105
|
return unless log.level < 5
|
|
81
106
|
|
|
82
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
|
+
|
|
83
114
|
raw = maybe_redact(message)
|
|
84
115
|
formatted = format_message_for_level(:fatal, raw)
|
|
85
116
|
with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
|
|
@@ -90,11 +121,17 @@ module Legion
|
|
|
90
121
|
|
|
91
122
|
def unknown(message = nil, task_id: nil, conv_id: nil, request_id: nil, exchange_id: nil, chain_id: nil, **_ctx)
|
|
92
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
|
+
|
|
93
130
|
raw = maybe_redact(message)
|
|
94
131
|
formatted = format_message_for_level(:unknown, raw)
|
|
95
132
|
with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
|
|
96
133
|
exchange_id: exchange_id, chain_id: chain_id) do
|
|
97
|
-
write_async_or_sync(:unknown, formatted, raw)
|
|
134
|
+
write_async_or_sync(:unknown, formatted, raw, writer_context: build_writer_context(:unknown, raw))
|
|
98
135
|
end
|
|
99
136
|
end
|
|
100
137
|
|
|
@@ -103,6 +140,11 @@ module Legion
|
|
|
103
140
|
level = level.to_sym
|
|
104
141
|
message = yield if message.nil? && block_given?
|
|
105
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
|
|
106
148
|
|
|
107
149
|
raw = maybe_redact(message)
|
|
108
150
|
formatted = format_message_for_level(level, raw)
|
|
@@ -110,23 +152,21 @@ module Legion
|
|
|
110
152
|
with_tagged_context(segments, method_ctx) do
|
|
111
153
|
with_context_ids(task_id: task_id, conv_id: conv_id, request_id: request_id,
|
|
112
154
|
exchange_id: exchange_id, chain_id: chain_id) do
|
|
113
|
-
ctx =
|
|
155
|
+
ctx = build_writer_context(level, raw)
|
|
114
156
|
writer = @async_writer
|
|
115
157
|
caller_trace = capture_runner_trace_for_async
|
|
116
|
-
if writer
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
chain_id: Thread.current[:legion_log_chain_id]
|
|
126
|
-
))
|
|
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]))
|
|
127
167
|
else
|
|
128
168
|
with_caller_trace(caller_trace) { write_forced(level, formatted) }
|
|
129
|
-
fire_log_writer(level, raw) if ctx
|
|
169
|
+
fire_log_writer(level, raw, event: ctx[:event]) if ctx
|
|
130
170
|
end
|
|
131
171
|
end
|
|
132
172
|
end
|
|
@@ -141,6 +181,14 @@ module Legion
|
|
|
141
181
|
gem_name: nil, lex_version: nil, gem_path: nil,
|
|
142
182
|
source_code_uri: nil, handled: false, payload_summary: nil,
|
|
143
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
|
+
|
|
144
192
|
level = level.to_sym if level.respond_to?(:to_sym)
|
|
145
193
|
# 1. Log human-readable line + backtrace via async writer
|
|
146
194
|
msg = exception.respond_to?(:message) ? exception.message : exception.to_s
|
|
@@ -170,7 +218,7 @@ module Legion
|
|
|
170
218
|
event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
|
|
171
219
|
|
|
172
220
|
# 4. Publish rich event via exception_writer
|
|
173
|
-
|
|
221
|
+
publish_exception(event: event, level: level)
|
|
174
222
|
rescue StandardError => e
|
|
175
223
|
if respond_to?(:log) && log.respond_to?(:warn)
|
|
176
224
|
log.warn("Failed to publish structured exception event: #{e.class}: #{e.message}")
|
|
@@ -179,6 +227,45 @@ module Legion
|
|
|
179
227
|
end
|
|
180
228
|
end
|
|
181
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
|
+
|
|
182
269
|
def thread(kvl: false, **_opts)
|
|
183
270
|
if kvl
|
|
184
271
|
"thread=#{Thread.current.object_id}"
|
|
@@ -192,8 +279,9 @@ module Legion
|
|
|
192
279
|
def resolve_backtrace_limit(explicit_limit)
|
|
193
280
|
return explicit_limit unless explicit_limit.nil?
|
|
194
281
|
return nil unless defined?(Legion::Settings)
|
|
282
|
+
return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
195
283
|
|
|
196
|
-
Legion::Settings
|
|
284
|
+
Legion::Settings.dig(:logging, :backtrace_limit)
|
|
197
285
|
end
|
|
198
286
|
|
|
199
287
|
def build_exception_log_message(exception, msg, backtrace_limit)
|
|
@@ -291,30 +379,35 @@ module Legion
|
|
|
291
379
|
def write_async_or_sync(level, formatted_message, raw_message, writer_context: nil)
|
|
292
380
|
writer = @async_writer
|
|
293
381
|
caller_trace = capture_runner_trace_for_async
|
|
294
|
-
if writer
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
chain_id: Thread.current[:legion_log_chain_id]
|
|
306
|
-
))
|
|
307
|
-
return if queued
|
|
382
|
+
if @async || writer
|
|
383
|
+
return push_async_entry(entry: AsyncWriter::LogEntry.new(force: false, level: level,
|
|
384
|
+
message: formatted_message,
|
|
385
|
+
writer_context: writer_context,
|
|
386
|
+
segments: Thread.current[:legion_log_segments],
|
|
387
|
+
method_ctx: Thread.current[:legion_log_method],
|
|
388
|
+
caller_trace: caller_trace,
|
|
389
|
+
conv_id: Thread.current[:legion_log_conv_id],
|
|
390
|
+
request_id: Thread.current[:legion_log_request_id],
|
|
391
|
+
exchange_id: Thread.current[:legion_log_exchange_id],
|
|
392
|
+
chain_id: Thread.current[:legion_log_chain_id]))
|
|
308
393
|
end
|
|
309
394
|
|
|
310
395
|
with_caller_trace(caller_trace) do
|
|
311
396
|
log.public_send(level, formatted_message)
|
|
312
|
-
fire_log_writer(level, raw_message) if writer_context
|
|
397
|
+
fire_log_writer(level, raw_message, event: writer_context[:event]) if writer_context
|
|
313
398
|
end
|
|
314
399
|
end
|
|
315
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
|
+
|
|
316
408
|
def capture_runner_trace_for_async
|
|
317
|
-
|
|
409
|
+
location = Thread.current[:legion_log_origin]&.[](:location) || caller_locations(5, 1)&.first
|
|
410
|
+
build_runner_trace(location)
|
|
318
411
|
end
|
|
319
412
|
|
|
320
413
|
def with_caller_trace(caller_trace)
|
|
@@ -350,19 +443,35 @@ module Legion
|
|
|
350
443
|
}
|
|
351
444
|
end
|
|
352
445
|
|
|
353
|
-
def
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
446
|
+
def forward_level?(level)
|
|
447
|
+
forwardable_log_levels.include?(level.to_sym)
|
|
448
|
+
end
|
|
449
|
+
|
|
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
|
|
457
|
+
|
|
458
|
+
configured = Legion::Settings.dig(:logging, :transport, :log_levels)
|
|
459
|
+
return default unless configured.is_a?(Array) && !configured.empty?
|
|
460
|
+
|
|
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
|
|
360
465
|
end
|
|
361
466
|
|
|
362
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.
|
|
363
471
|
has_writer = !Legion::Logging.instance_variable_get(:@log_writer).nil?
|
|
364
472
|
has_hooks = defined?(Legion::Logging::Hooks) && Legion::Logging::Hooks.enabled?
|
|
365
473
|
return nil unless has_writer || has_hooks
|
|
474
|
+
return nil unless forward_level?(level)
|
|
366
475
|
|
|
367
476
|
lex_val = instance_variable_defined?(:@lex) ? @lex : nil
|
|
368
477
|
lex_segs = Thread.current[:legion_log_segments] || (instance_variable_defined?(:@lex_segments) ? @lex_segments : nil)
|
|
@@ -377,17 +486,7 @@ module Legion
|
|
|
377
486
|
{ level: level, event: event }
|
|
378
487
|
end
|
|
379
488
|
|
|
380
|
-
def fire_log_writer(level, message)
|
|
381
|
-
lex_val = instance_variable_defined?(:@lex) ? @lex : nil
|
|
382
|
-
lex_segs = Thread.current[:legion_log_segments] || (instance_variable_defined?(:@lex_segments) ? @lex_segments : nil)
|
|
383
|
-
|
|
384
|
-
event = Legion::Logging::EventBuilder.build(
|
|
385
|
-
level: level,
|
|
386
|
-
message: message,
|
|
387
|
-
lex: lex_val,
|
|
388
|
-
lex_segments: lex_segs,
|
|
389
|
-
caller_offset: 4
|
|
390
|
-
)
|
|
489
|
+
def fire_log_writer(level, message, event:)
|
|
391
490
|
lex_name = event[:lex] || 'core'
|
|
392
491
|
component = event.dig(:caller, :file).to_s[COMPONENT_REGEX, 1] || 'unknown'
|
|
393
492
|
routing_key = "legion.logging.log.#{level}.#{lex_name}.#{component}"
|