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
|
@@ -24,7 +24,7 @@ module Legion
|
|
|
24
24
|
response = post(uri, body)
|
|
25
25
|
response.is_a?(Net::HTTPSuccess)
|
|
26
26
|
rescue StandardError => e
|
|
27
|
-
|
|
27
|
+
Kernel.warn("HttpTransport ship failed: #{e.message}")
|
|
28
28
|
false
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ module Legion
|
|
|
36
36
|
apply_auth(req, uri)
|
|
37
37
|
|
|
38
38
|
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
|
|
39
|
-
open_timeout: 5, read_timeout: 10) do |http|
|
|
39
|
+
open_timeout: 5, read_timeout: 10, write_timeout: 10) do |http|
|
|
40
40
|
http.request(req, body)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -67,12 +67,14 @@ module Legion
|
|
|
67
67
|
|
|
68
68
|
def auth_token
|
|
69
69
|
return nil unless defined?(Legion::Settings)
|
|
70
|
+
return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
70
71
|
|
|
71
72
|
Legion::Settings.dig(:logging, :shipper, :auth_token)
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
def resolve_endpoint
|
|
75
76
|
return nil unless defined?(Legion::Settings)
|
|
77
|
+
return nil unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
76
78
|
|
|
77
79
|
Legion::Settings.dig(:logging, :shipper, :endpoint)
|
|
78
80
|
end
|
|
@@ -8,128 +8,183 @@ module Legion
|
|
|
8
8
|
module Logging
|
|
9
9
|
module Shipper
|
|
10
10
|
LEVEL_ORDER = %w[debug info warn error fatal].freeze
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
TRANSPORTS = { file: FileTransport, http: HttpTransport }.freeze
|
|
12
|
+
MUTEX = Mutex.new
|
|
13
|
+
WAKE = ConditionVariable.new
|
|
14
|
+
CHANGED = ConditionVariable.new
|
|
15
|
+
private_constant :MUTEX, :WAKE, :CHANGED
|
|
16
|
+
|
|
17
|
+
@buffer = []
|
|
18
|
+
@accepted = 0
|
|
19
|
+
@delivered = 0
|
|
20
|
+
@attempt = 0
|
|
21
|
+
@flush_target = 0
|
|
16
22
|
|
|
17
23
|
class << self
|
|
18
24
|
def ship(event)
|
|
19
25
|
return unless enabled?
|
|
20
26
|
return unless shippable_level?(event[:level] || event['level'])
|
|
27
|
+
return unless TRANSPORTS.key?(transport_type)
|
|
28
|
+
|
|
29
|
+
redacted = Redactor.redact(event)
|
|
30
|
+
raise ClosedQueueError, 'log shipper is stopping' unless start
|
|
31
|
+
|
|
32
|
+
MUTEX.synchronize do
|
|
33
|
+
raise ThreadError, 'log shipper cannot enqueue from its delivery thread' if Thread.current.equal?(@flush_thread)
|
|
34
|
+
|
|
35
|
+
CHANGED.wait(MUTEX) while !@stopping && @accepted - @delivered >= @capacity
|
|
36
|
+
raise ClosedQueueError, 'log shipper is stopping' if @stopping
|
|
21
37
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
@buffer << redacted
|
|
39
|
+
@accepted += 1
|
|
40
|
+
@flush_requested = true if @buffer.size >= @batch_size
|
|
41
|
+
WAKE.signal if @flush_requested
|
|
42
|
+
end
|
|
43
|
+
true
|
|
25
44
|
end
|
|
26
45
|
|
|
27
|
-
def flush
|
|
28
|
-
return if
|
|
46
|
+
def flush(timeout: 5)
|
|
47
|
+
return true if MUTEX.synchronize { @accepted == @delivered }
|
|
48
|
+
return false unless start
|
|
49
|
+
|
|
50
|
+
deadline = timeout && (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout)
|
|
51
|
+
MUTEX.synchronize do
|
|
52
|
+
return false if Thread.current.equal?(@flush_thread)
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
target = @accepted
|
|
55
|
+
attempt = @attempt
|
|
56
|
+
@flush_target = [@flush_target, target].max
|
|
57
|
+
@flush_requested = true
|
|
58
|
+
WAKE.signal
|
|
59
|
+
while @delivered < target
|
|
60
|
+
return false if @attempt > attempt && !@last_success
|
|
61
|
+
return false unless @flush_thread&.alive?
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
batch = nil
|
|
36
|
-
@mutex.synchronize { batch = @buffer.dup }
|
|
37
|
-
return if batch.empty?
|
|
63
|
+
remaining = deadline && (deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC))
|
|
64
|
+
return false if remaining && remaining <= 0
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
66
|
+
CHANGED.wait(MUTEX, remaining)
|
|
67
|
+
end
|
|
68
|
+
true
|
|
42
69
|
end
|
|
43
70
|
end
|
|
44
71
|
|
|
45
72
|
def start
|
|
46
|
-
return unless enabled?
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
return false unless enabled?
|
|
74
|
+
|
|
75
|
+
MUTEX.synchronize do
|
|
76
|
+
return !@stopping if @flush_thread&.alive?
|
|
77
|
+
|
|
78
|
+
@capacity = Legion::Settings.dig(:logging, :shipper, :buffer_size) || 10_000
|
|
79
|
+
@batch_size = [batch_size, @capacity].min
|
|
80
|
+
@interval = flush_interval
|
|
81
|
+
unless @capacity.is_a?(Integer) && @capacity.positive? && @batch_size.is_a?(Integer) && @batch_size.positive? &&
|
|
82
|
+
@interval.is_a?(Numeric) && @interval.positive?
|
|
83
|
+
raise ArgumentError, 'shipper buffer_size, batch_size, and flush_interval must be positive'
|
|
57
84
|
end
|
|
85
|
+
|
|
86
|
+
@transport = TRANSPORTS[transport_type]
|
|
87
|
+
return false unless @transport
|
|
88
|
+
|
|
89
|
+
@stopping = false
|
|
90
|
+
@flush_requested = @buffer.size >= @batch_size
|
|
91
|
+
@flush_thread = Thread.new { consume }
|
|
92
|
+
@flush_thread.name = 'legion-log-shipper'
|
|
93
|
+
@flush_thread.abort_on_exception = false
|
|
94
|
+
true
|
|
58
95
|
end
|
|
59
|
-
@flush_thread.abort_on_exception = false
|
|
60
96
|
end
|
|
61
97
|
|
|
62
|
-
def stop
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
def stop(timeout: 5)
|
|
99
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + timeout if timeout
|
|
100
|
+
thread = MUTEX.synchronize do
|
|
101
|
+
@stopping = true
|
|
102
|
+
WAKE.broadcast
|
|
103
|
+
CHANGED.broadcast
|
|
104
|
+
if !@flush_thread&.alive? && @accepted > @delivered
|
|
105
|
+
@flush_thread = Thread.new { consume }
|
|
106
|
+
@flush_thread.name = 'legion-log-shipper'
|
|
107
|
+
@flush_thread.abort_on_exception = false
|
|
108
|
+
end
|
|
109
|
+
@flush_thread
|
|
110
|
+
end
|
|
111
|
+
return MUTEX.synchronize { @accepted == @delivered } unless thread
|
|
112
|
+
return false if Thread.current.equal?(thread)
|
|
113
|
+
|
|
114
|
+
remaining = [deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC), 0].max if deadline
|
|
115
|
+
thread.join(remaining)
|
|
116
|
+
MUTEX.synchronize { !thread.alive? && @accepted == @delivered }
|
|
66
117
|
end
|
|
67
118
|
|
|
68
119
|
def enabled?
|
|
69
120
|
return false unless defined?(Legion::Settings)
|
|
121
|
+
return false unless Legion::Settings.respond_to?(:loaded?) ? Legion::Settings.loaded? : true
|
|
70
122
|
|
|
71
123
|
Legion::Settings.dig(:logging, :shipper, :enabled) == true
|
|
72
124
|
end
|
|
73
125
|
|
|
74
126
|
private
|
|
75
127
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
def consume
|
|
129
|
+
loop do
|
|
130
|
+
batch = MUTEX.synchronize do
|
|
131
|
+
WAKE.wait(MUTEX, @interval) unless @flush_requested || @stopping
|
|
132
|
+
@flush_requested = false
|
|
133
|
+
@buffer.shift(@batch_size)
|
|
134
|
+
end
|
|
135
|
+
delivered = batch.empty? || deliver(@transport, batch)
|
|
136
|
+
stopping = MUTEX.synchronize do
|
|
137
|
+
@attempt += 1
|
|
138
|
+
@last_success = delivered
|
|
139
|
+
if delivered
|
|
140
|
+
@delivered += batch.size
|
|
141
|
+
@flush_requested = @buffer.size >= @batch_size || @delivered < @flush_target
|
|
142
|
+
else
|
|
143
|
+
@buffer.prepend(*batch)
|
|
144
|
+
end
|
|
145
|
+
CHANGED.broadcast
|
|
146
|
+
@stopping && (!delivered || @buffer.empty?)
|
|
147
|
+
end
|
|
148
|
+
break if stopping
|
|
149
|
+
end
|
|
150
|
+
ensure
|
|
151
|
+
MUTEX.synchronize do
|
|
152
|
+
@stopping = true
|
|
153
|
+
CHANGED.broadcast
|
|
84
154
|
end
|
|
85
|
-
|
|
86
|
-
flush if full
|
|
87
155
|
end
|
|
88
156
|
|
|
89
157
|
def deliver(transport, batch)
|
|
90
|
-
|
|
91
|
-
transport.ship_batch(batch)
|
|
92
|
-
else
|
|
93
|
-
batch.all? { |event| transport.ship(event) }
|
|
94
|
-
end
|
|
158
|
+
transport.ship_batch(batch)
|
|
95
159
|
rescue StandardError => e
|
|
96
|
-
|
|
160
|
+
Kernel.warn("Shipper deliver failed: #{e.message}")
|
|
97
161
|
false
|
|
98
162
|
end
|
|
99
163
|
|
|
100
164
|
def shippable_level?(level)
|
|
101
165
|
return true if level.nil?
|
|
102
166
|
|
|
103
|
-
|
|
104
|
-
LEVEL_ORDER.index(level.to_s.downcase).to_i >= LEVEL_ORDER.index(min).to_i
|
|
167
|
+
LEVEL_ORDER.index(level.to_s.downcase).to_i >= LEVEL_ORDER.index(minimum_level).to_i
|
|
105
168
|
end
|
|
106
169
|
|
|
107
170
|
def transport_type
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
key = Legion::Settings.dig(:logging, :shipper, :transport)
|
|
171
|
+
key = Legion::Settings.dig(:logging, :shipper, :transport) if defined?(Legion::Settings)
|
|
111
172
|
key ? key.to_sym : :file
|
|
112
173
|
end
|
|
113
174
|
|
|
114
175
|
def batch_size
|
|
115
|
-
return 100 unless defined?(Legion::Settings)
|
|
116
|
-
|
|
117
176
|
Legion::Settings.dig(:logging, :shipper, :batch_size) || 100
|
|
118
177
|
end
|
|
119
178
|
|
|
120
179
|
def flush_interval
|
|
121
|
-
return 5 unless defined?(Legion::Settings)
|
|
122
|
-
|
|
123
180
|
Legion::Settings.dig(:logging, :shipper, :flush_interval) || 5
|
|
124
181
|
end
|
|
125
182
|
|
|
126
183
|
def minimum_level
|
|
127
|
-
return 'warn' unless defined?(Legion::Settings)
|
|
128
|
-
|
|
129
184
|
levels = Legion::Settings.dig(:logging, :shipper, :levels)
|
|
130
185
|
return 'warn' unless levels.is_a?(Array) && !levels.empty?
|
|
131
186
|
|
|
132
|
-
levels.min_by { |
|
|
187
|
+
levels.min_by { |level| LEVEL_ORDER.index(level.to_s) || 99 }.to_s
|
|
133
188
|
end
|
|
134
189
|
end
|
|
135
190
|
end
|
|
@@ -28,7 +28,8 @@ module Legion
|
|
|
28
28
|
req['Content-Type'] = 'application/json'
|
|
29
29
|
req.body = ::JSON.dump({ event: redact_phi(event.to_s), time: Time.now.to_f })
|
|
30
30
|
|
|
31
|
-
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https'
|
|
31
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
|
|
32
|
+
open_timeout: 5, read_timeout: 10, write_timeout: 10) do |http|
|
|
32
33
|
http.request(req)
|
|
33
34
|
end
|
|
34
35
|
rescue StandardError => e
|
|
@@ -32,44 +32,44 @@ module Legion
|
|
|
32
32
|
@level_value
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def debug(message = nil)
|
|
35
|
+
def debug(message = nil, **ctx)
|
|
36
36
|
return unless @level_value < 1
|
|
37
37
|
|
|
38
38
|
message = yield if message.nil? && block_given?
|
|
39
|
-
with_segments { dispatch(:debug, message) }
|
|
39
|
+
with_segments { dispatch(:debug, message, **ctx) }
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def info(message = nil)
|
|
42
|
+
def info(message = nil, **ctx)
|
|
43
43
|
return unless @level_value < 2
|
|
44
44
|
|
|
45
45
|
message = yield if message.nil? && block_given?
|
|
46
|
-
with_segments { dispatch(:info, message) }
|
|
46
|
+
with_segments { dispatch(:info, message, **ctx) }
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def warn(message = nil)
|
|
49
|
+
def warn(message = nil, **ctx)
|
|
50
50
|
return unless @level_value < 3
|
|
51
51
|
|
|
52
52
|
message = yield if message.nil? && block_given?
|
|
53
|
-
with_segments { dispatch(:warn, message) }
|
|
53
|
+
with_segments { dispatch(:warn, message, **ctx) }
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def error(message = nil)
|
|
56
|
+
def error(message = nil, **ctx)
|
|
57
57
|
return unless @level_value < 4
|
|
58
58
|
|
|
59
59
|
message = yield if message.nil? && block_given?
|
|
60
|
-
with_segments { dispatch(:error, message) }
|
|
60
|
+
with_segments { dispatch(:error, message, **ctx) }
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def fatal(message = nil)
|
|
63
|
+
def fatal(message = nil, **ctx)
|
|
64
64
|
return unless @level_value < 5
|
|
65
65
|
|
|
66
66
|
message = yield if message.nil? && block_given?
|
|
67
|
-
with_segments { dispatch(:fatal, message) }
|
|
67
|
+
with_segments { dispatch(:fatal, message, **ctx) }
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def unknown(message = nil)
|
|
70
|
+
def unknown(message = nil, **ctx)
|
|
71
71
|
message = yield if message.nil? && block_given?
|
|
72
|
-
with_segments { dispatch(:unknown, message) }
|
|
72
|
+
with_segments { dispatch(:unknown, message, **ctx) }
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def trace(raw_message = nil, size: @trace_size, log_caller: true)
|
|
@@ -94,21 +94,23 @@ module Legion
|
|
|
94
94
|
|
|
95
95
|
private
|
|
96
96
|
|
|
97
|
-
def dispatch(level, message)
|
|
97
|
+
def dispatch(level, message, **ctx)
|
|
98
98
|
return unless defined?(Legion::Logging)
|
|
99
99
|
|
|
100
100
|
if Legion::Logging.respond_to?(:emit_tagged)
|
|
101
|
-
Legion::Logging.emit_tagged(level, message, segments: @segments)
|
|
101
|
+
Legion::Logging.emit_tagged(level, message, segments: @segments, **ctx)
|
|
102
102
|
return
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
if Legion::Logging.respond_to?(level)
|
|
106
|
-
Legion::Logging.public_send(level, message)
|
|
106
|
+
Legion::Logging.public_send(level, message, **ctx)
|
|
107
107
|
return
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
fallback = fallback_level(level)
|
|
111
|
-
|
|
111
|
+
return unless fallback && Legion::Logging.respond_to?(fallback)
|
|
112
|
+
|
|
113
|
+
Legion::Logging.public_send(fallback, message, **ctx)
|
|
112
114
|
end
|
|
113
115
|
|
|
114
116
|
def fallback_level(level)
|
data/lib/legion/logging.rb
CHANGED
|
@@ -76,36 +76,37 @@ module Legion
|
|
|
76
76
|
Hooks.clear_hooks!
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
def setup(level: 'debug', format: :text, async: true, **options)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
format:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
79
|
+
def setup(level: 'debug', format: :text, async: true, shutdown_timeout: 2, **options)
|
|
80
|
+
with_lifecycle_lock do
|
|
81
|
+
return false if @async_writer && stop_async_writer(timeout: shutdown_timeout) == false
|
|
82
|
+
return false if output(**options) == false
|
|
83
|
+
|
|
84
|
+
log_level(level)
|
|
85
|
+
log_format(format: format, **options)
|
|
86
|
+
@color = options[:color]
|
|
87
|
+
@color = format != :json && (options[:color] || (options[:color].nil? && options[:log_file].nil?))
|
|
88
|
+
@current_settings = {
|
|
89
|
+
level: level,
|
|
90
|
+
format: format.to_sym,
|
|
91
|
+
async: async,
|
|
92
|
+
trace: options.fetch(:trace, true),
|
|
93
|
+
trace_size: options.fetch(:trace_size, 4),
|
|
94
|
+
extended: options.fetch(:extended, true),
|
|
95
|
+
log_file: options[:log_file],
|
|
96
|
+
log_stdout: options[:log_stdout],
|
|
97
|
+
include_pid: options.fetch(:include_pid, false),
|
|
98
|
+
color: @color
|
|
99
|
+
}.freeze
|
|
100
|
+
@configuration_generation = configuration_generation + 1
|
|
101
|
+
Legion::Logging::Redactor.refresh_patterns! if defined?(Legion::Logging::Redactor)
|
|
102
|
+
if async
|
|
103
|
+
start_async_writer(buffer_size: Legion::Logging::Settings.async_buffer_size)
|
|
104
|
+
else
|
|
105
|
+
@async_writer = nil
|
|
106
|
+
@async = false
|
|
107
|
+
log.async_writer = nil
|
|
108
|
+
true
|
|
109
|
+
end
|
|
109
110
|
end
|
|
110
111
|
end
|
|
111
112
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-logging
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.5.
|
|
4
|
+
version: 1.5.19
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -63,12 +63,14 @@ files:
|
|
|
63
63
|
- lib/legion/logging/builder.rb
|
|
64
64
|
- lib/legion/logging/category_registry.rb
|
|
65
65
|
- lib/legion/logging/event_builder.rb
|
|
66
|
+
- lib/legion/logging/header_builder.rb
|
|
66
67
|
- lib/legion/logging/helper.rb
|
|
67
68
|
- lib/legion/logging/hooks.rb
|
|
68
69
|
- lib/legion/logging/logger.rb
|
|
69
70
|
- lib/legion/logging/method_tracer.rb
|
|
70
71
|
- lib/legion/logging/methods.rb
|
|
71
72
|
- lib/legion/logging/multi_io.rb
|
|
73
|
+
- lib/legion/logging/output_logger.rb
|
|
72
74
|
- lib/legion/logging/redactor.rb
|
|
73
75
|
- lib/legion/logging/settings.rb
|
|
74
76
|
- lib/legion/logging/shipper.rb
|