semantic_logger 4.17.0 → 5.0.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/README.md +42 -81
- data/Rakefile +8 -1
- data/lib/semantic_logger/appender/async.rb +86 -173
- data/lib/semantic_logger/appender/cloudwatch_logs.rb +4 -4
- data/lib/semantic_logger/appender/elasticsearch.rb +6 -182
- data/lib/semantic_logger/appender/elasticsearch_base.rb +212 -0
- data/lib/semantic_logger/appender/elasticsearch_http.rb +2 -2
- data/lib/semantic_logger/appender/file.rb +20 -4
- data/lib/semantic_logger/appender/graylog.rb +2 -2
- data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
- data/lib/semantic_logger/appender/http.rb +27 -2
- data/lib/semantic_logger/appender/io.rb +8 -4
- data/lib/semantic_logger/appender/kafka.rb +2 -2
- data/lib/semantic_logger/appender/loki.rb +2 -4
- data/lib/semantic_logger/appender/mongodb.rb +3 -6
- data/lib/semantic_logger/appender/new_relic_logs.rb +12 -2
- data/lib/semantic_logger/appender/open_telemetry.rb +25 -11
- data/lib/semantic_logger/appender/opensearch.rb +35 -0
- data/lib/semantic_logger/appender/rabbitmq.rb +3 -3
- data/lib/semantic_logger/appender/sentry_ruby.rb +8 -3
- data/lib/semantic_logger/appender/splunk.rb +2 -2
- data/lib/semantic_logger/appender/splunk_http.rb +3 -3
- data/lib/semantic_logger/appender/syslog.rb +5 -4
- data/lib/semantic_logger/appender/tcp.rb +2 -2
- data/lib/semantic_logger/appender/udp.rb +2 -2
- data/lib/semantic_logger/appender/wrapper.rb +4 -4
- data/lib/semantic_logger/appender.rb +30 -19
- data/lib/semantic_logger/appenders.rb +26 -5
- data/lib/semantic_logger/base.rb +113 -21
- data/lib/semantic_logger/concerns/compatibility.rb +2 -2
- data/lib/semantic_logger/core_ext/process.rb +34 -0
- data/lib/semantic_logger/formatters/base.rb +46 -7
- data/lib/semantic_logger/formatters/color.rb +6 -3
- data/lib/semantic_logger/formatters/default.rb +6 -4
- data/lib/semantic_logger/formatters/ecs.rb +151 -0
- data/lib/semantic_logger/formatters/fluentd.rb +15 -4
- data/lib/semantic_logger/formatters/json.rb +6 -1
- data/lib/semantic_logger/formatters/logfmt.rb +2 -2
- data/lib/semantic_logger/formatters/loki.rb +4 -4
- data/lib/semantic_logger/formatters/new_relic_logs.rb +4 -6
- data/lib/semantic_logger/formatters/open_telemetry.rb +40 -2
- data/lib/semantic_logger/formatters/pattern.rb +235 -0
- data/lib/semantic_logger/formatters/raw.rb +2 -2
- data/lib/semantic_logger/formatters/signalfx.rb +2 -2
- data/lib/semantic_logger/formatters/syslog.rb +14 -3
- data/lib/semantic_logger/formatters/syslog_cee.rb +1 -1
- data/lib/semantic_logger/formatters.rb +2 -0
- data/lib/semantic_logger/log.rb +23 -4
- data/lib/semantic_logger/logger.rb +2 -2
- data/lib/semantic_logger/metric/new_relic.rb +2 -2
- data/lib/semantic_logger/metric/signalfx.rb +2 -2
- data/lib/semantic_logger/metric/statsd.rb +2 -2
- data/lib/semantic_logger/processor.rb +22 -1
- data/lib/semantic_logger/queue_processor.rb +369 -0
- data/lib/semantic_logger/reporters/minitest.rb +4 -4
- data/lib/semantic_logger/semantic_logger.rb +103 -11
- data/lib/semantic_logger/subscriber.rb +15 -2
- data/lib/semantic_logger/sync_processor.rb +25 -3
- data/lib/semantic_logger/test/capture_log_events.rb +2 -2
- data/lib/semantic_logger/test/minitest.rb +8 -4
- data/lib/semantic_logger/test/rspec.rb +249 -0
- data/lib/semantic_logger/utils.rb +83 -4
- data/lib/semantic_logger/version.rb +1 -1
- data/lib/semantic_logger.rb +9 -0
- metadata +15 -6
- data/lib/semantic_logger/appender/async_batch.rb +0 -93
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
module SemanticLogger
|
|
2
|
+
# Internal class that processes log messages from a queue on a separate thread.
|
|
3
|
+
#
|
|
4
|
+
# Internal use only: it owns the worker thread and the in-memory queue that back the
|
|
5
|
+
# asynchronous appender proxy (SemanticLogger::Appender::Async). It is never returned to
|
|
6
|
+
# application code.
|
|
7
|
+
#
|
|
8
|
+
# Supports two processing modes, selected by the `batch:` option:
|
|
9
|
+
# * Streaming (batch: false): each log message is written to the appender as it is dequeued.
|
|
10
|
+
# * Batching (batch: true): log messages are grouped and written via the appender's #batch
|
|
11
|
+
# method, either once batch_size messages have accumulated, or
|
|
12
|
+
# batch_seconds have elapsed since the previous batch.
|
|
13
|
+
class QueueProcessor
|
|
14
|
+
attr_accessor :lag_check_interval, :lag_threshold_s, :dropped_message_report_seconds,
|
|
15
|
+
:batch_size, :batch_seconds
|
|
16
|
+
attr_reader :appender, :queue, :max_queue_size, :non_blocking, :signal,
|
|
17
|
+
:processed_count, :dropped_count, :async_max_retries, :retry_count
|
|
18
|
+
|
|
19
|
+
# Create a new processor and start its worker thread.
|
|
20
|
+
def self.start(**args)
|
|
21
|
+
processor = new(**args)
|
|
22
|
+
processor.thread
|
|
23
|
+
processor
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Parameters:
|
|
27
|
+
# appender: [SemanticLogger::Subscriber]
|
|
28
|
+
# The appender to forward log messages to from the worker thread.
|
|
29
|
+
#
|
|
30
|
+
# max_queue_size: [Integer]
|
|
31
|
+
# The maximum number of log messages to hold on the queue before blocking attempts to add to the queue.
|
|
32
|
+
# -1: The queue size is uncapped and will never block no matter how long the queue is.
|
|
33
|
+
# Default: 10,000
|
|
34
|
+
#
|
|
35
|
+
# lag_threshold_s: [Float]
|
|
36
|
+
# Log a warning when a log message has been on the queue for longer than this period in seconds.
|
|
37
|
+
# Default: 30
|
|
38
|
+
#
|
|
39
|
+
# lag_check_interval: [Integer]
|
|
40
|
+
# Number of messages to process before checking for slow logging.
|
|
41
|
+
# Default: 1,000
|
|
42
|
+
# Note: Not applicable when batch: true.
|
|
43
|
+
#
|
|
44
|
+
# batch: [true|false]
|
|
45
|
+
# Process log messages in batches via the appender's #batch method.
|
|
46
|
+
# Default: false
|
|
47
|
+
# Note: The appender must implement #batch.
|
|
48
|
+
#
|
|
49
|
+
# batch_size: [Integer]
|
|
50
|
+
# Maximum number of messages to batch up before sending.
|
|
51
|
+
# Default: 300
|
|
52
|
+
# Note: Only applicable when batch: true.
|
|
53
|
+
#
|
|
54
|
+
# batch_seconds: [Integer]
|
|
55
|
+
# Maximum number of seconds between sending batches.
|
|
56
|
+
# Default: 5
|
|
57
|
+
# Note: Only applicable when batch: true.
|
|
58
|
+
#
|
|
59
|
+
# non_blocking: [true|false]
|
|
60
|
+
# Whether to drop log messages instead of blocking the calling thread when the queue is full.
|
|
61
|
+
# Only applies to a capped queue.
|
|
62
|
+
# Default: false
|
|
63
|
+
#
|
|
64
|
+
# dropped_message_report_seconds: [Integer]
|
|
65
|
+
# When non_blocking is enabled, log the count of dropped messages to the internal logger
|
|
66
|
+
# at most once every this number of seconds.
|
|
67
|
+
# Default: 30
|
|
68
|
+
#
|
|
69
|
+
# async_max_retries: [Integer]
|
|
70
|
+
# Maximum number of consecutive times to restart the worker thread after it raises an
|
|
71
|
+
# exception while processing messages. Each restart first sleeps for `retry_count` seconds
|
|
72
|
+
# (1s, then 2s, ...) as a back-off. Once this many consecutive retries are exhausted the
|
|
73
|
+
# thread stops instead of restarting. The counter resets to zero whenever a message is
|
|
74
|
+
# processed successfully.
|
|
75
|
+
# -1: Retry indefinitely and never stop the thread (the pre-v5 behaviour). The back-off
|
|
76
|
+
# still applies, and still resets after any successful message.
|
|
77
|
+
# Default: 100
|
|
78
|
+
def initialize(appender:,
|
|
79
|
+
max_queue_size: 10_000,
|
|
80
|
+
lag_check_interval: 1_000,
|
|
81
|
+
lag_threshold_s: 30,
|
|
82
|
+
batch: false,
|
|
83
|
+
batch_size: 300,
|
|
84
|
+
batch_seconds: 5,
|
|
85
|
+
non_blocking: false,
|
|
86
|
+
dropped_message_report_seconds: 30,
|
|
87
|
+
async_max_retries: 100)
|
|
88
|
+
@appender = appender
|
|
89
|
+
@max_queue_size = max_queue_size
|
|
90
|
+
@lag_check_interval = lag_check_interval
|
|
91
|
+
@lag_threshold_s = lag_threshold_s
|
|
92
|
+
@batch = batch
|
|
93
|
+
@batch_size = batch_size
|
|
94
|
+
@batch_seconds = batch_seconds
|
|
95
|
+
@non_blocking = non_blocking
|
|
96
|
+
@dropped_message_report_seconds = dropped_message_report_seconds
|
|
97
|
+
@async_max_retries = async_max_retries
|
|
98
|
+
@retry_count = 0
|
|
99
|
+
@thread = nil
|
|
100
|
+
# Only batch mode parks the worker on the signal; streaming mode never touches it.
|
|
101
|
+
@signal = Concurrent::Event.new if batch
|
|
102
|
+
@dropped_message_count = 0
|
|
103
|
+
@dropped_message_reported_at = Time.now
|
|
104
|
+
@dropped_message_mutex = Mutex.new
|
|
105
|
+
@processed_count = 0
|
|
106
|
+
@dropped_count = 0
|
|
107
|
+
create_queue
|
|
108
|
+
|
|
109
|
+
return unless batch? && !appender.respond_to?(:batch)
|
|
110
|
+
|
|
111
|
+
raise(ArgumentError, "#{appender.class.name} does not support batching. It must implement #batch")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Internal logger used to report problems encountered while processing log messages.
|
|
115
|
+
def logger
|
|
116
|
+
appender.logger
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns [Thread] the worker thread.
|
|
120
|
+
#
|
|
121
|
+
# Starts the worker thread if it is not currently running.
|
|
122
|
+
def thread
|
|
123
|
+
return @thread if @thread&.alive?
|
|
124
|
+
|
|
125
|
+
@thread = spawn_worker
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns [true|false] whether the worker thread is running.
|
|
129
|
+
def active?
|
|
130
|
+
@thread&.alive?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns [true|false] whether the queue has a capped size.
|
|
134
|
+
def capped?
|
|
135
|
+
@capped
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns [true|false] whether messages are dropped instead of blocking when the queue is full.
|
|
139
|
+
# Only a capped queue can drop messages.
|
|
140
|
+
def non_blocking?
|
|
141
|
+
@non_blocking && capped?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns [true|false] whether messages are processed in batches.
|
|
145
|
+
def batch?
|
|
146
|
+
@batch
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Add a log message to the queue for processing.
|
|
150
|
+
#
|
|
151
|
+
# When non-blocking and the queue is full, the message is dropped instead of blocking the
|
|
152
|
+
# calling thread, and the count of dropped messages is reported periodically.
|
|
153
|
+
def log(log)
|
|
154
|
+
enqueued = true
|
|
155
|
+
if non_blocking?
|
|
156
|
+
begin
|
|
157
|
+
queue.push(log, true)
|
|
158
|
+
rescue ThreadError
|
|
159
|
+
message_dropped
|
|
160
|
+
enqueued = false
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
queue << log
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# For batches wake up the processing thread once the number of queued messages has been
|
|
167
|
+
# exceeded. Also wake it on a drop so it drains the full queue rather than waiting out the
|
|
168
|
+
# batch interval.
|
|
169
|
+
signal.set if batch? && (queue.size >= batch_size)
|
|
170
|
+
|
|
171
|
+
enqueued
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Flush all queued log entries to the appender.
|
|
175
|
+
# All queued log messages are written and then the appender is flushed.
|
|
176
|
+
def flush
|
|
177
|
+
submit_request(:flush)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Flush any outstanding messages and close the appender.
|
|
181
|
+
def close
|
|
182
|
+
submit_request(:close)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Re-open the queue and worker thread after a fork.
|
|
186
|
+
def reopen
|
|
187
|
+
# Workaround CRuby crash on fork by recreating queue on reopen
|
|
188
|
+
# https://github.com/reidmorrison/semantic_logger/issues/103
|
|
189
|
+
@queue&.close
|
|
190
|
+
create_queue
|
|
191
|
+
|
|
192
|
+
@thread&.kill if @thread&.alive?
|
|
193
|
+
@thread = spawn_worker
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
# Start the worker thread, naming it after the internal logger.
|
|
199
|
+
def spawn_worker
|
|
200
|
+
Thread.new do
|
|
201
|
+
Thread.current.name = logger.name
|
|
202
|
+
process
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def create_queue
|
|
207
|
+
if max_queue_size == -1
|
|
208
|
+
@queue = Queue.new
|
|
209
|
+
@capped = false
|
|
210
|
+
else
|
|
211
|
+
@queue = SizedQueue.new(max_queue_size)
|
|
212
|
+
@capped = true
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Separate thread for processing log messages.
|
|
217
|
+
def process
|
|
218
|
+
# This thread is designed to never go down unless the main thread terminates
|
|
219
|
+
# or the appender is closed.
|
|
220
|
+
logger.trace "Async: Appender thread active"
|
|
221
|
+
begin
|
|
222
|
+
batch? ? process_messages_in_batches : process_messages
|
|
223
|
+
rescue StandardError => e
|
|
224
|
+
if async_max_retries == -1 || retry_count < async_max_retries
|
|
225
|
+
@retry_count += 1
|
|
226
|
+
limit = async_max_retries == -1 ? "unlimited" : async_max_retries
|
|
227
|
+
safe_log(:warn, "Async: Restarting due to exception, retry #{retry_count} of #{limit}, sleeping #{retry_count}s", e)
|
|
228
|
+
sleep(retry_count)
|
|
229
|
+
retry
|
|
230
|
+
else
|
|
231
|
+
safe_log(:error, "Async: Stopping after #{retry_count} failed retries", e)
|
|
232
|
+
end
|
|
233
|
+
rescue Exception => e
|
|
234
|
+
safe_log(:error, "Async: Stopping due to fatal exception", e)
|
|
235
|
+
ensure
|
|
236
|
+
@thread = nil
|
|
237
|
+
safe_log(:trace, "Async: Thread has stopped")
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Log to the internal logger, ignoring any error.
|
|
242
|
+
# These calls may run after Ruby has released file handles during shutdown.
|
|
243
|
+
def safe_log(level, message, exception = nil)
|
|
244
|
+
logger.public_send(level, message, exception)
|
|
245
|
+
rescue StandardError
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Streaming: write each log message to the appender as it is dequeued.
|
|
250
|
+
def process_messages
|
|
251
|
+
count = 0
|
|
252
|
+
while (message = queue.pop)
|
|
253
|
+
if message.is_a?(Log)
|
|
254
|
+
appender.log(message)
|
|
255
|
+
@processed_count += 1
|
|
256
|
+
@retry_count = 0 unless retry_count.zero?
|
|
257
|
+
count += 1
|
|
258
|
+
# Check every few log messages whether this appender thread is falling behind
|
|
259
|
+
if count > lag_check_interval
|
|
260
|
+
check_lag(message)
|
|
261
|
+
count = 0
|
|
262
|
+
end
|
|
263
|
+
else
|
|
264
|
+
break unless process_message(message)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
logger.trace "Async: Queue Closed"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Batching: group up log messages before writing them via the appender's #batch method.
|
|
271
|
+
def process_messages_in_batches
|
|
272
|
+
loop do
|
|
273
|
+
# Wait for batch interval or number of messages to be exceeded.
|
|
274
|
+
signal.wait(batch_seconds)
|
|
275
|
+
|
|
276
|
+
logs = []
|
|
277
|
+
messages = []
|
|
278
|
+
first = true
|
|
279
|
+
message_count = queue.length
|
|
280
|
+
message_count.times do
|
|
281
|
+
# Queue#pop(true) raises an exception when there are no more messages, which is considered expensive.
|
|
282
|
+
message = queue.pop
|
|
283
|
+
if message.is_a?(Log)
|
|
284
|
+
logs << message
|
|
285
|
+
if first
|
|
286
|
+
check_lag(message)
|
|
287
|
+
first = false
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
messages << message
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
if logs.size.positive?
|
|
294
|
+
appender.batch(logs)
|
|
295
|
+
@processed_count += logs.size
|
|
296
|
+
@retry_count = 0 unless retry_count.zero?
|
|
297
|
+
end
|
|
298
|
+
# Stop processing once a command (e.g. :close) signals the thread to terminate.
|
|
299
|
+
break if messages.any? { |message| !process_message(message) }
|
|
300
|
+
|
|
301
|
+
signal.reset unless queue.size >= batch_size
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Returns false when message processing should be stopped.
|
|
306
|
+
def process_message(message)
|
|
307
|
+
case message[:command]
|
|
308
|
+
when :flush
|
|
309
|
+
appender.flush
|
|
310
|
+
message[:reply_queue] << true if message[:reply_queue]
|
|
311
|
+
when :close
|
|
312
|
+
appender.close
|
|
313
|
+
message[:reply_queue] << true if message[:reply_queue]
|
|
314
|
+
return false
|
|
315
|
+
else
|
|
316
|
+
logger.warn "Async: Appender thread: Ignoring unknown command: #{message[:command]}"
|
|
317
|
+
end
|
|
318
|
+
true
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Record a dropped message, reporting the running count to the internal logger at most
|
|
322
|
+
# once every dropped_message_report_seconds.
|
|
323
|
+
def message_dropped
|
|
324
|
+
@dropped_message_mutex.synchronize do
|
|
325
|
+
@dropped_message_count += 1
|
|
326
|
+
@dropped_count += 1
|
|
327
|
+
diff = Time.now - @dropped_message_reported_at
|
|
328
|
+
return if diff < dropped_message_report_seconds
|
|
329
|
+
|
|
330
|
+
logger.warn(
|
|
331
|
+
"Async: Dropped #{@dropped_message_count} log messages in the last #{diff.round} seconds. " \
|
|
332
|
+
"The queue is full (max_queue_size: #{max_queue_size}). " \
|
|
333
|
+
"Consider reducing the log level, increasing max_queue_size, or changing the appenders"
|
|
334
|
+
)
|
|
335
|
+
@dropped_message_count = 0
|
|
336
|
+
@dropped_message_reported_at = Time.now
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def check_lag(log)
|
|
341
|
+
diff = Time.now - log.time
|
|
342
|
+
return unless diff > lag_threshold_s
|
|
343
|
+
|
|
344
|
+
logger.warn "Async: Appender thread has fallen behind by #{diff} seconds with #{queue.size} messages queued up. Consider reducing the log level or changing the appenders"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Submit a command to the worker thread and wait for the reply.
|
|
348
|
+
def submit_request(command)
|
|
349
|
+
return false unless active?
|
|
350
|
+
|
|
351
|
+
queue_size = queue.size
|
|
352
|
+
msg = "Async: Queued log messages: #{queue_size}, running command: #{command}"
|
|
353
|
+
if queue_size > 1_000
|
|
354
|
+
logger.warn msg
|
|
355
|
+
elsif queue_size > 100
|
|
356
|
+
logger.info msg
|
|
357
|
+
elsif queue_size.positive?
|
|
358
|
+
logger.trace msg
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Wake up the processing thread to process this command immediately.
|
|
362
|
+
signal.set if batch?
|
|
363
|
+
|
|
364
|
+
reply_queue = Queue.new
|
|
365
|
+
queue << {command: command, reply_queue: reply_queue}
|
|
366
|
+
reply_queue.pop
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
@@ -28,18 +28,18 @@ module SemanticLogger
|
|
|
28
28
|
attr_accessor :io
|
|
29
29
|
|
|
30
30
|
def before_test(test)
|
|
31
|
-
logger.info("START #{test.
|
|
31
|
+
logger.info("START #{test.class.name} #{test.name}")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def after_test(test)
|
|
35
35
|
if test.error?
|
|
36
|
-
logger.benchmark_error("FAIL #{test.
|
|
36
|
+
logger.benchmark_error("FAIL #{test.class.name} #{test.name}", duration: test.time * 1_000,
|
|
37
37
|
metric: "minitest/fail")
|
|
38
38
|
elsif test.skipped?
|
|
39
|
-
logger.benchmark_warn("SKIP #{test.
|
|
39
|
+
logger.benchmark_warn("SKIP #{test.class.name} #{test.name}", duration: test.time * 1_000,
|
|
40
40
|
metric: "minitest/skip")
|
|
41
41
|
else
|
|
42
|
-
logger.benchmark_info("PASS #{test.
|
|
42
|
+
logger.benchmark_info("PASS #{test.class.name} #{test.name}", duration: test.time * 1_000,
|
|
43
43
|
metric: "minitest/pass")
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -4,9 +4,42 @@ module SemanticLogger
|
|
|
4
4
|
# Logging levels in order of most detailed to most severe
|
|
5
5
|
LEVELS = Levels::LEVELS
|
|
6
6
|
|
|
7
|
-
# Return a logger for the supplied class or class_name
|
|
7
|
+
# Return a logger for the supplied class or class_name.
|
|
8
|
+
#
|
|
9
|
+
# When `SemanticLogger.cache_loggers` is enabled (opt-in, default off) and a
|
|
10
|
+
# Class or Module is supplied, the same Logger instance is returned for every
|
|
11
|
+
# call with that class. This makes it possible to obtain a logger once and
|
|
12
|
+
# later change its level (or filter) and have every holder of that logger see
|
|
13
|
+
# the change.
|
|
14
|
+
#
|
|
15
|
+
# A String is always given its own new Logger instance, even when caching is
|
|
16
|
+
# enabled: callers that pass a string typically want an independent logger
|
|
17
|
+
# (for example to set a different level per call site). Anonymous classes
|
|
18
|
+
# (those with no `name`) are never cached, to avoid pinning short-lived
|
|
19
|
+
# dynamically created classes in memory.
|
|
8
20
|
def self.[](klass)
|
|
9
|
-
Logger.new(klass)
|
|
21
|
+
return Logger.new(klass) if !@cache_loggers || klass.is_a?(String) || klass.name.nil?
|
|
22
|
+
|
|
23
|
+
logger_cache.compute_if_absent(klass) { Logger.new(klass) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Whether `SemanticLogger[Class]` returns a shared, cached Logger instance per
|
|
27
|
+
# class. Disabled by default. Strings are never cached (see #[]).
|
|
28
|
+
def self.cache_loggers=(cache_loggers)
|
|
29
|
+
@cache_loggers = cache_loggers
|
|
30
|
+
clear_logger_cache unless cache_loggers
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns whether logger caching is enabled.
|
|
34
|
+
def self.cache_loggers?
|
|
35
|
+
@cache_loggers
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Discard all cached loggers so that subsequent `SemanticLogger[Class]` calls
|
|
39
|
+
# build fresh instances. Primarily useful in tests, or after redefining a
|
|
40
|
+
# class that was previously cached.
|
|
41
|
+
def self.clear_logger_cache
|
|
42
|
+
@logger_cache&.clear
|
|
10
43
|
end
|
|
11
44
|
|
|
12
45
|
# Sets the global default log level
|
|
@@ -162,8 +195,8 @@ module SemanticLogger
|
|
|
162
195
|
# logger = SemanticLogger['Example']
|
|
163
196
|
# logger.info "Hello World"
|
|
164
197
|
# logger.debug("Login time", user: 'Joe', duration: 100, ip_address: '127.0.0.1')
|
|
165
|
-
def self.add_appender(**args, &
|
|
166
|
-
appender = appenders.add(**args, &
|
|
198
|
+
def self.add_appender(**args, &)
|
|
199
|
+
appender = appenders.add(**args, &)
|
|
167
200
|
# Start appender thread if it is not already running
|
|
168
201
|
Logger.processor.start
|
|
169
202
|
appender
|
|
@@ -202,14 +235,43 @@ module SemanticLogger
|
|
|
202
235
|
Logger.processor.close
|
|
203
236
|
end
|
|
204
237
|
|
|
205
|
-
#
|
|
206
|
-
#
|
|
238
|
+
# Re-open any open file handles etc. to resources.
|
|
239
|
+
#
|
|
240
|
+
# Called automatically in the child process after a fork (see reopen_on_fork?),
|
|
241
|
+
# and may also be called manually.
|
|
242
|
+
#
|
|
243
|
+
# To avoid reopening twice after a single fork (for example when the automatic
|
|
244
|
+
# fork hook and a framework's `after_fork` callback both fire), reopen is a no-op
|
|
245
|
+
# if it has already run in the current process. Pass `force: true` to override
|
|
246
|
+
# this guard and reopen unconditionally, for example when re-opening file handles
|
|
247
|
+
# in the same process after an external log rotation.
|
|
207
248
|
#
|
|
208
249
|
# Note:
|
|
209
250
|
# Not all appender's implement reopen.
|
|
210
251
|
# Check the code for each appender you are using before relying on this behavior.
|
|
211
|
-
def self.reopen
|
|
252
|
+
def self.reopen(force: false)
|
|
253
|
+
return if !force && @reopened_pid == ::Process.pid
|
|
254
|
+
|
|
212
255
|
Logger.processor.reopen
|
|
256
|
+
@reopened_pid = ::Process.pid
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Whether appenders are automatically reopened in the child process after a fork.
|
|
260
|
+
#
|
|
261
|
+
# Enabled by default. A `Process._fork` hook (Ruby 3.1+) calls SemanticLogger.reopen
|
|
262
|
+
# in the child after `fork`, `Process.daemon`, `IO.popen`, `Kernel#system`, and
|
|
263
|
+
# backticks, so framework specific `after_fork` hooks (Puma, Unicorn, Resque,
|
|
264
|
+
# Spring, etc.) are no longer required.
|
|
265
|
+
def self.reopen_on_fork?
|
|
266
|
+
@reopen_on_fork != false
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Enable or disable automatic reopening of appenders after a fork.
|
|
270
|
+
#
|
|
271
|
+
# # Opt out of the automatic behavior and manage reopen manually:
|
|
272
|
+
# SemanticLogger.reopen_on_fork = false
|
|
273
|
+
def self.reopen_on_fork=(reopen_on_fork)
|
|
274
|
+
@reopen_on_fork = reopen_on_fork
|
|
213
275
|
end
|
|
214
276
|
|
|
215
277
|
# Supply a callback to be called whenever a log entry is created.
|
|
@@ -236,8 +298,8 @@ module SemanticLogger
|
|
|
236
298
|
# Note:
|
|
237
299
|
# * This callback is called within the thread of the application making the logging call.
|
|
238
300
|
# * If these callbacks are slow they will slow down the application.
|
|
239
|
-
def self.on_log(object = nil, &
|
|
240
|
-
Logger.subscribe(object, &
|
|
301
|
+
def self.on_log(object = nil, &)
|
|
302
|
+
Logger.subscribe(object, &)
|
|
241
303
|
end
|
|
242
304
|
|
|
243
305
|
# Add signal handlers for Semantic Logger
|
|
@@ -342,13 +404,13 @@ module SemanticLogger
|
|
|
342
404
|
# `logger.tagged([['first', nil], nil, ['more'], 'other'])`
|
|
343
405
|
# to the equivalent of:
|
|
344
406
|
# `logger.tagged('first', 'more', 'other')`
|
|
345
|
-
def self.tagged(*tags, &
|
|
407
|
+
def self.tagged(*tags, &)
|
|
346
408
|
return yield if tags.empty?
|
|
347
409
|
|
|
348
410
|
# Allow named tags to be passed into the logger
|
|
349
411
|
if tags.size == 1
|
|
350
412
|
tag = tags[0]
|
|
351
|
-
return tag.is_a?(Hash) ? named_tagged(tag, &
|
|
413
|
+
return tag.is_a?(Hash) ? named_tagged(tag, &) : fast_tag(tag, &)
|
|
352
414
|
end
|
|
353
415
|
|
|
354
416
|
begin
|
|
@@ -475,6 +537,27 @@ module SemanticLogger
|
|
|
475
537
|
Logger.processor.queue.size
|
|
476
538
|
end
|
|
477
539
|
|
|
540
|
+
# Returns [Hash] operational statistics for the logging pipeline.
|
|
541
|
+
#
|
|
542
|
+
# Useful for exporting Semantic Logger's own health to a monitoring system such as
|
|
543
|
+
# Prometheus, statsd, etc. The returned Hash contains:
|
|
544
|
+
#
|
|
545
|
+
# queue_size: [Integer] Number of log messages waiting on the main pipeline queue.
|
|
546
|
+
# capped: [Boolean] Whether the main queue has a maximum size.
|
|
547
|
+
# max_queue_size: [Integer] Maximum queue size, or nil when uncapped.
|
|
548
|
+
# thread_active: [Boolean] Whether the main pipeline thread is running.
|
|
549
|
+
# processed: [Integer] Cumulative number of log messages processed since startup.
|
|
550
|
+
# dropped: [Integer] Cumulative number of log messages dropped at the main queue.
|
|
551
|
+
# appenders: [Array<Hash>] Per-appender statistics. Appenders that run their own
|
|
552
|
+
# async thread report their queue_size and processed/dropped
|
|
553
|
+
# counts; appenders that log inline report `async: false`.
|
|
554
|
+
#
|
|
555
|
+
# All counters are cumulative since process startup. They are thread-safe to read and
|
|
556
|
+
# are maintained without adding any locking to the logging hot path.
|
|
557
|
+
def self.stats
|
|
558
|
+
Logger.processor.stats
|
|
559
|
+
end
|
|
560
|
+
|
|
478
561
|
# Returns the check_interval which is the number of messages between checks
|
|
479
562
|
# to determine if the appender thread is falling behind.
|
|
480
563
|
def self.lag_check_interval
|
|
@@ -516,6 +599,14 @@ module SemanticLogger
|
|
|
516
599
|
@backtrace_level = :error
|
|
517
600
|
@backtrace_level_index = Levels.index(@backtrace_level)
|
|
518
601
|
@sync = false
|
|
602
|
+
@cache_loggers = false
|
|
603
|
+
@logger_cache = nil
|
|
604
|
+
|
|
605
|
+
# Lazily initialized thread-safe cache of one Logger per Class/Module.
|
|
606
|
+
def self.logger_cache
|
|
607
|
+
@logger_cache ||= Concurrent::Map.new
|
|
608
|
+
end
|
|
609
|
+
private_class_method :logger_cache
|
|
519
610
|
|
|
520
611
|
# @formatter:off
|
|
521
612
|
module Metric
|
|
@@ -531,6 +622,7 @@ module SemanticLogger
|
|
|
531
622
|
module Test
|
|
532
623
|
autoload :CaptureLogEvents, "semantic_logger/test/capture_log_events"
|
|
533
624
|
autoload :Minitest, "semantic_logger/test/minitest"
|
|
625
|
+
autoload :RSpec, "semantic_logger/test/rspec"
|
|
534
626
|
end
|
|
535
627
|
|
|
536
628
|
if defined?(JRuby)
|
|
@@ -73,9 +73,22 @@ module SemanticLogger
|
|
|
73
73
|
super && (log.metric_only? ? metrics? : true)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
#
|
|
76
|
+
# The console stream this appender writes to, if any.
|
|
77
|
+
# Returns one of :stdout, :stderr, or nil when not writing to a console stream.
|
|
78
|
+
def console_stream
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Whether this appender is logging to stdout or stderr.
|
|
77
83
|
def console_output?
|
|
78
|
-
|
|
84
|
+
!console_stream.nil?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Whether an appender that implements #batch should be wrapped in a batch
|
|
88
|
+
# proxy automatically. Appenders that support batching but should only batch
|
|
89
|
+
# when explicitly requested (via `batch: true`) override this to return false.
|
|
90
|
+
def batch_by_default?
|
|
91
|
+
true
|
|
79
92
|
end
|
|
80
93
|
|
|
81
94
|
private
|
|
@@ -9,7 +9,28 @@ module SemanticLogger
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def log(...)
|
|
12
|
-
@monitor.synchronize
|
|
12
|
+
@monitor.synchronize do
|
|
13
|
+
@processed_count += 1
|
|
14
|
+
@appenders.log(...)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns [Hash] operational statistics for the logging pipeline.
|
|
19
|
+
#
|
|
20
|
+
# In synchronous mode there is no queue: messages are written inline on the calling
|
|
21
|
+
# thread, so queue_size is always 0 and no messages can be dropped.
|
|
22
|
+
def stats
|
|
23
|
+
@monitor.synchronize do
|
|
24
|
+
{
|
|
25
|
+
queue_size: 0,
|
|
26
|
+
capped: false,
|
|
27
|
+
max_queue_size: nil,
|
|
28
|
+
thread_active: false,
|
|
29
|
+
processed: @processed_count,
|
|
30
|
+
dropped: 0,
|
|
31
|
+
appenders: @appenders.stats
|
|
32
|
+
}
|
|
33
|
+
end
|
|
13
34
|
end
|
|
14
35
|
|
|
15
36
|
def flush
|
|
@@ -47,8 +68,9 @@ module SemanticLogger
|
|
|
47
68
|
attr_reader :appenders
|
|
48
69
|
|
|
49
70
|
def initialize(appenders = nil)
|
|
50
|
-
@monitor
|
|
51
|
-
@appenders
|
|
71
|
+
@monitor = Monitor.new
|
|
72
|
+
@appenders = appenders || Appenders.new(self.class.logger.dup)
|
|
73
|
+
@processed_count = 0
|
|
52
74
|
end
|
|
53
75
|
|
|
54
76
|
def start
|
|
@@ -57,15 +57,17 @@ module SemanticLogger
|
|
|
57
57
|
if payload_includes
|
|
58
58
|
payload_includes.each_pair do |key, expected|
|
|
59
59
|
actual = event.payload[key]
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
assert_semantic_logger_entry(event, "payload #{key}", expected, actual)
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
return unless exception_includes
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
exception_includes.each_pair do |key, expected|
|
|
67
68
|
actual = event.exception.send(key)
|
|
68
|
-
|
|
69
|
+
|
|
70
|
+
assert_semantic_logger_entry(event, "exception #{key}", expected, actual)
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
@@ -76,9 +78,11 @@ module SemanticLogger
|
|
|
76
78
|
|
|
77
79
|
case expected
|
|
78
80
|
when :nil
|
|
81
|
+
|
|
79
82
|
assert_nil actual, "Expected nil #{name} for log event: #{event.to_h.inspect}"
|
|
80
83
|
when Class
|
|
81
|
-
|
|
84
|
+
|
|
85
|
+
assert_kind_of expected, actual, lambda {
|
|
82
86
|
"Type #{expected} expected for #{name} in log event: #{event.to_h.inspect}"
|
|
83
87
|
}
|
|
84
88
|
else
|