legion-logging 1.4.3 → 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/CHANGELOG.md +21 -1
- data/lib/legion/logging/async_writer.rb +35 -10
- data/lib/legion/logging/builder.rb +39 -9
- data/lib/legion/logging/event_builder.rb +73 -0
- data/lib/legion/logging/helper.rb +303 -23
- data/lib/legion/logging/methods.rb +104 -65
- data/lib/legion/logging/multi_io.rb +0 -1
- data/lib/legion/logging/redactor.rb +33 -2
- data/lib/legion/logging/settings.rb +2 -2
- data/lib/legion/logging/shipper/file_transport.rb +9 -1
- data/lib/legion/logging/shipper/http_transport.rb +4 -0
- data/lib/legion/logging/shipper.rb +16 -12
- data/lib/legion/logging/tagged_logger.rb +39 -8
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +28 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62c3c14ff8e6f67a11941a869f4f4ea2b682686c85c41bb1568368b448f23d0c
|
|
4
|
+
data.tar.gz: af9c1588f601a09f6b986d9dc47cc07b1c8e9b966e482b45d1a7242342e8d500
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66a26f844b44432886b1d703d93dbb9547134ede3542610861e123e240b8ec89746d7bdc522ae8e831c93e212f9e1a9bff5fc9b2fa19a78dfb47007373c5e0ab
|
|
7
|
+
data.tar.gz: add0a140cbe3c101dc83dcfb4b1c3eef385e4c778a4b6957be6d71b2ec95a9316c3b36d7b687906867046d482b2246ad0a44e8fd4cc98472ad1157994207146a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Legion::Logging Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-04-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Logging.current_settings` and `.configuration_generation` so helper mixins can refresh memoized tagged loggers after runtime reconfiguration
|
|
7
|
+
- Component logger overrides from local `settings`, top-level `Legion::Settings[component]`, and `Legion::Settings.dig(:extensions, component)` for `log_level`, `trace`, `trace_size`, and `extended`
|
|
8
|
+
- `Methods#emit_tagged` / `TaggedLogger#dispatch` path so component-level loggers can emit with their own level while preserving tagged context
|
|
9
|
+
- Fallback exception event construction in `Helper#handle_exception` when structured exception support is unavailable
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `setup` and `Builder#log_level` now default to `debug`
|
|
13
|
+
- Default helper/tagged logger behavior enables trace and extended metadata
|
|
14
|
+
- `Helper#log` rebuilds memoized `TaggedLogger` instances when logging configuration changes
|
|
15
|
+
- Runtime logger settings take precedence over loaded global settings for helper-mixed components
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `setup(async: true)` now tolerates boolean `logging.async` settings without probing for `buffer_size`
|
|
19
|
+
- Exception stdout/file output now falls back safely when singleton logger helpers are unavailable
|
|
20
|
+
- Structured exception publishing is skipped when the exception writer/EventBuilder path is unavailable
|
|
21
|
+
- `TaggedLogger#unknown` falls back to `debug` output when `Legion::Logging.unknown` is unavailable
|
|
22
|
+
|
|
3
23
|
## [1.4.3] - 2026-04-01
|
|
4
24
|
|
|
5
25
|
### Added
|
|
@@ -176,4 +196,4 @@
|
|
|
176
196
|
- `format_for_elk` produces ELK-compatible event hashes
|
|
177
197
|
|
|
178
198
|
## v1.2.0
|
|
179
|
-
Moving from BitBucket to GitHub. All git history is reset from this point on
|
|
199
|
+
Moving from BitBucket to GitHub. All git history is reset from this point on
|
|
@@ -5,40 +5,58 @@ require_relative 'methods'
|
|
|
5
5
|
module Legion
|
|
6
6
|
module Logging
|
|
7
7
|
class AsyncWriter
|
|
8
|
-
LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx)
|
|
8
|
+
LogEntry = ::Data.define(:level, :message, :writer_context, :segments, :method_ctx, :caller_trace)
|
|
9
9
|
SHUTDOWN = :shutdown
|
|
10
10
|
|
|
11
|
+
attr_reader :logger
|
|
12
|
+
|
|
11
13
|
def initialize(logger, buffer_size: 10_000)
|
|
12
14
|
@logger = logger
|
|
15
|
+
@buffer_size = buffer_size
|
|
13
16
|
@queue = SizedQueue.new(buffer_size)
|
|
14
17
|
@thread = nil
|
|
18
|
+
@state_mutex = Mutex.new
|
|
19
|
+
@accepting = true
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
def start
|
|
18
23
|
return if @thread&.alive?
|
|
19
24
|
|
|
25
|
+
@state_mutex.synchronize { @accepting = true }
|
|
20
26
|
drain
|
|
27
|
+
@queue = SizedQueue.new(@buffer_size)
|
|
21
28
|
@thread = Thread.new { consume }
|
|
22
29
|
@thread.name = 'legion-log-writer'
|
|
23
30
|
@thread.abort_on_exception = false
|
|
24
31
|
end
|
|
25
32
|
|
|
33
|
+
# rubocop:disable Naming/PredicateMethod
|
|
26
34
|
def stop(timeout: 2)
|
|
27
|
-
|
|
35
|
+
@state_mutex.synchronize { @accepting = false }
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
unless @thread&.alive?
|
|
38
|
+
drain
|
|
39
|
+
@thread = nil
|
|
40
|
+
return true
|
|
33
41
|
end
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
|
|
42
|
+
|
|
43
|
+
@queue.close
|
|
44
|
+
timeout ? @thread.join(timeout) : @thread.join
|
|
45
|
+
return false if @thread&.alive?
|
|
46
|
+
|
|
47
|
+
@thread = nil
|
|
48
|
+
true
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
def push(entry)
|
|
52
|
+
return false unless accepting?
|
|
53
|
+
|
|
40
54
|
@queue.push(entry)
|
|
55
|
+
true
|
|
56
|
+
rescue ClosedQueueError
|
|
57
|
+
false
|
|
41
58
|
end
|
|
59
|
+
# rubocop:enable Naming/PredicateMethod
|
|
42
60
|
|
|
43
61
|
def alive?
|
|
44
62
|
@thread&.alive? || false
|
|
@@ -49,7 +67,7 @@ module Legion
|
|
|
49
67
|
def consume
|
|
50
68
|
loop do
|
|
51
69
|
entry = @queue.pop
|
|
52
|
-
break if entry == SHUTDOWN
|
|
70
|
+
break if entry.nil? || entry == SHUTDOWN
|
|
53
71
|
|
|
54
72
|
write_entry(entry)
|
|
55
73
|
end
|
|
@@ -58,8 +76,10 @@ module Legion
|
|
|
58
76
|
def write_entry(entry)
|
|
59
77
|
prev_segments = Thread.current[:legion_log_segments]
|
|
60
78
|
prev_method_ctx = Thread.current[:legion_log_method]
|
|
79
|
+
prev_caller = Thread.current[:legion_log_caller]
|
|
61
80
|
Thread.current[:legion_log_segments] = entry.segments
|
|
62
81
|
Thread.current[:legion_log_method] = entry.method_ctx
|
|
82
|
+
Thread.current[:legion_log_caller] = entry.caller_trace
|
|
63
83
|
@logger.send(entry.level, entry.message)
|
|
64
84
|
fire_writer(entry) if entry.writer_context
|
|
65
85
|
rescue StandardError => e
|
|
@@ -67,6 +87,7 @@ module Legion
|
|
|
67
87
|
ensure
|
|
68
88
|
Thread.current[:legion_log_segments] = prev_segments
|
|
69
89
|
Thread.current[:legion_log_method] = prev_method_ctx
|
|
90
|
+
Thread.current[:legion_log_caller] = prev_caller
|
|
70
91
|
end
|
|
71
92
|
|
|
72
93
|
def drain
|
|
@@ -78,6 +99,10 @@ module Legion
|
|
|
78
99
|
nil
|
|
79
100
|
end
|
|
80
101
|
|
|
102
|
+
def accepting?
|
|
103
|
+
@state_mutex.synchronize { @accepting }
|
|
104
|
+
end
|
|
105
|
+
|
|
81
106
|
def fire_writer(entry)
|
|
82
107
|
ctx = entry.writer_context
|
|
83
108
|
event = ctx[:event]
|
|
@@ -41,7 +41,7 @@ module Legion
|
|
|
41
41
|
def text_format(include_pid: false, **options)
|
|
42
42
|
log.formatter = proc do |severity, datetime, _progname, msg|
|
|
43
43
|
lex_name = resolve_lex_tag(options)
|
|
44
|
-
runner_trace = build_runner_trace if lex_name
|
|
44
|
+
runner_trace = Thread.current[:legion_log_caller] || build_runner_trace if lex_name
|
|
45
45
|
|
|
46
46
|
string = "[#{datetime}]"
|
|
47
47
|
string.concat("[#{::Process.pid}]") if include_pid
|
|
@@ -69,8 +69,7 @@ module Legion
|
|
|
69
69
|
tag
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def build_runner_trace
|
|
73
|
-
loc = caller_locations(6, 1)&.first
|
|
72
|
+
def build_runner_trace(loc = caller_locations(6, 1)&.first)
|
|
74
73
|
return unless loc
|
|
75
74
|
|
|
76
75
|
path = loc.to_s.split('/').last(2)
|
|
@@ -91,16 +90,25 @@ module Legion
|
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
def set_log(logfile: nil, log_stdout: nil, **)
|
|
93
|
+
previous_log = @log
|
|
94
|
+
|
|
94
95
|
if logfile && log_stdout != false
|
|
95
96
|
path = prepare_log_path(logfile)
|
|
96
97
|
require_relative 'multi_io'
|
|
97
|
-
|
|
98
|
+
file = File.new(path, 'a')
|
|
99
|
+
file.sync = true
|
|
100
|
+
io = MultiIO.new($stdout, file)
|
|
98
101
|
@log = ::Logger.new(io)
|
|
99
102
|
elsif logfile
|
|
100
|
-
|
|
103
|
+
file = File.new(prepare_log_path(logfile), 'a')
|
|
104
|
+
file.sync = true
|
|
105
|
+
@log = ::Logger.new(file)
|
|
101
106
|
else
|
|
102
107
|
@log = ::Logger.new($stdout)
|
|
103
108
|
end
|
|
109
|
+
|
|
110
|
+
close_replaced_log(previous_log)
|
|
111
|
+
@log
|
|
104
112
|
end
|
|
105
113
|
|
|
106
114
|
def prepare_log_path(path)
|
|
@@ -113,7 +121,7 @@ module Legion
|
|
|
113
121
|
log.level
|
|
114
122
|
end
|
|
115
123
|
|
|
116
|
-
def log_level(level = '
|
|
124
|
+
def log_level(level = 'debug')
|
|
117
125
|
log.level = case level
|
|
118
126
|
when 'trace', 'debug'
|
|
119
127
|
::Logger::DEBUG
|
|
@@ -140,19 +148,41 @@ module Legion
|
|
|
140
148
|
(@async == true && @async_writer&.alive?) || false
|
|
141
149
|
end
|
|
142
150
|
|
|
151
|
+
# rubocop:disable Naming/PredicateMethod
|
|
143
152
|
def start_async_writer(buffer_size: 10_000)
|
|
144
153
|
require_relative 'async_writer'
|
|
145
|
-
|
|
154
|
+
return false if @async_writer&.alive? && stop_async_writer == false
|
|
155
|
+
|
|
146
156
|
@async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
|
|
147
157
|
@async_writer.start
|
|
148
158
|
@async = true
|
|
159
|
+
true
|
|
149
160
|
end
|
|
150
161
|
|
|
151
162
|
def stop_async_writer
|
|
152
163
|
writer = @async_writer
|
|
153
|
-
|
|
164
|
+
stopped = writer&.stop
|
|
165
|
+
return false if stopped == false
|
|
166
|
+
|
|
167
|
+
close_replaced_log(writer.logger) if writer.respond_to?(:logger)
|
|
168
|
+
@async_writer = nil if @async_writer.equal?(writer)
|
|
154
169
|
@async = false
|
|
155
|
-
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
# rubocop:enable Naming/PredicateMethod
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def close_replaced_log(logger)
|
|
177
|
+
return unless logger
|
|
178
|
+
return if logger.equal?(@log)
|
|
179
|
+
return if @async_writer&.alive? && @async_writer.respond_to?(:logger) && @async_writer.logger.equal?(logger)
|
|
180
|
+
|
|
181
|
+
log_device = logger.instance_variable_get(:@logdev)
|
|
182
|
+
dev = log_device&.dev
|
|
183
|
+
return if dev.nil? || [$stdout, $stderr].include?(dev)
|
|
184
|
+
|
|
185
|
+
dev.close if dev.respond_to?(:close)
|
|
156
186
|
end
|
|
157
187
|
end
|
|
158
188
|
end
|
|
@@ -11,6 +11,29 @@ module Legion
|
|
|
11
11
|
MAX_PAYLOAD_BYTES = 8192
|
|
12
12
|
MAX_TOTAL_BYTES = 65_536
|
|
13
13
|
BACKTRACE_FALLBACK_FRAMES = 20
|
|
14
|
+
MIN_TRUNCATED_FIELD_BYTES = 256
|
|
15
|
+
|
|
16
|
+
CORE_EXCEPTION_FIELDS = %i[
|
|
17
|
+
timestamp
|
|
18
|
+
level
|
|
19
|
+
exception_class
|
|
20
|
+
message
|
|
21
|
+
caller_file
|
|
22
|
+
caller_line
|
|
23
|
+
caller_function
|
|
24
|
+
lex
|
|
25
|
+
component_type
|
|
26
|
+
gem_name
|
|
27
|
+
lex_version
|
|
28
|
+
handled
|
|
29
|
+
pid
|
|
30
|
+
thread
|
|
31
|
+
task_id
|
|
32
|
+
conversation_id
|
|
33
|
+
user
|
|
34
|
+
error_fingerprint
|
|
35
|
+
node
|
|
36
|
+
].freeze
|
|
14
37
|
|
|
15
38
|
GEM_SPEC_CACHE_MUTEX = Mutex.new
|
|
16
39
|
private_constant :GEM_SPEC_CACHE_MUTEX
|
|
@@ -310,6 +333,56 @@ module Legion
|
|
|
310
333
|
return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
|
|
311
334
|
|
|
312
335
|
event[:message] = truncate_bytes(event[:message].to_s, 1024)
|
|
336
|
+
trim_optional_fields!(event)
|
|
337
|
+
hard_cap_message!(event)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def trim_optional_fields!(event)
|
|
341
|
+
while safe_json_bytesize(event) > MAX_TOTAL_BYTES
|
|
342
|
+
key = largest_optional_field(event)
|
|
343
|
+
break unless key
|
|
344
|
+
|
|
345
|
+
reduced = reduce_field(event[key])
|
|
346
|
+
if reduced.nil?
|
|
347
|
+
event.delete(key)
|
|
348
|
+
else
|
|
349
|
+
event[key] = reduced
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def largest_optional_field(event)
|
|
355
|
+
event.each_key
|
|
356
|
+
.reject { |key| CORE_EXCEPTION_FIELDS.include?(key) }
|
|
357
|
+
.max_by { |key| safe_json_bytesize(event[key]) }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def reduce_field(value)
|
|
361
|
+
case value
|
|
362
|
+
when String
|
|
363
|
+
return nil if value.bytesize <= MIN_TRUNCATED_FIELD_BYTES
|
|
364
|
+
|
|
365
|
+
truncate_bytes(value, [value.bytesize / 2, MIN_TRUNCATED_FIELD_BYTES].max)
|
|
366
|
+
when Array
|
|
367
|
+
return nil if value.size <= 1
|
|
368
|
+
|
|
369
|
+
value.first([value.size / 2, 1].max)
|
|
370
|
+
when Hash
|
|
371
|
+
return nil if value.size <= 1
|
|
372
|
+
|
|
373
|
+
value.first([value.size / 2, 1].max).to_h
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def hard_cap_message!(event)
|
|
378
|
+
return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
|
|
379
|
+
|
|
380
|
+
event[:message] = truncate_bytes(event[:message].to_s, MIN_TRUNCATED_FIELD_BYTES)
|
|
381
|
+
return if safe_json_bytesize(event) <= MAX_TOTAL_BYTES
|
|
382
|
+
|
|
383
|
+
message_overhead = safe_json_bytesize(event.merge(message: ''))
|
|
384
|
+
available = MAX_TOTAL_BYTES - message_overhead
|
|
385
|
+
event[:message] = truncate_bytes(event[:message].to_s, [available, 0].max)
|
|
313
386
|
end
|
|
314
387
|
end
|
|
315
388
|
end
|
|
@@ -54,7 +54,19 @@ module Legion
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def log
|
|
57
|
-
|
|
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
|
|
63
|
+
|
|
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
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@log
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
def with_log_context(method_name)
|
|
@@ -70,19 +82,13 @@ module Legion
|
|
|
70
82
|
spec = gem_spec
|
|
71
83
|
ctx = Thread.current[:legion_context] || {}
|
|
72
84
|
|
|
73
|
-
event =
|
|
85
|
+
event = build_exception_event(
|
|
74
86
|
exception: exception,
|
|
75
87
|
level: level,
|
|
76
|
-
|
|
77
|
-
component_type: derive_component_type,
|
|
78
|
-
gem_name: gem_name,
|
|
79
|
-
lex_version: spec&.version&.to_s,
|
|
80
|
-
gem_path: spec&.full_gem_path,
|
|
81
|
-
source_code_uri: spec&.metadata&.[]('source_code_uri'),
|
|
88
|
+
spec: spec,
|
|
82
89
|
handled: handled,
|
|
83
90
|
task_id: task_id || ctx[:task_id],
|
|
84
|
-
payload_summary: opts.empty? ? nil : opts
|
|
85
|
-
caller_offset: 3
|
|
91
|
+
payload_summary: opts.empty? ? nil : opts
|
|
86
92
|
)
|
|
87
93
|
|
|
88
94
|
event[:conversation_id] ||= ctx[:conversation_id]
|
|
@@ -93,11 +99,57 @@ module Legion
|
|
|
93
99
|
event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
|
|
94
100
|
|
|
95
101
|
write_exception_to_log(exception, event, level, segments)
|
|
96
|
-
publish_exception(event, level)
|
|
102
|
+
publish_exception(event, level) if structured_exception_support?
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
private
|
|
100
106
|
|
|
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
|
+
|
|
101
153
|
def derive_log_segments
|
|
102
154
|
key = respond_to?(:ancestors) ? ancestors.first : self.class
|
|
103
155
|
return SEGMENT_CACHE[key] if SEGMENT_CACHE.key?(key)
|
|
@@ -167,22 +219,238 @@ module Legion
|
|
|
167
219
|
@gem_spec_value = nil
|
|
168
220
|
end
|
|
169
221
|
|
|
170
|
-
def
|
|
171
|
-
|
|
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
|
|
172
233
|
end
|
|
173
234
|
|
|
174
|
-
def
|
|
175
|
-
|
|
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
|
|
176
252
|
|
|
177
|
-
|
|
253
|
+
defaults.merge(settings_logging).merge(runtime_logging)
|
|
178
254
|
end
|
|
179
255
|
|
|
180
256
|
def resolve_logger_settings
|
|
181
|
-
|
|
182
|
-
|
|
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?
|
|
183
314
|
|
|
184
|
-
|
|
185
|
-
|
|
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?)
|
|
186
454
|
end
|
|
187
455
|
|
|
188
456
|
# -- Exception stdout/file output --
|
|
@@ -193,9 +461,14 @@ module Legion
|
|
|
193
461
|
|
|
194
462
|
message = format_exception_output(exception, event)
|
|
195
463
|
message = Legion::Logging::Redactor.redact_string(message) if defined?(Legion::Logging::Redactor) && redaction_enabled?
|
|
196
|
-
message = colorize_exception(message, level) if Legion::Logging.color
|
|
464
|
+
message = colorize_exception(message, level) if Legion::Logging.respond_to?(:color) && Legion::Logging.color
|
|
197
465
|
|
|
198
|
-
Legion::Logging.log.
|
|
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
|
|
199
472
|
ensure
|
|
200
473
|
Thread.current[:legion_log_segments] = prev_segs
|
|
201
474
|
end
|
|
@@ -251,6 +524,8 @@ module Legion
|
|
|
251
524
|
# -- Exception structured publish --
|
|
252
525
|
|
|
253
526
|
def publish_exception(event, level)
|
|
527
|
+
return unless structured_exception_support?
|
|
528
|
+
|
|
254
529
|
lex_name = event[:lex] || 'core'
|
|
255
530
|
comp = event[:component_type] || :unknown
|
|
256
531
|
routing_key = "legion.logging.exception.#{level}.#{lex_name}.#{comp}"
|
|
@@ -260,7 +535,12 @@ module Legion
|
|
|
260
535
|
|
|
261
536
|
Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
|
|
262
537
|
rescue StandardError => e
|
|
263
|
-
Legion::Logging.warn("Failed to publish exception event: #{e.class}: #{e.message}")
|
|
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)
|
|
264
544
|
end
|
|
265
545
|
|
|
266
546
|
def build_exception_headers(event, comp, level)
|
|
@@ -30,78 +30,36 @@ module Legion
|
|
|
30
30
|
return unless log.level < 1
|
|
31
31
|
|
|
32
32
|
message = yield if message.nil? && block_given?
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if writer&.alive?
|
|
37
|
-
writer.push(AsyncWriter::LogEntry.new(
|
|
38
|
-
level: :debug, message: message, writer_context: nil,
|
|
39
|
-
segments: Thread.current[:legion_log_segments],
|
|
40
|
-
method_ctx: Thread.current[:legion_log_method]
|
|
41
|
-
))
|
|
42
|
-
else
|
|
43
|
-
log.debug(message)
|
|
44
|
-
end
|
|
33
|
+
raw = maybe_redact(message)
|
|
34
|
+
formatted = format_message_for_level(:debug, raw)
|
|
35
|
+
write_async_or_sync(:debug, formatted, raw)
|
|
45
36
|
end
|
|
46
37
|
|
|
47
38
|
def info(message = nil)
|
|
48
39
|
return unless log.level < 2
|
|
49
40
|
|
|
50
41
|
message = yield if message.nil? && block_given?
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if writer&.alive?
|
|
55
|
-
writer.push(AsyncWriter::LogEntry.new(
|
|
56
|
-
level: :info, message: message, writer_context: nil,
|
|
57
|
-
segments: Thread.current[:legion_log_segments],
|
|
58
|
-
method_ctx: Thread.current[:legion_log_method]
|
|
59
|
-
))
|
|
60
|
-
else
|
|
61
|
-
log.info(message)
|
|
62
|
-
end
|
|
42
|
+
raw = maybe_redact(message)
|
|
43
|
+
formatted = format_message_for_level(:info, raw)
|
|
44
|
+
write_async_or_sync(:info, formatted, raw)
|
|
63
45
|
end
|
|
64
46
|
|
|
65
47
|
def warn(message = nil)
|
|
66
48
|
return unless log.level < 3
|
|
67
49
|
|
|
68
50
|
message = yield if message.nil? && block_given?
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
writer = @async_writer
|
|
73
|
-
if writer&.alive?
|
|
74
|
-
ctx = build_writer_context(:warn, raw)
|
|
75
|
-
writer.push(AsyncWriter::LogEntry.new(
|
|
76
|
-
level: :warn, message: message, writer_context: ctx,
|
|
77
|
-
segments: Thread.current[:legion_log_segments],
|
|
78
|
-
method_ctx: Thread.current[:legion_log_method]
|
|
79
|
-
))
|
|
80
|
-
else
|
|
81
|
-
log.warn(message)
|
|
82
|
-
fire_log_writer(:warn, raw)
|
|
83
|
-
end
|
|
51
|
+
raw = maybe_redact(message)
|
|
52
|
+
formatted = format_message_for_level(:warn, raw)
|
|
53
|
+
write_async_or_sync(:warn, formatted, raw, writer_context: build_writer_context(:warn, raw))
|
|
84
54
|
end
|
|
85
55
|
|
|
86
56
|
def error(message = nil)
|
|
87
57
|
return unless log.level < 4
|
|
88
58
|
|
|
89
59
|
message = yield if message.nil? && block_given?
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
writer = @async_writer
|
|
94
|
-
if writer&.alive?
|
|
95
|
-
ctx = build_writer_context(:error, raw)
|
|
96
|
-
writer.push(AsyncWriter::LogEntry.new(
|
|
97
|
-
level: :error, message: message, writer_context: ctx,
|
|
98
|
-
segments: Thread.current[:legion_log_segments],
|
|
99
|
-
method_ctx: Thread.current[:legion_log_method]
|
|
100
|
-
))
|
|
101
|
-
else
|
|
102
|
-
log.error(message)
|
|
103
|
-
fire_log_writer(:error, raw)
|
|
104
|
-
end
|
|
60
|
+
raw = maybe_redact(message)
|
|
61
|
+
formatted = format_message_for_level(:error, raw)
|
|
62
|
+
write_async_or_sync(:error, formatted, raw, writer_context: build_writer_context(:error, raw))
|
|
105
63
|
end
|
|
106
64
|
|
|
107
65
|
def fatal(message = nil)
|
|
@@ -117,17 +75,22 @@ module Legion
|
|
|
117
75
|
|
|
118
76
|
def unknown(message = nil)
|
|
119
77
|
message = yield if message.nil? && block_given?
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
78
|
+
raw = maybe_redact(message)
|
|
79
|
+
formatted = format_message_for_level(:unknown, raw)
|
|
80
|
+
write_async_or_sync(:unknown, formatted, raw)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def emit_tagged(level, message = nil, segments: nil, method_ctx: nil)
|
|
84
|
+
level = level.to_sym
|
|
85
|
+
message = yield if message.nil? && block_given?
|
|
86
|
+
return if message.nil?
|
|
87
|
+
|
|
88
|
+
raw = maybe_redact(message)
|
|
89
|
+
formatted = format_message_for_level(level, raw)
|
|
90
|
+
|
|
91
|
+
with_tagged_context(segments, method_ctx) do
|
|
92
|
+
write_forced(level, formatted)
|
|
93
|
+
fire_log_writer(level, raw) if %i[warn error fatal].include?(level)
|
|
131
94
|
end
|
|
132
95
|
end
|
|
133
96
|
|
|
@@ -143,6 +106,7 @@ module Legion
|
|
|
143
106
|
level = level.to_sym if level.respond_to?(:to_sym)
|
|
144
107
|
# 1. Log human-readable line to stdout/file (bypass writer callbacks)
|
|
145
108
|
msg = exception.respond_to?(:message) ? exception.message : exception.to_s
|
|
109
|
+
msg = maybe_redact(msg)
|
|
146
110
|
log.public_send(level, msg) if respond_to?(:log) && log.respond_to?(level)
|
|
147
111
|
|
|
148
112
|
# 2. Build rich exception event
|
|
@@ -195,6 +159,81 @@ module Legion
|
|
|
195
159
|
message
|
|
196
160
|
end
|
|
197
161
|
|
|
162
|
+
def format_message_for_level(level, message)
|
|
163
|
+
return Rainbow(message).blue if level == :debug && @color
|
|
164
|
+
return Rainbow(message).green if level == :info && @color
|
|
165
|
+
return Rainbow(message).yellow if level == :warn && @color
|
|
166
|
+
return Rainbow(message).red if level == :error && @color
|
|
167
|
+
return Rainbow(message).darkred if level == :fatal && @color
|
|
168
|
+
return Rainbow(message).purple if level == :unknown && @color
|
|
169
|
+
|
|
170
|
+
message
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def with_tagged_context(segments, method_ctx)
|
|
174
|
+
prev_segments = Thread.current[:legion_log_segments]
|
|
175
|
+
prev_method_ctx = Thread.current[:legion_log_method]
|
|
176
|
+
|
|
177
|
+
Thread.current[:legion_log_segments] = segments unless segments.nil?
|
|
178
|
+
Thread.current[:legion_log_method] = method_ctx unless method_ctx.nil?
|
|
179
|
+
yield
|
|
180
|
+
ensure
|
|
181
|
+
Thread.current[:legion_log_segments] = prev_segments
|
|
182
|
+
Thread.current[:legion_log_method] = prev_method_ctx
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def write_forced(level, message)
|
|
186
|
+
logger = log
|
|
187
|
+
formatter = logger.formatter || ::Logger::Formatter.new
|
|
188
|
+
rendered = formatter.call(severity_label_for(level), Time.now, nil, message)
|
|
189
|
+
|
|
190
|
+
log_device = logger.instance_variable_get(:@logdev)
|
|
191
|
+
if log_device.respond_to?(:write)
|
|
192
|
+
log_device.write(rendered)
|
|
193
|
+
else
|
|
194
|
+
$stdout.write(rendered)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def severity_label_for(level)
|
|
199
|
+
return 'ANY' if level == :unknown
|
|
200
|
+
|
|
201
|
+
level.to_s.upcase
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def write_async_or_sync(level, formatted_message, raw_message, writer_context: nil)
|
|
205
|
+
writer = @async_writer
|
|
206
|
+
caller_trace = capture_runner_trace_for_async
|
|
207
|
+
if writer&.alive?
|
|
208
|
+
queued = writer.push(AsyncWriter::LogEntry.new(
|
|
209
|
+
level: level,
|
|
210
|
+
message: formatted_message,
|
|
211
|
+
writer_context: writer_context,
|
|
212
|
+
segments: Thread.current[:legion_log_segments],
|
|
213
|
+
method_ctx: Thread.current[:legion_log_method],
|
|
214
|
+
caller_trace: caller_trace
|
|
215
|
+
))
|
|
216
|
+
return if queued
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
with_caller_trace(caller_trace) do
|
|
220
|
+
log.public_send(level, formatted_message)
|
|
221
|
+
fire_log_writer(level, raw_message) if writer_context
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def capture_runner_trace_for_async
|
|
226
|
+
build_runner_trace(caller_locations(4, 1)&.first)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def with_caller_trace(caller_trace)
|
|
230
|
+
prev_caller_trace = Thread.current[:legion_log_caller]
|
|
231
|
+
Thread.current[:legion_log_caller] = caller_trace
|
|
232
|
+
yield
|
|
233
|
+
ensure
|
|
234
|
+
Thread.current[:legion_log_caller] = prev_caller_trace
|
|
235
|
+
end
|
|
236
|
+
|
|
198
237
|
def redaction_enabled?
|
|
199
238
|
return false unless defined?(Legion::Settings)
|
|
200
239
|
|
|
@@ -18,7 +18,18 @@ module Legion
|
|
|
18
18
|
bearer_token: %r{Bearer\s+[A-Za-z0-9._~+/=-]{20,}}i
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
-
SENSITIVE_FIELDS = %w[
|
|
21
|
+
SENSITIVE_FIELDS = %w[
|
|
22
|
+
password
|
|
23
|
+
secret
|
|
24
|
+
token
|
|
25
|
+
api_key
|
|
26
|
+
access_key
|
|
27
|
+
private_key
|
|
28
|
+
public_key
|
|
29
|
+
authorization
|
|
30
|
+
].freeze
|
|
31
|
+
SENSITIVE_SUFFIXES = %w[token secret password passphrase credential credentials].freeze
|
|
32
|
+
SAFE_KEY_FIELDS = %w[primary_key foreign_key sort_key partition_key routing_key].freeze
|
|
22
33
|
|
|
23
34
|
REDACTED = '[REDACTED]'
|
|
24
35
|
|
|
@@ -49,7 +60,17 @@ module Legion
|
|
|
49
60
|
private
|
|
50
61
|
|
|
51
62
|
def sensitive_field?(key)
|
|
52
|
-
|
|
63
|
+
normalized = normalize_key(key)
|
|
64
|
+
return false if SAFE_KEY_FIELDS.include?(normalized)
|
|
65
|
+
return true if SENSITIVE_FIELDS.include?(normalized)
|
|
66
|
+
return true if normalized.include?('authorization')
|
|
67
|
+
return true if normalized.start_with?('auth_') || normalized.end_with?('_auth')
|
|
68
|
+
return true if normalized.start_with?('bearer_') || normalized.end_with?('_bearer')
|
|
69
|
+
return true if SENSITIVE_SUFFIXES.any? { |suffix| normalized.end_with?("_#{suffix}") }
|
|
70
|
+
|
|
71
|
+
%w[api access client private public auth secret signing session].any? do |prefix|
|
|
72
|
+
normalized == "#{prefix}_key"
|
|
73
|
+
end
|
|
53
74
|
end
|
|
54
75
|
|
|
55
76
|
def all_patterns
|
|
@@ -79,6 +100,16 @@ module Legion
|
|
|
79
100
|
def reset_pattern_cache!
|
|
80
101
|
@all_patterns = nil
|
|
81
102
|
end
|
|
103
|
+
|
|
104
|
+
def refresh_patterns!
|
|
105
|
+
reset_pattern_cache!
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
public :refresh_patterns!
|
|
109
|
+
|
|
110
|
+
def normalize_key(key)
|
|
111
|
+
key.to_s.downcase.gsub(/[^a-z0-9]+/, '_').gsub(/\A_+|_+\z/, '')
|
|
112
|
+
end
|
|
82
113
|
end
|
|
83
114
|
end
|
|
84
115
|
end
|
|
@@ -11,10 +11,18 @@ module Legion
|
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def ship(event)
|
|
14
|
+
ship_batch([event])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ship_batch(events)
|
|
18
|
+
batch = Array(events)
|
|
19
|
+
return true if batch.empty?
|
|
20
|
+
|
|
14
21
|
path = resolve_path
|
|
15
22
|
FileUtils.mkdir_p(File.dirname(path))
|
|
16
23
|
File.open(path, 'a') do |f|
|
|
17
|
-
f.
|
|
24
|
+
f.write(batch.map { |event| ::JSON.generate(event) }.join("\n"))
|
|
25
|
+
f.write("\n")
|
|
18
26
|
end
|
|
19
27
|
true
|
|
20
28
|
rescue StandardError => e
|
|
@@ -30,21 +30,25 @@ module Legion
|
|
|
30
30
|
transport = TRANSPORTS[transport_type]
|
|
31
31
|
return unless transport
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
@
|
|
35
|
-
batch =
|
|
36
|
-
@buffer.
|
|
33
|
+
@flush_mutex ||= Mutex.new
|
|
34
|
+
@flush_mutex.synchronize do
|
|
35
|
+
batch = nil
|
|
36
|
+
@mutex.synchronize { batch = @buffer.dup }
|
|
37
|
+
return if batch.empty?
|
|
38
|
+
|
|
39
|
+
delivered = deliver(transport, batch)
|
|
40
|
+
@mutex.synchronize { @buffer.shift(batch.size) if delivered }
|
|
41
|
+
delivered
|
|
37
42
|
end
|
|
38
|
-
|
|
39
|
-
deliver(transport, batch)
|
|
40
43
|
end
|
|
41
44
|
|
|
42
45
|
def start
|
|
43
46
|
return unless enabled?
|
|
44
47
|
return if @flush_thread&.alive?
|
|
45
48
|
|
|
46
|
-
@buffer
|
|
47
|
-
@mutex
|
|
49
|
+
@buffer ||= []
|
|
50
|
+
@mutex ||= Mutex.new
|
|
51
|
+
@flush_mutex ||= Mutex.new
|
|
48
52
|
interval = flush_interval
|
|
49
53
|
@flush_thread = Thread.new do
|
|
50
54
|
loop do
|
|
@@ -83,14 +87,14 @@ module Legion
|
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
def deliver(transport, batch)
|
|
86
|
-
if transport.
|
|
87
|
-
|
|
88
|
-
transport.ship(batch)
|
|
90
|
+
if transport.respond_to?(:ship_batch)
|
|
91
|
+
transport.ship_batch(batch)
|
|
89
92
|
else
|
|
90
|
-
batch.
|
|
93
|
+
batch.all? { |event| transport.ship(event) }
|
|
91
94
|
end
|
|
92
95
|
rescue StandardError => e
|
|
93
96
|
Legion::Logging.error("Shipper deliver failed: #{e.message}") if defined?(Legion::Logging)
|
|
97
|
+
false
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
def shippable_level?(level)
|
|
@@ -7,13 +7,21 @@ module Legion
|
|
|
7
7
|
|
|
8
8
|
attr_reader :segments, :trace_enabled, :extended
|
|
9
9
|
|
|
10
|
-
def initialize(
|
|
10
|
+
def initialize(
|
|
11
|
+
segments:,
|
|
12
|
+
level: Legion::Logging::Settings.default[:level],
|
|
13
|
+
trace: Legion::Logging::Settings.default[:trace],
|
|
14
|
+
trace_size: Legion::Logging::Settings.default[:trace_size],
|
|
15
|
+
extended: Legion::Logging::Settings.default[:extended],
|
|
16
|
+
**_opts
|
|
17
|
+
)
|
|
11
18
|
@segments = segments
|
|
12
19
|
@level_value =
|
|
13
20
|
if level.is_a?(Integer)
|
|
14
21
|
level
|
|
15
22
|
else
|
|
16
|
-
|
|
23
|
+
default_level = Legion::Logging::Settings.default[:level].to_s.downcase.to_sym
|
|
24
|
+
LEVELS.fetch(level.to_s.downcase.to_sym, LEVELS.fetch(default_level, LEVELS[:info]))
|
|
17
25
|
end
|
|
18
26
|
@trace_enabled = trace
|
|
19
27
|
@trace_size = trace_size
|
|
@@ -28,40 +36,40 @@ module Legion
|
|
|
28
36
|
return unless @level_value < 1
|
|
29
37
|
|
|
30
38
|
message = yield if message.nil? && block_given?
|
|
31
|
-
with_segments {
|
|
39
|
+
with_segments { dispatch(:debug, message) }
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
def info(message = nil)
|
|
35
43
|
return unless @level_value < 2
|
|
36
44
|
|
|
37
45
|
message = yield if message.nil? && block_given?
|
|
38
|
-
with_segments {
|
|
46
|
+
with_segments { dispatch(:info, message) }
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def warn(message = nil)
|
|
42
50
|
return unless @level_value < 3
|
|
43
51
|
|
|
44
52
|
message = yield if message.nil? && block_given?
|
|
45
|
-
with_segments {
|
|
53
|
+
with_segments { dispatch(:warn, message) }
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
def error(message = nil)
|
|
49
57
|
return unless @level_value < 4
|
|
50
58
|
|
|
51
59
|
message = yield if message.nil? && block_given?
|
|
52
|
-
with_segments {
|
|
60
|
+
with_segments { dispatch(:error, message) }
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
def fatal(message = nil)
|
|
56
64
|
return unless @level_value < 5
|
|
57
65
|
|
|
58
66
|
message = yield if message.nil? && block_given?
|
|
59
|
-
with_segments {
|
|
67
|
+
with_segments { dispatch(:fatal, message) }
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
def unknown(message = nil)
|
|
63
71
|
message = yield if message.nil? && block_given?
|
|
64
|
-
with_segments {
|
|
72
|
+
with_segments { dispatch(:unknown, message) }
|
|
65
73
|
end
|
|
66
74
|
|
|
67
75
|
def trace(raw_message = nil, size: @trace_size, log_caller: true)
|
|
@@ -86,6 +94,29 @@ module Legion
|
|
|
86
94
|
|
|
87
95
|
private
|
|
88
96
|
|
|
97
|
+
def dispatch(level, message)
|
|
98
|
+
return unless defined?(Legion::Logging)
|
|
99
|
+
|
|
100
|
+
if Legion::Logging.respond_to?(:emit_tagged)
|
|
101
|
+
Legion::Logging.emit_tagged(level, message, segments: @segments)
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if Legion::Logging.respond_to?(level)
|
|
106
|
+
Legion::Logging.public_send(level, message)
|
|
107
|
+
return
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
fallback = fallback_level(level)
|
|
111
|
+
Legion::Logging.public_send(fallback, message) if fallback && Legion::Logging.respond_to?(fallback)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def fallback_level(level)
|
|
115
|
+
return :debug if level == :unknown
|
|
116
|
+
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
89
120
|
def with_segments
|
|
90
121
|
prev = Thread.current[:legion_log_segments]
|
|
91
122
|
Thread.current[:legion_log_segments] = @segments
|
data/lib/legion/logging.rb
CHANGED
|
@@ -36,6 +36,14 @@ module Legion
|
|
|
36
36
|
@exception_writer || DEFAULT_EXCEPTION_WRITER
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def current_settings
|
|
40
|
+
(@current_settings || {}).dup
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configuration_generation
|
|
44
|
+
@configuration_generation || 0
|
|
45
|
+
end
|
|
46
|
+
|
|
39
47
|
def register_category(name, description: nil, expected_fields: [])
|
|
40
48
|
CategoryRegistry.register_category(name, description: description, expected_fields: expected_fields)
|
|
41
49
|
end
|
|
@@ -68,18 +76,33 @@ module Legion
|
|
|
68
76
|
Hooks.clear_hooks!
|
|
69
77
|
end
|
|
70
78
|
|
|
71
|
-
def setup(level: '
|
|
79
|
+
def setup(level: 'debug', format: :text, async: true, **options)
|
|
72
80
|
output(**options)
|
|
73
81
|
log_level(level)
|
|
74
82
|
log_format(format: format, **options)
|
|
75
83
|
@color = options[:color]
|
|
76
84
|
@color = format != :json && (options[:color] || (options[:color].nil? && options[:log_file].nil?))
|
|
85
|
+
@current_settings = {
|
|
86
|
+
level: level,
|
|
87
|
+
format: format.to_sym,
|
|
88
|
+
async: async,
|
|
89
|
+
trace: options.fetch(:trace, true),
|
|
90
|
+
trace_size: options.fetch(:trace_size, 4),
|
|
91
|
+
extended: options.fetch(:extended, true),
|
|
92
|
+
log_file: options[:log_file],
|
|
93
|
+
log_stdout: options[:log_stdout],
|
|
94
|
+
include_pid: options.fetch(:include_pid, false),
|
|
95
|
+
color: @color
|
|
96
|
+
}.freeze
|
|
97
|
+
@configuration_generation = configuration_generation + 1
|
|
98
|
+
Legion::Logging::Redactor.refresh_patterns! if defined?(Legion::Logging::Redactor)
|
|
77
99
|
if async
|
|
78
|
-
buffer = if defined?(Legion::Settings)
|
|
79
|
-
Legion::Settings
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
buffer = if defined?(Legion::Settings) && Legion::Settings.respond_to?(:[])
|
|
101
|
+
logging_settings = Legion::Settings[:logging]
|
|
102
|
+
async_settings = logging_settings[:async] if logging_settings.is_a?(Hash)
|
|
103
|
+
async_settings[:buffer_size] if async_settings.is_a?(Hash)
|
|
82
104
|
end
|
|
105
|
+
buffer ||= 10_000
|
|
83
106
|
start_async_writer(buffer_size: buffer)
|
|
84
107
|
else
|
|
85
108
|
stop_async_writer
|