legion-logging 1.5.6 → 1.5.21
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 +114 -0
- data/CLAUDE.md +20 -10
- data/README.md +109 -10
- data/lib/legion/logging/async_writer.rb +125 -36
- data/lib/legion/logging/builder.rb +107 -39
- data/lib/legion/logging/event_builder.rb +9 -6
- data/lib/legion/logging/helper.rb +11 -19
- data/lib/legion/logging/logger.rb +1 -1
- data/lib/legion/logging/method_tracer.rb +13 -2
- data/lib/legion/logging/methods.rb +152 -53
- data/lib/legion/logging/multi_io.rb +3 -1
- data/lib/legion/logging/output_logger.rb +60 -0
- data/lib/legion/logging/redactor.rb +3 -2
- data/lib/legion/logging/rotating_file.rb +239 -0
- data/lib/legion/logging/rotation.rb +143 -0
- data/lib/legion/logging/settings.rb +27 -5
- data/lib/legion/logging/shipper/file_transport.rb +2 -1
- data/lib/legion/logging/shipper/http_transport.rb +4 -2
- data/lib/legion/logging/shipper.rb +118 -69
- data/lib/legion/logging/siem_exporter.rb +1 -1
- data/lib/legion/logging/stdio_rotator.rb +492 -0
- data/lib/legion/logging/version.rb +1 -1
- data/lib/legion/logging.rb +35 -30
- metadata +5 -1
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
+
require 'monitor'
|
|
5
|
+
require_relative 'output_logger'
|
|
6
|
+
require_relative 'rotation'
|
|
4
7
|
|
|
5
8
|
module Legion
|
|
6
9
|
module Logging
|
|
7
10
|
module Builder
|
|
11
|
+
LIFECYCLE_MUTEX = Mutex.new
|
|
12
|
+
private_constant :LIFECYCLE_MUTEX
|
|
13
|
+
|
|
8
14
|
def log_format(format: :text, include_pid: false, **)
|
|
9
15
|
@format = format.to_sym
|
|
10
16
|
if @format == :json
|
|
@@ -20,11 +26,19 @@ module Legion
|
|
|
20
26
|
|
|
21
27
|
def json_format(include_pid: false)
|
|
22
28
|
log.formatter = proc do |severity, datetime, _progname, msg|
|
|
29
|
+
origin = Thread.current[:legion_log_origin]
|
|
30
|
+
if origin
|
|
31
|
+
timestamp = origin[:timestamp]
|
|
32
|
+
thread_id = origin[:thread]
|
|
33
|
+
else
|
|
34
|
+
timestamp = datetime.utc
|
|
35
|
+
thread_id = Thread.current.object_id
|
|
36
|
+
end
|
|
23
37
|
entry = {
|
|
24
|
-
timestamp:
|
|
38
|
+
timestamp: timestamp.iso8601(3),
|
|
25
39
|
level: severity.downcase,
|
|
26
40
|
message: msg.is_a?(String) ? msg.gsub(/\e\[[0-9;]*m/, '') : msg.to_s,
|
|
27
|
-
thread:
|
|
41
|
+
thread: thread_id
|
|
28
42
|
}
|
|
29
43
|
entry[:pid] = ::Process.pid if include_pid
|
|
30
44
|
segments = Thread.current[:legion_log_segments]
|
|
@@ -48,6 +62,7 @@ module Legion
|
|
|
48
62
|
|
|
49
63
|
def text_format(include_pid: false, **options)
|
|
50
64
|
log.formatter = proc do |severity, datetime, _progname, msg|
|
|
65
|
+
datetime = Thread.current[:legion_log_origin]&.[](:timestamp)&.getlocal || datetime
|
|
51
66
|
lex_name = resolve_lex_tag(options)
|
|
52
67
|
runner_trace = Thread.current[:legion_log_caller] || build_runner_trace if lex_name
|
|
53
68
|
|
|
@@ -109,33 +124,40 @@ module Legion
|
|
|
109
124
|
end
|
|
110
125
|
|
|
111
126
|
def output(**options)
|
|
112
|
-
set_log(logfile: options[:log_file], log_stdout: options[:log_stdout]
|
|
127
|
+
set_log(logfile: options[:log_file], log_stdout: options[:log_stdout], rotation: options[:rotation],
|
|
128
|
+
shutdown_timeout: options.fetch(:shutdown_timeout, 2))
|
|
113
129
|
end
|
|
114
130
|
|
|
115
131
|
def log
|
|
116
132
|
@log ||= set_log
|
|
117
133
|
end
|
|
118
134
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
# A nil rotation option reads loaded Settings; false disables rotation.
|
|
136
|
+
def set_log(logfile: nil, log_stdout: nil, rotation: nil, shutdown_timeout: 2, **)
|
|
137
|
+
with_lifecycle_lock do
|
|
138
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + shutdown_timeout if shutdown_timeout
|
|
139
|
+
return false if @async_writer && stop_async_writer(timeout: shutdown_timeout) == false
|
|
140
|
+
|
|
141
|
+
previous_log = @log
|
|
142
|
+
rotation_opts = Legion::Logging::Rotation.resolve(raw: rotation.nil? ? Legion::Logging::Rotation.from_settings : rotation)
|
|
143
|
+
remaining = [deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), 0].max if deadline
|
|
144
|
+
stdout = stdout_target(rotation_opts: rotation_opts, uses_stdout: !logfile || log_stdout != false, timeout: remaining)
|
|
145
|
+
return false if stdout == false
|
|
146
|
+
|
|
147
|
+
if logfile && log_stdout != false
|
|
148
|
+
require_relative 'multi_io'
|
|
149
|
+
io = Legion::Logging::MultiIO.new(stdout, open_log_file(path: prepare_log_path(logfile), rotation_opts: rotation_opts))
|
|
150
|
+
@log = Legion::Logging::OutputLogger.new(io)
|
|
151
|
+
elsif logfile
|
|
152
|
+
@log = Legion::Logging::OutputLogger.new(open_log_file(path: prepare_log_path(logfile), rotation_opts: rotation_opts))
|
|
153
|
+
else
|
|
154
|
+
@log = Legion::Logging::OutputLogger.new(stdout)
|
|
155
|
+
end
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
@log.async_writer = @async_writer
|
|
158
|
+
close_replaced_log(previous_log)
|
|
159
|
+
@log
|
|
160
|
+
end
|
|
139
161
|
end
|
|
140
162
|
|
|
141
163
|
def prepare_log_path(path)
|
|
@@ -175,31 +197,77 @@ module Legion
|
|
|
175
197
|
(@async == true && @async_writer&.alive?) || false
|
|
176
198
|
end
|
|
177
199
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
200
|
+
def start_async_writer(buffer_size: 10_000, timeout: 2)
|
|
201
|
+
with_lifecycle_lock do
|
|
202
|
+
require_relative 'async_writer'
|
|
203
|
+
return false if @async_writer && stop_async_writer(timeout: timeout) == false
|
|
182
204
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
205
|
+
@async_writer = AsyncWriter.new(log, buffer_size: buffer_size)
|
|
206
|
+
log.async_writer = @async_writer
|
|
207
|
+
@async_writer.start
|
|
208
|
+
@async = true
|
|
209
|
+
true
|
|
210
|
+
end
|
|
187
211
|
end
|
|
188
212
|
|
|
189
|
-
def stop_async_writer
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
213
|
+
def stop_async_writer(timeout: 2)
|
|
214
|
+
with_lifecycle_lock do
|
|
215
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
216
|
+
return false if @async_writer && @async_writer.stop(timeout: timeout) == false
|
|
193
217
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
true
|
|
218
|
+
remaining = [deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), 0].max if deadline
|
|
219
|
+
flush_stdio_output!(timeout: remaining)
|
|
220
|
+
end
|
|
198
221
|
end
|
|
199
|
-
# rubocop:enable Naming/PredicateMethod
|
|
200
222
|
|
|
201
223
|
private
|
|
202
224
|
|
|
225
|
+
def open_log_file(path:, rotation_opts:)
|
|
226
|
+
if rotation_opts[:enabled]
|
|
227
|
+
return Legion::Logging::RotatingFile.new(path: path, max_bytes: rotation_opts[:max_bytes], max_files: rotation_opts[:max_files])
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
file = File.new(path, 'a')
|
|
231
|
+
file.sync = true
|
|
232
|
+
file
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def stdout_target(rotation_opts:, uses_stdout:, timeout:)
|
|
236
|
+
if rotation_opts[:enabled] && rotation_opts[:stdio]
|
|
237
|
+
return $stdout unless uses_stdout || equal?(Legion::Logging)
|
|
238
|
+
|
|
239
|
+
return Legion::Logging::StdioRotator.shared(max_bytes: rotation_opts[:max_bytes], max_files: rotation_opts[:max_files]) || $stdout
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
return false if equal?(Legion::Logging) && !Legion::Logging::StdioRotator.reset!(timeout: timeout)
|
|
243
|
+
|
|
244
|
+
$stdout
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def flush_stdio_output!(timeout:) # rubocop:disable Naming/PredicateMethod -- Performs a drain; false signals its deadline.
|
|
248
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
249
|
+
device = @log&.instance_variable_get(:@logdev)&.dev
|
|
250
|
+
targets = defined?(Legion::Logging::MultiIO) && device.is_a?(Legion::Logging::MultiIO) ? device.targets : [device]
|
|
251
|
+
captures = targets.grep(Legion::Logging::StdioRotator)
|
|
252
|
+
current = Legion::Logging::StdioRotator.current
|
|
253
|
+
captures << current if equal?(Legion::Logging) && current && !captures.include?(current)
|
|
254
|
+
captures.each do |capture|
|
|
255
|
+
remaining = [deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), 0].max if deadline
|
|
256
|
+
return false if capture.flush(timeout: remaining) == false
|
|
257
|
+
end
|
|
258
|
+
true
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def with_lifecycle_lock
|
|
262
|
+
lock = LIFECYCLE_MUTEX.synchronize { @lifecycle_lock ||= Monitor.new }
|
|
263
|
+
entered = lock.try_enter
|
|
264
|
+
return false unless entered
|
|
265
|
+
|
|
266
|
+
yield
|
|
267
|
+
ensure
|
|
268
|
+
lock.exit if entered
|
|
269
|
+
end
|
|
270
|
+
|
|
203
271
|
def close_replaced_log(logger)
|
|
204
272
|
return unless logger
|
|
205
273
|
return if logger.equal?(@log)
|
|
@@ -69,9 +69,10 @@ module Legion
|
|
|
69
69
|
bt = Array(exception.backtrace)
|
|
70
70
|
cf_file, cf_line, cf_func = parse_backtrace_location(bt.first) ||
|
|
71
71
|
caller_location(caller_offset)
|
|
72
|
+
origin = Thread.current[:legion_log_origin]
|
|
72
73
|
|
|
73
74
|
event = {
|
|
74
|
-
timestamp: Time.now.utc.iso8601(3),
|
|
75
|
+
timestamp: (origin&.[](:timestamp) || Time.now.utc).iso8601(3),
|
|
75
76
|
level: level,
|
|
76
77
|
exception_class: exception.class.name,
|
|
77
78
|
message: truncate_bytes(exception.message.to_s, MAX_MESSAGE_BYTES),
|
|
@@ -89,7 +90,7 @@ module Legion
|
|
|
89
90
|
ruby_version: "#{RUBY_VERSION} #{RUBY_PLATFORM}",
|
|
90
91
|
handled: handled,
|
|
91
92
|
pid: ::Process.pid,
|
|
92
|
-
thread: Thread.current.object_id
|
|
93
|
+
thread: origin&.[](:thread) || Thread.current.object_id
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
event[:task_id] = task_id if task_id
|
|
@@ -147,12 +148,13 @@ module Legion
|
|
|
147
148
|
private
|
|
148
149
|
|
|
149
150
|
def base_fields(level, message)
|
|
151
|
+
origin = Thread.current[:legion_log_origin]
|
|
150
152
|
{
|
|
151
|
-
timestamp: Time.now.utc.iso8601(3),
|
|
153
|
+
timestamp: (origin&.[](:timestamp) || Time.now.utc).iso8601(3),
|
|
152
154
|
level: level,
|
|
153
155
|
message: message.is_a?(Exception) ? message.message : strip_ansi(message.to_s),
|
|
154
156
|
pid: ::Process.pid,
|
|
155
|
-
thread: Thread.current.object_id
|
|
157
|
+
thread: origin&.[](:thread) || Thread.current.object_id
|
|
156
158
|
}
|
|
157
159
|
end
|
|
158
160
|
|
|
@@ -166,6 +168,7 @@ module Legion
|
|
|
166
168
|
|
|
167
169
|
def add_node(event)
|
|
168
170
|
return unless defined?(Legion::Settings)
|
|
171
|
+
return unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
169
172
|
|
|
170
173
|
name = begin
|
|
171
174
|
Legion::Settings[:client][:name]
|
|
@@ -177,7 +180,7 @@ module Legion
|
|
|
177
180
|
end
|
|
178
181
|
|
|
179
182
|
def add_caller_info(event, offset)
|
|
180
|
-
loc = caller_locations(offset + 1, 1)&.first
|
|
183
|
+
loc = Thread.current[:legion_log_origin]&.[](:location) || caller_locations(offset + 1, 1)&.first
|
|
181
184
|
return unless loc
|
|
182
185
|
|
|
183
186
|
event[:caller] = {
|
|
@@ -268,7 +271,7 @@ module Legion
|
|
|
268
271
|
end
|
|
269
272
|
|
|
270
273
|
def caller_location(offset)
|
|
271
|
-
loc = caller_locations(offset + 2, 1)&.first
|
|
274
|
+
loc = Thread.current[:legion_log_origin]&.[](:location) || caller_locations(offset + 2, 1)&.first
|
|
272
275
|
return [nil, nil, nil] unless loc
|
|
273
276
|
|
|
274
277
|
[loc.absolute_path || loc.path, loc.lineno, loc.base_label]
|
|
@@ -80,6 +80,13 @@ module Legion
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def handle_exception(exception, task_id: nil, level: :error, handled: true, **opts)
|
|
83
|
+
if Legion::Logging.async_producer?
|
|
84
|
+
return Legion::Logging.defer_log_call(
|
|
85
|
+
receiver: self, method_name: :handle_exception, arguments: [exception],
|
|
86
|
+
keywords: { task_id: task_id, level: level, handled: handled, **opts }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
83
90
|
segments = derive_log_segments
|
|
84
91
|
spec = gem_spec
|
|
85
92
|
ctx = Thread.current[:legion_context] || {}
|
|
@@ -101,7 +108,7 @@ module Legion
|
|
|
101
108
|
event = Legion::Logging::Redactor.redact(event) if defined?(Legion::Logging::Redactor)
|
|
102
109
|
|
|
103
110
|
write_exception_to_log(exception, event, level, segments)
|
|
104
|
-
publish_exception(event, level) if structured_exception_support?
|
|
111
|
+
Legion::Logging.publish_exception(event: event, level: level) if structured_exception_support?
|
|
105
112
|
end
|
|
106
113
|
|
|
107
114
|
def self.included(base)
|
|
@@ -519,7 +526,9 @@ module Legion
|
|
|
519
526
|
lines << " #{context_line}" unless context_line.empty?
|
|
520
527
|
|
|
521
528
|
max_frames = if event[:backtrace_limit].nil?
|
|
522
|
-
defined?(Legion::Settings)
|
|
529
|
+
if defined?(Legion::Settings) && (!Legion::Settings.respond_to?(:loaded?) || Legion::Settings.loaded?)
|
|
530
|
+
Legion::Settings.dig(:logging, :backtrace_limit)
|
|
531
|
+
end
|
|
523
532
|
else
|
|
524
533
|
event[:backtrace_limit]
|
|
525
534
|
end
|
|
@@ -555,23 +564,6 @@ module Legion
|
|
|
555
564
|
parts.join(' | ')
|
|
556
565
|
end
|
|
557
566
|
|
|
558
|
-
# -- Exception structured publish --
|
|
559
|
-
|
|
560
|
-
def publish_exception(event, level)
|
|
561
|
-
return unless structured_exception_support?
|
|
562
|
-
|
|
563
|
-
lex_name = event[:lex] || 'core'
|
|
564
|
-
comp = event[:component_type] || :unknown
|
|
565
|
-
routing_key = "legion.logging.exception.#{level}.#{lex_name}.#{comp}"
|
|
566
|
-
|
|
567
|
-
headers = build_exception_headers(event, comp, level)
|
|
568
|
-
properties = build_exception_properties(event, level)
|
|
569
|
-
|
|
570
|
-
Legion::Logging.exception_writer.call(event, routing_key: routing_key, headers: headers, properties: properties)
|
|
571
|
-
rescue StandardError => e
|
|
572
|
-
Legion::Logging.warn("Failed to publish exception event: #{e.class}: #{e.message}") if Legion::Logging.respond_to?(:warn)
|
|
573
|
-
end
|
|
574
|
-
|
|
575
567
|
def structured_exception_support?
|
|
576
568
|
defined?(Legion::Logging::EventBuilder) &&
|
|
577
569
|
Legion::Logging.respond_to?(:exception_writer)
|
|
@@ -13,7 +13,7 @@ module Legion
|
|
|
13
13
|
|
|
14
14
|
def initialize(level: 'info', log_file: nil, log_stdout: nil, lex: nil, trace: false, extended: false, trace_size: 4, format: :text, async: true, **opts)
|
|
15
15
|
@lex = lex
|
|
16
|
-
set_log(logfile: log_file, log_stdout: log_stdout)
|
|
16
|
+
set_log(logfile: log_file, log_stdout: log_stdout, rotation: opts[:rotation])
|
|
17
17
|
log_level(level)
|
|
18
18
|
log_format(format: format, lex: lex, extended: extended, **opts)
|
|
19
19
|
@color = opts[:color]
|
|
@@ -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
|