semantic_logger 4.18.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 +10 -3
- 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/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 +2 -2
- data/lib/semantic_logger/appender/open_telemetry.rb +8 -6
- 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 +2 -2
- 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 +100 -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/open_telemetry.rb +15 -5
- 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 +18 -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 +21 -0
- 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 +17 -8
- data/lib/semantic_logger/appender/async_batch.rb +0 -93
|
@@ -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
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
require "rspec/expectations"
|
|
2
|
+
|
|
3
|
+
module SemanticLogger
|
|
4
|
+
module Test
|
|
5
|
+
# RSpec matchers and helpers for asserting on Semantic Logger events.
|
|
6
|
+
#
|
|
7
|
+
# These mirror the Minitest helpers in SemanticLogger::Test::Minitest.
|
|
8
|
+
#
|
|
9
|
+
# Enable them once, in spec_helper.rb:
|
|
10
|
+
#
|
|
11
|
+
# require "semantic_logger/test/rspec"
|
|
12
|
+
#
|
|
13
|
+
# RSpec.configure do |config|
|
|
14
|
+
# config.include SemanticLogger::Test::RSpec
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Capture the events emitted whilst running a block, then assert on them:
|
|
18
|
+
#
|
|
19
|
+
# events = capture_semantic_logger_events { User.new.enable! }
|
|
20
|
+
# expect(events.first).to be_a_semantic_logger_event(level: :info, message: "User enabled")
|
|
21
|
+
#
|
|
22
|
+
# Or assert directly that a block logs a matching event:
|
|
23
|
+
#
|
|
24
|
+
# expect { User.new.enable! }.to(
|
|
25
|
+
# log_semantic_logger_event(level: :info, message: "User enabled")
|
|
26
|
+
# )
|
|
27
|
+
module RSpec
|
|
28
|
+
# Attributes that may be asserted on a single log event. The keys map
|
|
29
|
+
# directly onto SemanticLogger::Log attributes, except for the
|
|
30
|
+
# *_includes variants which assert a partial (substring / subset) match.
|
|
31
|
+
EVENT_ATTRIBUTES = %i[
|
|
32
|
+
level name message thread_name tags named_tags context
|
|
33
|
+
metric metric_amount dimensions level_index duration time
|
|
34
|
+
exception backtrace payload
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# Matches a single SemanticLogger::Log against a set of expectations.
|
|
38
|
+
#
|
|
39
|
+
# An expected value may be:
|
|
40
|
+
# * a Class - the actual value must be a kind_of that class
|
|
41
|
+
# * :nil - the actual value must be nil
|
|
42
|
+
# * any value - compared with ==
|
|
43
|
+
class EventMatcher
|
|
44
|
+
include ::RSpec::Matchers::Composable
|
|
45
|
+
|
|
46
|
+
def initialize(message_includes: nil, payload_includes: nil, exception_includes: nil, **expected)
|
|
47
|
+
unknown = expected.keys - EVENT_ATTRIBUTES
|
|
48
|
+
raise ArgumentError, "Unknown log event attribute(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
49
|
+
|
|
50
|
+
@expected = expected
|
|
51
|
+
@message_includes = message_includes
|
|
52
|
+
@payload_includes = payload_includes
|
|
53
|
+
@exception_includes = exception_includes
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def matches?(event)
|
|
57
|
+
@event = event
|
|
58
|
+
return failed("no log event") unless event
|
|
59
|
+
|
|
60
|
+
@expected.each_pair do |name, expected|
|
|
61
|
+
actual = event.public_send(name)
|
|
62
|
+
return failed(mismatch(name, expected, actual)) unless attribute_matches?(expected, actual)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return failed(includes_failure(:message, @message_includes)) unless message_includes_matches?
|
|
66
|
+
return failed(includes_failure(:payload, @payload_includes)) unless payload_includes_matches?
|
|
67
|
+
return failed(includes_failure(:exception, @exception_includes)) unless exception_includes_matches?
|
|
68
|
+
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def description
|
|
73
|
+
"be a semantic logger event matching #{full_expectations.inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def failure_message
|
|
77
|
+
"expected log event to #{description}, but #{@failure}.\n" \
|
|
78
|
+
"Log event: #{event_inspect}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def failure_message_when_negated
|
|
82
|
+
"expected log event not to #{description}.\nLog event: #{event_inspect}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def failed(reason)
|
|
88
|
+
@failure = reason
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def attribute_matches?(expected, actual)
|
|
93
|
+
case expected
|
|
94
|
+
when :nil
|
|
95
|
+
actual.nil?
|
|
96
|
+
when Class
|
|
97
|
+
actual.is_a?(expected)
|
|
98
|
+
else
|
|
99
|
+
expected == actual
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def message_includes_matches?
|
|
104
|
+
return true unless @message_includes
|
|
105
|
+
|
|
106
|
+
@event.message&.include?(@message_includes)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def payload_includes_matches?
|
|
110
|
+
return true unless @payload_includes
|
|
111
|
+
|
|
112
|
+
payload = @event.payload || {}
|
|
113
|
+
@payload_includes.all? { |key, value| payload[key] == value }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def exception_includes_matches?
|
|
117
|
+
return true unless @exception_includes
|
|
118
|
+
|
|
119
|
+
exception = @event.exception
|
|
120
|
+
return false unless exception
|
|
121
|
+
|
|
122
|
+
@exception_includes.all? { |key, value| exception.public_send(key) == value }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def mismatch(name, expected, actual)
|
|
126
|
+
"#{name} was #{actual.inspect} (expected #{expected.inspect})"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def includes_failure(name, expected)
|
|
130
|
+
"#{name} did not include #{expected.inspect}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def full_expectations
|
|
134
|
+
@expected.merge(
|
|
135
|
+
{
|
|
136
|
+
message_includes: @message_includes,
|
|
137
|
+
payload_includes: @payload_includes,
|
|
138
|
+
exception_includes: @exception_includes
|
|
139
|
+
}.compact
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def event_inspect
|
|
144
|
+
@event.respond_to?(:to_h) ? @event.to_h.inspect : @event.inspect
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Matches a block, asserting that it emits at least one log event that
|
|
149
|
+
# matches the supplied expectations.
|
|
150
|
+
class LogEventMatcher
|
|
151
|
+
include ::RSpec::Matchers::Composable
|
|
152
|
+
|
|
153
|
+
def initialize(capture, on:, expected:)
|
|
154
|
+
@capture = capture
|
|
155
|
+
@on = on
|
|
156
|
+
@matcher = EventMatcher.new(**expected)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def matches?(block)
|
|
160
|
+
@events = @capture.call(@on, &block)
|
|
161
|
+
@events.any? { |event| @matcher.matches?(event) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def supports_block_expectations?
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def description
|
|
169
|
+
"log a semantic logger event matching #{@matcher.description}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def failure_message
|
|
173
|
+
"expected the block to #{description}.\n" \
|
|
174
|
+
"Captured #{@events.size} event(s):\n#{captured_inspect}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def failure_message_when_negated
|
|
178
|
+
"expected the block not to #{description}, but it did.\n" \
|
|
179
|
+
"Captured #{@events.size} event(s):\n#{captured_inspect}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def captured_inspect
|
|
185
|
+
@events.map { |event| " #{event.to_h.inspect}" }.join("\n")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Returns [Array<SemanticLogger::Log>] the log events captured whilst
|
|
190
|
+
# running the supplied block.
|
|
191
|
+
#
|
|
192
|
+
# Notes:
|
|
193
|
+
# - All log events are captured regardless of the global default log level.
|
|
194
|
+
# - Pass a class to capture only events logged through that class's logger.
|
|
195
|
+
# Otherwise every log event in the process is captured for the duration
|
|
196
|
+
# of the block.
|
|
197
|
+
def capture_semantic_logger_events(klass = nil, silence: :trace, &block)
|
|
198
|
+
logger = SemanticLogger::Test::CaptureLogEvents.new
|
|
199
|
+
|
|
200
|
+
if klass
|
|
201
|
+
allow(klass).to receive(:logger).and_return(logger)
|
|
202
|
+
block.call
|
|
203
|
+
elsif silence
|
|
204
|
+
SemanticLogger.silence(silence) do
|
|
205
|
+
stub_processor(logger, &block)
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
stub_processor(logger, &block)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
logger.events
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Matcher for a single captured log event.
|
|
215
|
+
#
|
|
216
|
+
# expect(events.first).to be_a_semantic_logger_event(level: :info, message: "Hi")
|
|
217
|
+
def be_a_semantic_logger_event(**expected)
|
|
218
|
+
EventMatcher.new(**expected)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Composable alias, for use inside other matchers:
|
|
222
|
+
#
|
|
223
|
+
# expect(events).to include(a_semantic_logger_event(message: "Hi"))
|
|
224
|
+
def a_semantic_logger_event(**expected)
|
|
225
|
+
EventMatcher.new(**expected)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Block matcher asserting that the block logs a matching event.
|
|
229
|
+
#
|
|
230
|
+
# expect { User.new.enable! }.to(
|
|
231
|
+
# log_semantic_logger_event(level: :info, message: "User enabled")
|
|
232
|
+
# )
|
|
233
|
+
#
|
|
234
|
+
# Pass `on:` to capture only one class's events.
|
|
235
|
+
def log_semantic_logger_event(on: nil, **expected)
|
|
236
|
+
LogEventMatcher.new(method(:capture_semantic_logger_events), on: on, expected: expected)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
def stub_processor(logger)
|
|
242
|
+
allow(SemanticLogger::Logger).to receive(:processor).and_return(logger)
|
|
243
|
+
yield
|
|
244
|
+
ensure
|
|
245
|
+
allow(SemanticLogger::Logger).to receive(:processor).and_call_original
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
module SemanticLogger
|
|
2
4
|
# Internal-use only utility functions for Semantic Logger.
|
|
3
5
|
# Not intended for public use.
|
|
4
6
|
module Utils
|
|
5
7
|
def self.constantize_symbol(symbol, namespace = "SemanticLogger::Appender")
|
|
6
8
|
klass = "#{namespace}::#{camelize(symbol.to_s)}"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
constant =
|
|
10
|
+
begin
|
|
11
|
+
Object.const_get(klass)
|
|
12
|
+
rescue NameError
|
|
13
|
+
raise(ArgumentError,
|
|
14
|
+
"Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}. Looking for: #{klass}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The resolved constant is instantiated by the caller, so ensure it is
|
|
18
|
+
# actually a class within the expected namespace rather than some other
|
|
19
|
+
# constant that happens to share the name.
|
|
20
|
+
unless constant.is_a?(Class)
|
|
10
21
|
raise(ArgumentError,
|
|
11
|
-
"Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}.
|
|
22
|
+
"Could not convert symbol: #{symbol.inspect} to a class in: #{namespace}. #{klass} is not a class.")
|
|
12
23
|
end
|
|
24
|
+
|
|
25
|
+
constant
|
|
13
26
|
end
|
|
14
27
|
|
|
15
28
|
# Borrow from Rails, when not running Rails
|
|
@@ -76,5 +89,71 @@ module SemanticLogger
|
|
|
76
89
|
def self.strip_path?(path)
|
|
77
90
|
strip_paths.any? { |exclude| path.start_with?(exclude) }
|
|
78
91
|
end
|
|
92
|
+
|
|
93
|
+
# Serializes the value to JSON, repairing invalid UTF-8 only when necessary.
|
|
94
|
+
#
|
|
95
|
+
# Non UTF-8 data appears in well under 1% of log events, so it is wasteful to
|
|
96
|
+
# walk and reallocate the entire structure (see .encode_utf8) on every call.
|
|
97
|
+
# Instead this attempts `.to_json` directly and only falls back to cleansing
|
|
98
|
+
# when serialization fails because of an encoding problem.
|
|
99
|
+
#
|
|
100
|
+
# The exception raised for non UTF-8 data depends on the json gem version:
|
|
101
|
+
# older versions raise Encoding::UndefinedConversionError (an EncodingError),
|
|
102
|
+
# newer versions wrap it as JSON::GeneratorError, so both are rescued. The
|
|
103
|
+
# retry is attempted only once: if it still fails (for example a
|
|
104
|
+
# JSON::GeneratorError caused by something other than encoding, such as NaN),
|
|
105
|
+
# the error propagates unchanged rather than being swallowed.
|
|
106
|
+
def self.to_json(value)
|
|
107
|
+
value.to_json
|
|
108
|
+
rescue JSON::GeneratorError, EncodingError
|
|
109
|
+
encode_utf8(value).to_json
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns a copy of the supplied value with every String converted to valid UTF-8.
|
|
113
|
+
#
|
|
114
|
+
# Recurses through Hash and Array structures, cleansing both keys and values.
|
|
115
|
+
# Strings that are already valid UTF-8 are returned unchanged (the common case),
|
|
116
|
+
# so the fast path allocates nothing. Any other value (Symbol, Numeric, Time, nil,
|
|
117
|
+
# ...) is returned as-is.
|
|
118
|
+
#
|
|
119
|
+
# Used by .to_json on the rare failing path, and directly by formatters that
|
|
120
|
+
# serialize per value or emit to a non-JSON sink (where a single `.to_json`
|
|
121
|
+
# rescue boundary cannot catch an intermediate failure).
|
|
122
|
+
def self.encode_utf8(value)
|
|
123
|
+
case value
|
|
124
|
+
when String
|
|
125
|
+
encode_utf8_string(value)
|
|
126
|
+
when Hash
|
|
127
|
+
value.each_with_object({}) do |(key, val), hash|
|
|
128
|
+
hash[encode_utf8(key)] = encode_utf8(val)
|
|
129
|
+
end
|
|
130
|
+
when Array
|
|
131
|
+
value.map { |element| encode_utf8(element) }
|
|
132
|
+
else
|
|
133
|
+
value
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Options used when transcoding a string to UTF-8.
|
|
138
|
+
# Invalid byte sequences and characters that cannot be represented in UTF-8 are
|
|
139
|
+
# dropped rather than substituted, matching the preference in issue #180.
|
|
140
|
+
ENCODE_UTF8_OPTIONS = {invalid: :replace, undef: :replace, replace: "".freeze}.freeze
|
|
141
|
+
|
|
142
|
+
# Returns the string converted to valid UTF-8, dropping any invalid bytes.
|
|
143
|
+
def self.encode_utf8_string(string)
|
|
144
|
+
return string if string.encoding == Encoding::UTF_8 && string.valid_encoding?
|
|
145
|
+
|
|
146
|
+
if string.encoding == Encoding::UTF_8
|
|
147
|
+
# Correctly tagged as UTF-8 but contains invalid byte sequences.
|
|
148
|
+
string.scrub("")
|
|
149
|
+
else
|
|
150
|
+
# Different encoding (e.g. ASCII-8BIT / Latin-1): transcode into UTF-8.
|
|
151
|
+
string.encode(Encoding::UTF_8, **ENCODE_UTF8_OPTIONS)
|
|
152
|
+
end
|
|
153
|
+
rescue EncodingError
|
|
154
|
+
# Last resort for encodings without a converter to UTF-8: reinterpret the
|
|
155
|
+
# raw bytes as UTF-8 and drop anything invalid. Logging must never raise.
|
|
156
|
+
string.dup.force_encoding(Encoding::UTF_8).scrub("")
|
|
157
|
+
end
|
|
79
158
|
end
|
|
80
159
|
end
|
data/lib/semantic_logger.rb
CHANGED
|
@@ -12,12 +12,21 @@ require "semantic_logger/loggable"
|
|
|
12
12
|
require "semantic_logger/concerns/compatibility"
|
|
13
13
|
require "semantic_logger/appender"
|
|
14
14
|
require "semantic_logger/appenders"
|
|
15
|
+
require "semantic_logger/queue_processor"
|
|
15
16
|
require "semantic_logger/processor"
|
|
16
17
|
require "semantic_logger/sync_processor"
|
|
17
18
|
require "semantic_logger/logger"
|
|
18
19
|
require "semantic_logger/debug_as_trace_logger"
|
|
19
20
|
require "semantic_logger/semantic_logger"
|
|
20
21
|
|
|
22
|
+
# Automatically reopen appenders in the child process after a fork.
|
|
23
|
+
# Enabled by default; opt out with `SemanticLogger.reopen_on_fork = false`.
|
|
24
|
+
# Skipped on platforms without `Process._fork` (e.g. JRuby), which cannot fork.
|
|
25
|
+
if Process.respond_to?(:_fork)
|
|
26
|
+
require "semantic_logger/core_ext/process"
|
|
27
|
+
Process.singleton_class.prepend(SemanticLogger::CoreExt::Process)
|
|
28
|
+
end
|
|
29
|
+
|
|
21
30
|
# Flush all appenders at exit, waiting for outstanding messages on the queue
|
|
22
31
|
# to be written first.
|
|
23
32
|
at_exit do
|