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,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:
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
363
|
-
return unless
|
|
371
|
+
keys = derive_component_settings_keys
|
|
372
|
+
return unless keys&.any?
|
|
364
373
|
|
|
365
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
data/lib/legion/logging/hooks.rb
CHANGED
|
@@ -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
|
-
|
|
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
|