legion-logging 1.4.2 → 1.5.0
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/.github/workflows/ci.yml +19 -1
- data/CHANGELOG.md +62 -1
- data/Gemfile +1 -0
- data/lib/legion/logging/async_writer.rb +46 -11
- data/lib/legion/logging/builder.rb +74 -34
- data/lib/legion/logging/event_builder.rb +92 -6
- data/lib/legion/logging/helper.rb +555 -26
- data/lib/legion/logging/hooks.rb +72 -0
- data/lib/legion/logging/methods.rb +121 -53
- data/lib/legion/logging/multi_io.rb +0 -1
- data/lib/legion/logging/redactor.rb +33 -2
- data/lib/legion/logging/settings.rb +16 -0
- data/lib/legion/logging/shipper/file_transport.rb +9 -1
- data/lib/legion/logging/shipper/http_transport.rb +7 -3
- data/lib/legion/logging/shipper.rb +16 -12
- data/lib/legion/logging/tagged_logger.rb +129 -0
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +56 -6
- data/lib/legion/service.rb +28 -0
- metadata +5 -1
|
@@ -1,47 +1,576 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require_relative 'tagged_logger'
|
|
5
|
+
|
|
3
6
|
module Legion
|
|
4
7
|
module Logging
|
|
5
8
|
module Helper
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
SEGMENT_CACHE = {} # rubocop:disable Style/MutableConstant
|
|
10
|
+
SEGMENT_CACHE_MUTEX = Mutex.new
|
|
11
|
+
private_constant :SEGMENT_CACHE_MUTEX
|
|
12
|
+
COMPONENT_MAP = {
|
|
13
|
+
'runners' => :runner,
|
|
14
|
+
'actors' => :actor,
|
|
15
|
+
'actor' => :actor,
|
|
16
|
+
'helpers' => :helper,
|
|
17
|
+
'hooks' => :hook,
|
|
18
|
+
'absorbers' => :absorber,
|
|
19
|
+
'matchers' => :matcher,
|
|
20
|
+
'transport' => :transport,
|
|
21
|
+
'exchanges' => :exchange,
|
|
22
|
+
'queues' => :queue,
|
|
23
|
+
'messages' => :message,
|
|
24
|
+
'data' => :data,
|
|
25
|
+
'builders' => :builder,
|
|
26
|
+
'tools' => :tool,
|
|
27
|
+
'adapters' => :adapter,
|
|
28
|
+
'engines' => :engine,
|
|
29
|
+
'formatters' => :formatter,
|
|
30
|
+
'parsers' => :parser,
|
|
31
|
+
'middleware' => :middleware
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
EXCEPTION_BACKTRACE_LIMIT = 10
|
|
35
|
+
EXCEPTION_PRIORITY = { warn: 0, error: 5, fatal: 9 }.freeze
|
|
36
|
+
EXCEPTION_COLORS = {
|
|
37
|
+
fatal: :darkred,
|
|
38
|
+
error: :red,
|
|
39
|
+
warn: :yellow,
|
|
40
|
+
debug: :aqua,
|
|
41
|
+
unknown: :magenta
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
def self.current_log_method
|
|
45
|
+
Thread.current[:legion_log_method]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.current_log_segments
|
|
49
|
+
Thread.current[:legion_log_segments]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.current_context
|
|
53
|
+
Thread.current[:legion_context]
|
|
54
|
+
end
|
|
8
55
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
56
|
+
def log
|
|
57
|
+
current_generation =
|
|
58
|
+
if defined?(Legion::Logging) && Legion::Logging.respond_to?(:configuration_generation)
|
|
59
|
+
Legion::Logging.configuration_generation
|
|
60
|
+
else
|
|
61
|
+
0
|
|
62
|
+
end
|
|
14
63
|
|
|
15
|
-
if
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
logger_hash[:log_file] = ls[:log_file] if ls.key?(:log_file)
|
|
19
|
-
logger_hash[:trace] = ls[:trace] if ls.key?(:trace)
|
|
20
|
-
logger_hash[:extended] = ls[:extended] if ls.key?(:extended)
|
|
64
|
+
if !defined?(@log) || @log.nil? || @log_generation != current_generation
|
|
65
|
+
@log = Legion::Logging::TaggedLogger.new(segments: derive_log_segments, **tagged_logger_settings)
|
|
66
|
+
@log_generation = current_generation
|
|
21
67
|
end
|
|
22
68
|
|
|
23
|
-
@log
|
|
69
|
+
@log
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def with_log_context(method_name)
|
|
73
|
+
prev = Thread.current[:legion_log_method]
|
|
74
|
+
Thread.current[:legion_log_method] = method_name.to_s
|
|
75
|
+
yield
|
|
76
|
+
ensure
|
|
77
|
+
Thread.current[:legion_log_method] = prev
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_exception(exception, task_id: nil, level: :error, handled: true, **opts)
|
|
81
|
+
segments = derive_log_segments
|
|
82
|
+
spec = gem_spec
|
|
83
|
+
ctx = Thread.current[:legion_context] || {}
|
|
84
|
+
|
|
85
|
+
event = build_exception_event(
|
|
86
|
+
exception: exception,
|
|
87
|
+
level: level,
|
|
88
|
+
spec: spec,
|
|
89
|
+
handled: handled,
|
|
90
|
+
task_id: task_id || ctx[:task_id],
|
|
91
|
+
payload_summary: opts.empty? ? nil : opts
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
event[:conversation_id] ||= ctx[:conversation_id]
|
|
95
|
+
event[:chain_id] ||= ctx[:chain_id]
|
|
96
|
+
event[:log_segments] = segments
|
|
97
|
+
event[:method] = Thread.current[:legion_log_method]
|
|
98
|
+
|
|
99
|
+
event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
|
|
100
|
+
|
|
101
|
+
write_exception_to_log(exception, event, level, segments)
|
|
102
|
+
publish_exception(event, level) if structured_exception_support?
|
|
24
103
|
end
|
|
25
104
|
|
|
26
105
|
private
|
|
27
106
|
|
|
28
|
-
def
|
|
107
|
+
def build_exception_event(exception:, level:, spec:, handled:, task_id:, payload_summary:)
|
|
108
|
+
unless structured_exception_support?
|
|
109
|
+
return fallback_exception_event(
|
|
110
|
+
exception: exception,
|
|
111
|
+
level: level,
|
|
112
|
+
spec: spec,
|
|
113
|
+
handled: handled,
|
|
114
|
+
task_id: task_id,
|
|
115
|
+
payload_summary: payload_summary
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
Legion::Logging::EventBuilder.build_exception(
|
|
120
|
+
exception: exception,
|
|
121
|
+
level: level,
|
|
122
|
+
lex: log_name,
|
|
123
|
+
component_type: derive_component_type,
|
|
124
|
+
gem_name: gem_name,
|
|
125
|
+
lex_version: spec&.version&.to_s,
|
|
126
|
+
gem_path: spec&.full_gem_path,
|
|
127
|
+
source_code_uri: spec&.metadata&.[]('source_code_uri'),
|
|
128
|
+
handled: handled,
|
|
129
|
+
task_id: task_id,
|
|
130
|
+
payload_summary: payload_summary,
|
|
131
|
+
caller_offset: 3
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def fallback_exception_event(exception:, level:, spec:, handled:, task_id:, payload_summary:)
|
|
136
|
+
{
|
|
137
|
+
exception_class: exception.class.to_s,
|
|
138
|
+
message: exception.message,
|
|
139
|
+
level: level,
|
|
140
|
+
lex: log_name,
|
|
141
|
+
component_type: derive_component_type,
|
|
142
|
+
gem_name: gem_name,
|
|
143
|
+
lex_version: spec&.version&.to_s,
|
|
144
|
+
gem_path: spec&.full_gem_path,
|
|
145
|
+
source_code_uri: spec&.metadata&.[]('source_code_uri'),
|
|
146
|
+
handled: handled,
|
|
147
|
+
task_id: task_id,
|
|
148
|
+
payload_summary: payload_summary,
|
|
149
|
+
error_fingerprint: SecureRandom.uuid
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def derive_log_segments
|
|
154
|
+
key = respond_to?(:ancestors) ? ancestors.first : self.class
|
|
155
|
+
return SEGMENT_CACHE[key] if SEGMENT_CACHE.key?(key)
|
|
156
|
+
|
|
157
|
+
segments = begin
|
|
158
|
+
parts = key.to_s.split('::')
|
|
159
|
+
parts.shift if parts.first == 'Legion'
|
|
160
|
+
parts.shift if parts.first == 'Extensions'
|
|
161
|
+
parts.map! do |p|
|
|
162
|
+
p.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
163
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
164
|
+
.downcase
|
|
165
|
+
end
|
|
166
|
+
parts.freeze
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
SEGMENT_CACHE_MUTEX.synchronize { SEGMENT_CACHE[key] ||= segments }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def derive_component_type
|
|
173
|
+
segments = derive_log_segments
|
|
174
|
+
match = segments.find { |s| COMPONENT_MAP.key?(s) }
|
|
175
|
+
return COMPONENT_MAP[match] if match
|
|
176
|
+
|
|
177
|
+
segments.last&.to_sym || :unknown
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def log_name
|
|
29
181
|
if respond_to?(:lex_filename)
|
|
30
182
|
fname = lex_filename
|
|
31
183
|
return fname.is_a?(Array) ? fname.first : fname
|
|
32
184
|
end
|
|
33
185
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
186
|
+
derive_log_segments.first
|
|
187
|
+
rescue StandardError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def gem_name
|
|
192
|
+
@gem_name_resolved ? @gem_name_value : resolve_gem_name
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def gem_spec
|
|
196
|
+
@gem_spec_resolved ? @gem_spec_value : resolve_gem_spec
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def resolve_gem_name
|
|
200
|
+
@gem_name_resolved = true
|
|
201
|
+
base = log_name
|
|
202
|
+
@gem_name_value = if base
|
|
203
|
+
%W[lex-#{base} legion-#{base} #{base}].find do |candidate|
|
|
204
|
+
Gem::Specification.find_by_name(candidate)
|
|
205
|
+
candidate
|
|
206
|
+
rescue Gem::MissingSpecError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
rescue StandardError
|
|
211
|
+
@gem_name_value = nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def resolve_gem_spec
|
|
215
|
+
@gem_spec_resolved = true
|
|
216
|
+
name = gem_name
|
|
217
|
+
@gem_spec_value = name ? Gem::Specification.find_by_name(name) : nil
|
|
218
|
+
rescue Gem::MissingSpecError
|
|
219
|
+
@gem_spec_value = nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def instance_log_level(default = Legion::Logging::Settings.default[:level] || :info)
|
|
223
|
+
component_level = component_log_level
|
|
224
|
+
return component_level if present_log_level?(component_level)
|
|
225
|
+
|
|
226
|
+
global_level = global_log_level
|
|
227
|
+
return global_level if present_log_level?(global_level)
|
|
228
|
+
|
|
229
|
+
Legion::Logging::Settings.default[:level] || default
|
|
230
|
+
rescue StandardError => e
|
|
231
|
+
Legion::Logging.warn("Legion::Logging::Helper.instance_log_level(#{default}) failed: #{e.class}: #{e.message}")
|
|
232
|
+
Legion::Logging::Settings.default[:level] || default
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def global_logger_settings
|
|
236
|
+
defaults = defined?(Legion::Logging::Settings) ? Legion::Logging::Settings.default.dup : {}
|
|
237
|
+
settings_logging = if defined?(Legion::Settings) &&
|
|
238
|
+
Legion::Settings.respond_to?(:loaded?) &&
|
|
239
|
+
Legion::Settings.loaded?
|
|
240
|
+
raw = Legion::Settings[:logging]
|
|
241
|
+
raw.is_a?(Hash) ? raw : {}
|
|
242
|
+
else
|
|
243
|
+
{}
|
|
244
|
+
end
|
|
245
|
+
runtime_logging = if defined?(Legion::Logging) &&
|
|
246
|
+
Legion::Logging.respond_to?(:current_settings)
|
|
247
|
+
current = Legion::Logging.current_settings
|
|
248
|
+
current.is_a?(Hash) ? current : {}
|
|
249
|
+
else
|
|
250
|
+
{}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
defaults.merge(settings_logging).merge(runtime_logging)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def resolve_logger_settings
|
|
257
|
+
base = global_logger_settings
|
|
258
|
+
override = component_logger_settings
|
|
259
|
+
merged = override ? base.merge(override) : base
|
|
260
|
+
merged.merge(
|
|
261
|
+
level: instance_log_level(merged[:level]),
|
|
262
|
+
trace: instance_trace(merged[:trace]),
|
|
263
|
+
trace_size: instance_trace_size(merged[:trace_size]),
|
|
264
|
+
extended: instance_extended(merged[:extended])
|
|
265
|
+
)
|
|
266
|
+
rescue StandardError
|
|
267
|
+
defined?(Legion::Logging::Settings) ? Legion::Logging::Settings.default : {}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def tagged_logger_settings
|
|
271
|
+
settings = resolve_logger_settings
|
|
272
|
+
{
|
|
273
|
+
level: settings[:level],
|
|
274
|
+
trace: settings[:trace],
|
|
275
|
+
trace_size: settings[:trace_size],
|
|
276
|
+
extended: settings[:extended]
|
|
277
|
+
}
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def component_logger_settings
|
|
281
|
+
source = component_settings
|
|
282
|
+
raw = settings_value(source, :logger)
|
|
283
|
+
raw.is_a?(Hash) ? raw : nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def component_log_level
|
|
287
|
+
source = component_settings
|
|
288
|
+
return unless source.is_a?(Hash)
|
|
289
|
+
|
|
290
|
+
settings_value(source, :log_level) ||
|
|
291
|
+
settings_value(source, :logger_level) ||
|
|
292
|
+
settings_value(source, :logger, :level)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def instance_trace(default = Legion::Logging::Settings.default[:trace])
|
|
296
|
+
component_trace = component_logger_option(:trace)
|
|
297
|
+
return component_trace unless component_trace.nil?
|
|
298
|
+
|
|
299
|
+
global_trace = global_logger_option(:trace)
|
|
300
|
+
return global_trace unless global_trace.nil?
|
|
301
|
+
|
|
302
|
+
Legion::Logging::Settings.default[:trace].nil? ? default : Legion::Logging::Settings.default[:trace]
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
Legion::Logging.warn("Legion::Logging::Helper.instance_trace(#{default}) failed: #{e.class}: #{e.message}")
|
|
305
|
+
Legion::Logging::Settings.default[:trace].nil? ? default : Legion::Logging::Settings.default[:trace]
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def instance_trace_size(default = Legion::Logging::Settings.default[:trace_size] || 4)
|
|
309
|
+
component_trace_size = component_logger_option(:trace_size)
|
|
310
|
+
return component_trace_size unless component_trace_size.nil?
|
|
311
|
+
|
|
312
|
+
global_trace_size = global_logger_option(:trace_size)
|
|
313
|
+
return global_trace_size unless global_trace_size.nil?
|
|
314
|
+
|
|
315
|
+
Legion::Logging::Settings.default[:trace_size] || default
|
|
316
|
+
rescue StandardError => e
|
|
317
|
+
Legion::Logging.warn("Legion::Logging::Helper.instance_trace_size(#{default}) failed: #{e.class}: #{e.message}")
|
|
318
|
+
Legion::Logging::Settings.default[:trace_size] || default
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def instance_extended(default = Legion::Logging::Settings.default[:extended])
|
|
322
|
+
component_extended = component_logger_option(:extended)
|
|
323
|
+
return component_extended unless component_extended.nil?
|
|
324
|
+
|
|
325
|
+
global_extended = global_logger_option(:extended)
|
|
326
|
+
return global_extended unless global_extended.nil?
|
|
327
|
+
|
|
328
|
+
Legion::Logging::Settings.default[:extended].nil? ? default : Legion::Logging::Settings.default[:extended]
|
|
329
|
+
rescue StandardError => e
|
|
330
|
+
Legion::Logging.warn("Legion::Logging::Helper.instance_extended(#{default}) failed: #{e.class}: #{e.message}")
|
|
331
|
+
Legion::Logging::Settings.default[:extended].nil? ? default : Legion::Logging::Settings.default[:extended]
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def component_settings
|
|
335
|
+
local = local_settings_hash
|
|
336
|
+
return local if local.is_a?(Hash)
|
|
337
|
+
|
|
338
|
+
legion_component_settings
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def local_settings_hash
|
|
342
|
+
return unless respond_to?(:settings, true)
|
|
343
|
+
|
|
344
|
+
source = settings
|
|
345
|
+
source if source.is_a?(Hash)
|
|
346
|
+
rescue StandardError
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def legion_component_settings
|
|
351
|
+
return unless defined?(Legion::Settings)
|
|
352
|
+
return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
353
|
+
|
|
354
|
+
key = derive_component_settings_key
|
|
355
|
+
return unless key
|
|
356
|
+
|
|
357
|
+
top_level = Legion::Settings[key]
|
|
358
|
+
return top_level if top_level.is_a?(Hash)
|
|
359
|
+
|
|
360
|
+
extension_settings = Legion::Settings.dig(:extensions, key)
|
|
361
|
+
extension_settings if extension_settings.is_a?(Hash)
|
|
362
|
+
rescue StandardError
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def derive_component_settings_key
|
|
367
|
+
base = log_name
|
|
368
|
+
return unless base
|
|
369
|
+
|
|
370
|
+
base.to_s.tr('-', '_').to_sym
|
|
371
|
+
rescue StandardError
|
|
372
|
+
nil
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def global_log_level
|
|
376
|
+
runtime_level = if defined?(Legion::Logging) &&
|
|
377
|
+
Legion::Logging.respond_to?(:current_settings)
|
|
378
|
+
settings_value(Legion::Logging.current_settings, :level)
|
|
379
|
+
end
|
|
380
|
+
return runtime_level if present_log_level?(runtime_level)
|
|
381
|
+
|
|
382
|
+
return unless defined?(Legion::Settings)
|
|
383
|
+
return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
384
|
+
|
|
385
|
+
settings_value(Legion::Settings[:logging], :level) || Legion::Settings[:level]
|
|
386
|
+
rescue StandardError
|
|
387
|
+
nil
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def component_logger_option(key)
|
|
391
|
+
source = component_settings
|
|
392
|
+
return unless source.is_a?(Hash)
|
|
393
|
+
|
|
394
|
+
return settings_value(source, key) if settings_key?(source, key)
|
|
395
|
+
return settings_value(source, :logger, key) if settings_key?(source, :logger, key)
|
|
396
|
+
|
|
397
|
+
nil
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def global_logger_option(key)
|
|
401
|
+
runtime_value = if defined?(Legion::Logging) &&
|
|
402
|
+
Legion::Logging.respond_to?(:current_settings)
|
|
403
|
+
settings_value(Legion::Logging.current_settings, key)
|
|
404
|
+
end
|
|
405
|
+
return runtime_value unless runtime_value.nil?
|
|
406
|
+
|
|
407
|
+
return unless defined?(Legion::Settings)
|
|
408
|
+
return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
409
|
+
|
|
410
|
+
settings_value(Legion::Settings[:logging], key)
|
|
411
|
+
rescue StandardError
|
|
412
|
+
nil
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def settings_value(source, *keys)
|
|
416
|
+
missing = Object.new
|
|
417
|
+
current = source
|
|
418
|
+
keys.each do |key|
|
|
419
|
+
current =
|
|
420
|
+
if current.is_a?(Hash) && current.key?(key)
|
|
421
|
+
current[key]
|
|
422
|
+
elsif current.is_a?(Hash) && current.key?(key.to_s)
|
|
423
|
+
current[key.to_s]
|
|
424
|
+
else
|
|
425
|
+
missing
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
break if current.equal?(missing)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
current.equal?(missing) ? nil : current
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def settings_key?(source, *keys)
|
|
435
|
+
current = source
|
|
436
|
+
keys.each do |key|
|
|
437
|
+
return false unless current.is_a?(Hash)
|
|
438
|
+
|
|
439
|
+
next_key = if current.key?(key)
|
|
440
|
+
key
|
|
441
|
+
elsif current.key?(key.to_s)
|
|
442
|
+
key.to_s
|
|
443
|
+
else
|
|
444
|
+
return false
|
|
445
|
+
end
|
|
446
|
+
current = current[next_key]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
true
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def present_log_level?(value)
|
|
453
|
+
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# -- Exception stdout/file output --
|
|
457
|
+
|
|
458
|
+
def write_exception_to_log(exception, event, level, segments)
|
|
459
|
+
prev_segs = Thread.current[:legion_log_segments]
|
|
460
|
+
Thread.current[:legion_log_segments] = segments
|
|
461
|
+
|
|
462
|
+
message = format_exception_output(exception, event)
|
|
463
|
+
message = Legion::Logging::Redactor.redact_string(message) if defined?(Legion::Logging::Redactor) && redaction_enabled?
|
|
464
|
+
message = colorize_exception(message, level) if Legion::Logging.respond_to?(:color) && Legion::Logging.color
|
|
465
|
+
|
|
466
|
+
logger = Legion::Logging.respond_to?(:log) ? Legion::Logging.log : nil
|
|
467
|
+
if logger.respond_to?(level)
|
|
468
|
+
logger.public_send(level, message)
|
|
469
|
+
elsif Legion::Logging.respond_to?(level)
|
|
470
|
+
Legion::Logging.public_send(level, message)
|
|
471
|
+
end
|
|
472
|
+
ensure
|
|
473
|
+
Thread.current[:legion_log_segments] = prev_segs
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def format_exception_output(exception, event)
|
|
477
|
+
lines = ["#{exception.class}: #{exception.message}"]
|
|
478
|
+
|
|
479
|
+
context_line = build_context_line(event)
|
|
480
|
+
lines << " #{context_line}" unless context_line.empty?
|
|
481
|
+
|
|
482
|
+
bt = exception.backtrace
|
|
483
|
+
if bt&.any?
|
|
484
|
+
bt.first(EXCEPTION_BACKTRACE_LIMIT).each { |frame| lines << " #{frame}" }
|
|
485
|
+
remaining = bt.length - EXCEPTION_BACKTRACE_LIMIT
|
|
486
|
+
lines << " ... #{remaining} more" if remaining.positive?
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
lines.join("\n")
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def colorize_exception(message, level)
|
|
493
|
+
color = EXCEPTION_COLORS[level] || :red
|
|
494
|
+
lines = message.split("\n")
|
|
495
|
+
lines[0] = Rainbow(lines[0]).color(color).bright
|
|
496
|
+
lines[1..].each_with_index do |line, i|
|
|
497
|
+
lines[i + 1] = Rainbow(line).color(color).faint
|
|
498
|
+
end
|
|
499
|
+
lines.join("\n")
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def build_context_line(event)
|
|
503
|
+
parts = []
|
|
504
|
+
gn = event[:gem_name]
|
|
505
|
+
gv = event[:lex_version]
|
|
506
|
+
parts << (gv ? "#{gn}@#{gv}" : gn.to_s) if gn
|
|
507
|
+
parts << "task:#{event[:task_id]}" if event[:task_id]
|
|
508
|
+
parts << "conversation:#{event[:conversation_id]}" if event[:conversation_id]
|
|
509
|
+
parts << "chain:#{event[:chain_id]}" if event[:chain_id]
|
|
510
|
+
parts.join(' | ')
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def redaction_enabled?
|
|
514
|
+
return false unless defined?(Legion::Settings)
|
|
515
|
+
|
|
516
|
+
loader = Legion::Settings.instance_variable_get(:@loader)
|
|
517
|
+
return false unless loader
|
|
518
|
+
|
|
519
|
+
loader.dig(:logging, :redaction, :enabled) == true
|
|
520
|
+
rescue StandardError
|
|
521
|
+
false
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# -- Exception structured publish --
|
|
525
|
+
|
|
526
|
+
def publish_exception(event, level)
|
|
527
|
+
return unless structured_exception_support?
|
|
528
|
+
|
|
529
|
+
lex_name = event[:lex] || 'core'
|
|
530
|
+
comp = event[:component_type] || :unknown
|
|
531
|
+
routing_key = "legion.logging.exception.#{level}.#{lex_name}.#{comp}"
|
|
532
|
+
|
|
533
|
+
headers = build_exception_headers(event, comp, level)
|
|
534
|
+
properties = build_exception_properties(event, level)
|
|
535
|
+
|
|
536
|
+
Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
|
|
537
|
+
rescue StandardError => e
|
|
538
|
+
Legion::Logging.warn("Failed to publish exception event: #{e.class}: #{e.message}") if Legion::Logging.respond_to?(:warn)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def structured_exception_support?
|
|
542
|
+
defined?(Legion::Logging::EventBuilder) &&
|
|
543
|
+
Legion::Logging.respond_to?(:exception_writer)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def build_exception_headers(event, comp, level)
|
|
547
|
+
headers = {
|
|
548
|
+
'x-error-fingerprint' => event[:error_fingerprint],
|
|
549
|
+
'x-exception-class' => event[:exception_class],
|
|
550
|
+
'x-handled' => event[:handled].to_s,
|
|
551
|
+
'x-gem-name' => event[:gem_name].to_s,
|
|
552
|
+
'x-lex-version' => event[:lex_version].to_s,
|
|
553
|
+
'x-component-type' => comp.to_s,
|
|
554
|
+
'x-level' => level.to_s
|
|
555
|
+
}
|
|
556
|
+
headers['x-task-id'] = event[:task_id].to_s if event[:task_id]
|
|
557
|
+
headers['x-conversation-id'] = event[:conversation_id].to_s if event[:conversation_id]
|
|
558
|
+
headers['x-chain-id'] = event[:chain_id].to_s if event[:chain_id]
|
|
559
|
+
headers['x-user'] = event[:user].to_s if event[:user]
|
|
560
|
+
headers
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def build_exception_properties(event, level)
|
|
564
|
+
{
|
|
565
|
+
content_type: 'application/json',
|
|
566
|
+
message_id: SecureRandom.uuid,
|
|
567
|
+
correlation_id: event[:error_fingerprint],
|
|
568
|
+
timestamp: Time.now.to_i,
|
|
569
|
+
app_id: 'legionio',
|
|
570
|
+
type: 'exception_event',
|
|
571
|
+
priority: EXCEPTION_PRIORITY[level] || 5,
|
|
572
|
+
delivery_mode: 2
|
|
573
|
+
}
|
|
45
574
|
end
|
|
46
575
|
end
|
|
47
576
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Logging
|
|
5
|
+
module Hooks
|
|
6
|
+
class << self
|
|
7
|
+
def on_fatal(&block)
|
|
8
|
+
fatal_hooks << block
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def on_error(&block)
|
|
12
|
+
error_hooks << block
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_warn(&block)
|
|
16
|
+
warn_hooks << block
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fire(level, message, event)
|
|
20
|
+
return unless @enabled
|
|
21
|
+
|
|
22
|
+
hooks_for(level).each do |hook|
|
|
23
|
+
hook.call(message, event)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
warn("Legion::Logging::Hooks#fire callback failed: #{e.message}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def enable_hooks!
|
|
30
|
+
@enabled = true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def disable_hooks!
|
|
34
|
+
@enabled = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def enabled?
|
|
38
|
+
@enabled || false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clear_hooks!
|
|
42
|
+
@fatal_hooks = []
|
|
43
|
+
@error_hooks = []
|
|
44
|
+
@warn_hooks = []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def hooks_for(level)
|
|
50
|
+
case level.to_sym
|
|
51
|
+
when :fatal then fatal_hooks
|
|
52
|
+
when :error then error_hooks
|
|
53
|
+
when :warn then warn_hooks
|
|
54
|
+
else []
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fatal_hooks
|
|
59
|
+
@fatal_hooks ||= []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def error_hooks
|
|
63
|
+
@error_hooks ||= []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def warn_hooks
|
|
67
|
+
@warn_hooks ||= []
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|