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
|
@@ -101,7 +101,7 @@ module SemanticLogger
|
|
|
101
101
|
data[:counter] = [hash]
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
Utils.to_json(data)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
# Returns [Hash] a batch of log messages.
|
|
@@ -138,7 +138,7 @@ module SemanticLogger
|
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
Utils.to_json(data)
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
private
|
|
@@ -49,14 +49,25 @@ module SemanticLogger
|
|
|
49
49
|
# level_map: [Hash | SemanticLogger::Formatters::Syslog::LevelMap]
|
|
50
50
|
# Supply a custom map of SemanticLogger levels to syslog levels.
|
|
51
51
|
#
|
|
52
|
+
# escape_control_chars: [Boolean]
|
|
53
|
+
# Replace control characters (newlines, the ANSI escape, etc.) in
|
|
54
|
+
# untrusted log data with a printable escaped form so that they cannot
|
|
55
|
+
# forge or split syslog records.
|
|
56
|
+
# Default: true (unlike other formatters, since syslog frames records
|
|
57
|
+
# with a separator)
|
|
58
|
+
#
|
|
52
59
|
# Example:
|
|
53
60
|
# # Change the warn level to LOG_NOTICE level instead of a the default of LOG_WARNING.
|
|
54
61
|
# SemanticLogger.add_appender(appender: :syslog, level_map: {warn: ::Syslog::LOG_NOTICE})
|
|
55
|
-
def initialize(facility: ::Syslog::LOG_USER, level_map: LevelMap.new, max_size: Integer
|
|
62
|
+
def initialize(facility: ::Syslog::LOG_USER, level_map: LevelMap.new, max_size: Integer,
|
|
63
|
+
escape_control_chars: true, **args)
|
|
56
64
|
@facility = facility
|
|
57
|
-
@level_map = level_map.is_a?(LevelMap) ? level_map : LevelMap.new(level_map)
|
|
65
|
+
@level_map = level_map.is_a?(LevelMap) ? level_map : LevelMap.new(**level_map)
|
|
58
66
|
@max_size = max_size
|
|
59
|
-
|
|
67
|
+
# Syslog frames records with a separator, so embedded newlines or other
|
|
68
|
+
# control characters in untrusted log data can forge or split records.
|
|
69
|
+
# Default to escaping them, overridable for backwards compatibility.
|
|
70
|
+
super(escape_control_chars: escape_control_chars, **args)
|
|
60
71
|
end
|
|
61
72
|
|
|
62
73
|
# Time is part of the syslog packet and is not included in the formatted message.
|
|
@@ -3,6 +3,7 @@ module SemanticLogger
|
|
|
3
3
|
autoload :Base, "semantic_logger/formatters/base"
|
|
4
4
|
autoload :Color, "semantic_logger/formatters/color"
|
|
5
5
|
autoload :Default, "semantic_logger/formatters/default"
|
|
6
|
+
autoload :Ecs, "semantic_logger/formatters/ecs"
|
|
6
7
|
autoload :Json, "semantic_logger/formatters/json"
|
|
7
8
|
autoload :Raw, "semantic_logger/formatters/raw"
|
|
8
9
|
autoload :OneLine, "semantic_logger/formatters/one_line"
|
|
@@ -11,6 +12,7 @@ module SemanticLogger
|
|
|
11
12
|
autoload :Syslog, "semantic_logger/formatters/syslog"
|
|
12
13
|
autoload :Fluentd, "semantic_logger/formatters/fluentd"
|
|
13
14
|
autoload :Logfmt, "semantic_logger/formatters/logfmt"
|
|
15
|
+
autoload :Pattern, "semantic_logger/formatters/pattern"
|
|
14
16
|
autoload :SyslogCee, "semantic_logger/formatters/syslog_cee"
|
|
15
17
|
autoload :NewRelicLogs, "semantic_logger/formatters/new_relic_logs"
|
|
16
18
|
autoload :Loki, "semantic_logger/formatters/loki"
|
data/lib/semantic_logger/log.rb
CHANGED
|
@@ -129,11 +129,17 @@ module SemanticLogger
|
|
|
129
129
|
true
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
# Keys that #assign_hash may assign directly to a log attribute. Every other
|
|
133
|
+
# key is folded into the payload, preventing a supplied hash from overwriting
|
|
134
|
+
# sensitive fields such as :level, :name, or :time, and keeping this path
|
|
135
|
+
# consistent with #extract_arguments.
|
|
136
|
+
ASSIGNABLE_KEYS = (NON_PAYLOAD_KEYS + %i[payload]).freeze
|
|
137
|
+
|
|
132
138
|
# Assign known keys to self, all other keys to the payload.
|
|
133
139
|
def assign_hash(hash)
|
|
134
140
|
self.payload ||= {}
|
|
135
141
|
hash.each_pair do |key, value|
|
|
136
|
-
if respond_to?(:"#{key}=")
|
|
142
|
+
if ASSIGNABLE_KEYS.include?(key) && respond_to?(:"#{key}=")
|
|
137
143
|
public_send(:"#{key}=", value)
|
|
138
144
|
else
|
|
139
145
|
payload[key] = value
|
|
@@ -250,7 +256,7 @@ module SemanticLogger
|
|
|
250
256
|
"#{$$}:#{format("%.#{thread_name_length}s", thread_name)}#{file_name}"
|
|
251
257
|
end
|
|
252
258
|
|
|
253
|
-
CALLER_REGEXP = /^(.*):(\d+)
|
|
259
|
+
CALLER_REGEXP = /^(.*):(\d+).*/
|
|
254
260
|
|
|
255
261
|
# Extract the filename and line number from the last entry in the supplied backtrace
|
|
256
262
|
def extract_file_and_line(stack, short_name = false)
|
|
@@ -269,9 +275,17 @@ module SemanticLogger
|
|
|
269
275
|
extract_file_and_line(stack, short_name)
|
|
270
276
|
end
|
|
271
277
|
|
|
272
|
-
# Strip the standard Rails colorizing from the logged message
|
|
278
|
+
# Strip the standard Rails colorizing from the logged message.
|
|
279
|
+
#
|
|
280
|
+
# Note: This unconditionally *strips* ANSI colorization, and is used to keep
|
|
281
|
+
# terminal escape codes out of structured (JSON/Loki) output. It is distinct
|
|
282
|
+
# from Formatters::Base#escape_control_characters, which instead *escapes*
|
|
283
|
+
# (preserves) control characters and is opt-in for the text formatters.
|
|
273
284
|
def cleansed_message
|
|
274
|
-
message.to_s
|
|
285
|
+
msg = message.to_s
|
|
286
|
+
return msg.strip unless msg.include?("\e")
|
|
287
|
+
|
|
288
|
+
msg.gsub(/\e\[[\d;]*[mz]?|\e/, "").strip
|
|
275
289
|
end
|
|
276
290
|
|
|
277
291
|
# Return the payload in text form
|
|
@@ -65,9 +65,9 @@ module SemanticLogger
|
|
|
65
65
|
#
|
|
66
66
|
# Subscribers are called inline before handing off to the queue so that
|
|
67
67
|
# they can capture additional context information as needed.
|
|
68
|
-
def log(log, message = nil, progname = nil, &
|
|
68
|
+
def log(log, message = nil, progname = nil, &)
|
|
69
69
|
# Compatibility with ::Logger
|
|
70
|
-
return add(log, message, progname, &
|
|
70
|
+
return add(log, message, progname, &) unless log.is_a?(SemanticLogger::Log)
|
|
71
71
|
|
|
72
72
|
Logger.call_subscribers(log)
|
|
73
73
|
|
|
@@ -38,9 +38,9 @@ module SemanticLogger
|
|
|
38
38
|
# regular expression. All other messages will be ignored.
|
|
39
39
|
# Proc: Only include log messages where the supplied Proc returns true
|
|
40
40
|
# The Proc must return true or false.
|
|
41
|
-
def initialize(prefix: "Custom", **args, &
|
|
41
|
+
def initialize(prefix: "Custom", **args, &)
|
|
42
42
|
@prefix = prefix
|
|
43
|
-
super(**args, &
|
|
43
|
+
super(**args, &)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# Returns metric name to use.
|
|
@@ -78,10 +78,10 @@ module SemanticLogger
|
|
|
78
78
|
url: "https://ingest.signalfx.com",
|
|
79
79
|
formatter: nil,
|
|
80
80
|
**args,
|
|
81
|
-
&
|
|
81
|
+
&)
|
|
82
82
|
formatter ||= SemanticLogger::Formatters::Signalfx.new(token: token, dimensions: dimensions)
|
|
83
83
|
|
|
84
|
-
super(url: url, formatter: formatter, **args, &
|
|
84
|
+
super(url: url, formatter: formatter, **args, &)
|
|
85
85
|
|
|
86
86
|
@header["X-SF-TOKEN"] = token
|
|
87
87
|
@full_url = "#{url}/#{END_POINT}"
|
|
@@ -24,7 +24,7 @@ module SemanticLogger
|
|
|
24
24
|
# Example:
|
|
25
25
|
# SemanticLogger.add_appender(
|
|
26
26
|
# metric: :statsd,
|
|
27
|
-
# url: 'localhost:8125'
|
|
27
|
+
# url: 'udp://localhost:8125'
|
|
28
28
|
# )
|
|
29
29
|
def initialize(url: "udp://localhost:8125")
|
|
30
30
|
@url = url
|
|
@@ -47,7 +47,7 @@ module SemanticLogger
|
|
|
47
47
|
else
|
|
48
48
|
amount = (log.metric_amount || 1).round
|
|
49
49
|
if amount.negative?
|
|
50
|
-
amount.times { @statsd.decrement(metric) }
|
|
50
|
+
amount.abs.times { @statsd.decrement(metric) }
|
|
51
51
|
else
|
|
52
52
|
amount.times { @statsd.increment(metric) }
|
|
53
53
|
end
|
|
@@ -35,5 +35,26 @@ module SemanticLogger
|
|
|
35
35
|
thread
|
|
36
36
|
true
|
|
37
37
|
end
|
|
38
|
+
|
|
39
|
+
# Returns [Hash] operational statistics for the logging pipeline.
|
|
40
|
+
#
|
|
41
|
+
# queue_size: [Integer] Number of log messages waiting on the main pipeline queue.
|
|
42
|
+
# capped: [Boolean] Whether the main queue has a maximum size.
|
|
43
|
+
# max_queue_size: [Integer] Maximum queue size, or nil when uncapped.
|
|
44
|
+
# thread_active: [Boolean] Whether the main pipeline thread is running.
|
|
45
|
+
# processed: [Integer] Cumulative number of log messages processed since startup.
|
|
46
|
+
# dropped: [Integer] Cumulative number of log messages dropped at the main queue.
|
|
47
|
+
# appenders: [Array<Hash>] Per-appender statistics, see Appenders#stats.
|
|
48
|
+
def stats
|
|
49
|
+
{
|
|
50
|
+
queue_size: queue.size,
|
|
51
|
+
capped: capped?,
|
|
52
|
+
max_queue_size: capped? ? max_queue_size : nil,
|
|
53
|
+
thread_active: active? || false,
|
|
54
|
+
processed: processed_count,
|
|
55
|
+
dropped: dropped_count,
|
|
56
|
+
appenders: appenders.stats
|
|
57
|
+
}
|
|
58
|
+
end
|
|
38
59
|
end
|
|
39
60
|
end
|
|
@@ -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
|