semantic_logger 5.0.0 → 5.2.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 +20 -10
- data/Rakefile +40 -0
- data/docs/api.md +506 -0
- data/docs/appenders.md +1074 -0
- data/docs/config.md +707 -0
- data/docs/index.md +223 -0
- data/docs/log.md +116 -0
- data/docs/metrics.md +171 -0
- data/docs/operations.md +446 -0
- data/docs/rails.md +890 -0
- data/docs/security.md +286 -0
- data/docs/testing.md +287 -0
- data/docs/upgrading.md +697 -0
- data/lib/semantic_logger/appender/async.rb +18 -5
- data/lib/semantic_logger/appender/bugsnag.rb +1 -1
- data/lib/semantic_logger/appender/cloudwatch_logs.rb +126 -29
- data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
- data/lib/semantic_logger/appender/elasticsearch_base.rb +48 -23
- data/lib/semantic_logger/appender/elasticsearch_http.rb +15 -8
- data/lib/semantic_logger/appender/file.rb +141 -28
- data/lib/semantic_logger/appender/graylog.rb +22 -6
- data/lib/semantic_logger/appender/honeybadger.rb +1 -1
- data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
- data/lib/semantic_logger/appender/http.rb +70 -11
- data/lib/semantic_logger/appender/io.rb +1 -1
- data/lib/semantic_logger/appender/kafka.rb +26 -12
- data/lib/semantic_logger/appender/loki.rb +13 -3
- data/lib/semantic_logger/appender/mongodb.rb +20 -1
- data/lib/semantic_logger/appender/new_relic.rb +7 -1
- data/lib/semantic_logger/appender/new_relic_logs.rb +11 -12
- data/lib/semantic_logger/appender/open_telemetry.rb +4 -3
- data/lib/semantic_logger/appender/opensearch.rb +2 -1
- data/lib/semantic_logger/appender/rabbitmq.rb +10 -4
- data/lib/semantic_logger/appender/sentry.rb +6 -1
- data/lib/semantic_logger/appender/sentry_ruby.rb +91 -17
- data/lib/semantic_logger/appender/splunk.rb +10 -1
- data/lib/semantic_logger/appender/syslog.rb +66 -25
- data/lib/semantic_logger/appender/tcp.rb +10 -4
- data/lib/semantic_logger/appender/udp.rb +11 -1
- data/lib/semantic_logger/appender/wrapper.rb +3 -2
- data/lib/semantic_logger/appender.rb +6 -3
- data/lib/semantic_logger/appenders.rb +11 -2
- data/lib/semantic_logger/base.rb +82 -32
- data/lib/semantic_logger/exception_details.rb +50 -0
- data/lib/semantic_logger/formatters/base.rb +52 -6
- data/lib/semantic_logger/formatters/color.rb +22 -3
- data/lib/semantic_logger/formatters/default.rb +28 -6
- data/lib/semantic_logger/formatters/logfmt.rb +155 -21
- data/lib/semantic_logger/formatters/loki.rb +26 -22
- data/lib/semantic_logger/formatters/one_line.rb +67 -3
- data/lib/semantic_logger/formatters/open_telemetry.rb +16 -17
- data/lib/semantic_logger/formatters/pattern.rb +11 -5
- data/lib/semantic_logger/formatters/raw.rb +4 -5
- data/lib/semantic_logger/formatters/syslog.rb +110 -17
- data/lib/semantic_logger/formatters/syslog_cee.rb +29 -15
- data/lib/semantic_logger/levels.rb +10 -1
- data/lib/semantic_logger/log.rb +183 -37
- data/lib/semantic_logger/loggable.rb +26 -3
- data/lib/semantic_logger/metric/new_relic.rb +1 -1
- data/lib/semantic_logger/metric/signalfx.rb +12 -8
- data/lib/semantic_logger/metric/statsd.rb +15 -7
- data/lib/semantic_logger/queue_processor/request.rb +55 -0
- data/lib/semantic_logger/queue_processor/throttled_counter.rb +46 -0
- data/lib/semantic_logger/queue_processor.rb +170 -68
- data/lib/semantic_logger/reporters/minitest.rb +4 -4
- data/lib/semantic_logger/semantic_logger.rb +15 -2
- data/lib/semantic_logger/subscriber.rb +96 -10
- data/lib/semantic_logger/sync_processor.rb +11 -0
- data/lib/semantic_logger/test/capture_log_events.rb +9 -6
- data/lib/semantic_logger/utils.rb +244 -16
- data/lib/semantic_logger/version.rb +1 -1
- data/lib/semantic_logger.rb +1 -1
- metadata +33 -6
- data/lib/semantic_logger/core_ext/thread.rb +0 -30
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
module SemanticLogger
|
|
9
9
|
module Metric
|
|
10
10
|
class Signalfx < SemanticLogger::Appender::Http
|
|
11
|
-
attr_reader :
|
|
11
|
+
attr_reader :full_path
|
|
12
12
|
|
|
13
13
|
END_POINT = "v2/datapoint".freeze
|
|
14
14
|
|
|
@@ -66,8 +66,10 @@ module SemanticLogger
|
|
|
66
66
|
# logger.info(metric: 'Common/User/authorize', duration: 1.4)
|
|
67
67
|
#
|
|
68
68
|
# Then it is translated into the following 2 log entries under the covers:
|
|
69
|
-
# logger.info(metric: 'Application.average', metric_amount: 1.4,
|
|
70
|
-
#
|
|
69
|
+
# logger.info(metric: 'Application.average', metric_amount: 1.4,
|
|
70
|
+
# dimensions: {class: 'Common::User', action: 'authorize'})
|
|
71
|
+
# logger.info(metric: 'Application.counter', metric_amount: 1,
|
|
72
|
+
# dimensions: {class: 'Common::User', action: 'authorize'})
|
|
71
73
|
#
|
|
72
74
|
# Similarly with a measure block which automatically supplies the duration:
|
|
73
75
|
# logger.measure_info(metric: 'Common/User/authorize') do
|
|
@@ -84,20 +86,22 @@ module SemanticLogger
|
|
|
84
86
|
super(url: url, formatter: formatter, **args, &)
|
|
85
87
|
|
|
86
88
|
@header["X-SF-TOKEN"] = token
|
|
87
|
-
|
|
89
|
+
# Built from the parsed path, not the supplied url, so that a query string in the
|
|
90
|
+
# url stays at the end of the request uri. See Appender::Http#uri_with_query.
|
|
91
|
+
@full_path = "#{@path.end_with?('/') ? @path : "#{@path}/"}#{END_POINT}"
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
def log(log)
|
|
91
|
-
message =
|
|
95
|
+
message = format_log(log)
|
|
92
96
|
logger.trace(message)
|
|
93
|
-
post(message,
|
|
97
|
+
post(message, full_path)
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
# Logs in batches
|
|
97
101
|
def batch(logs)
|
|
98
|
-
message =
|
|
102
|
+
message = format_batch(logs)
|
|
99
103
|
logger.trace(message)
|
|
100
|
-
post(message,
|
|
104
|
+
post(message, full_path)
|
|
101
105
|
end
|
|
102
106
|
|
|
103
107
|
# Only forward log entries that contain metrics.
|
|
@@ -32,7 +32,7 @@ module SemanticLogger
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def reopen
|
|
35
|
-
uri =
|
|
35
|
+
uri = Utils.parse_url(@url)
|
|
36
36
|
raise('Statsd only supports udp. Example: "udp://localhost:8125"') if uri.scheme != "udp"
|
|
37
37
|
|
|
38
38
|
@statsd = ::Statsd.new(uri.host, uri.port)
|
|
@@ -45,12 +45,15 @@ module SemanticLogger
|
|
|
45
45
|
if (duration = log.duration)
|
|
46
46
|
@statsd.timing(metric, duration)
|
|
47
47
|
else
|
|
48
|
-
amount =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
amount = log.metric_amount || 1
|
|
49
|
+
# Infinity and NaN cannot be counted, whether a Float or a BigDecimal.
|
|
50
|
+
return if amount.respond_to?(:finite?) && !amount.finite?
|
|
51
|
+
|
|
52
|
+
amount = amount.round
|
|
53
|
+
# A single counter packet, whatever the size of the amount. Incrementing once per unit
|
|
54
|
+
# would let a large amount, such as one supplied by a user, flood the network and
|
|
55
|
+
# stall the logging thread.
|
|
56
|
+
@statsd.count(metric, amount) unless amount.zero?
|
|
54
57
|
end
|
|
55
58
|
end
|
|
56
59
|
|
|
@@ -59,6 +62,11 @@ module SemanticLogger
|
|
|
59
62
|
# Does not support metrics with dimensions.
|
|
60
63
|
log.metric && !log.dimensions && meets_log_level?(log) && !filtered?(log)
|
|
61
64
|
end
|
|
65
|
+
|
|
66
|
+
# The url without any credentials. See Subscriber#inspect.
|
|
67
|
+
def inspect_details
|
|
68
|
+
"url=#{Utils.redacted_url(url).inspect}"
|
|
69
|
+
end
|
|
62
70
|
end
|
|
63
71
|
end
|
|
64
72
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module SemanticLogger
|
|
2
|
+
class QueueProcessor
|
|
3
|
+
# Internal: a command, such as :flush or :close, submitted on the queue for the worker thread
|
|
4
|
+
# to run, while the submitting thread waits for the reply.
|
|
5
|
+
#
|
|
6
|
+
# It knows its command and how its reply gets back to the submitting thread, and makes sure
|
|
7
|
+
# that the submitting thread is never left waiting on a reply that cannot come.
|
|
8
|
+
class Request
|
|
9
|
+
attr_reader :command
|
|
10
|
+
|
|
11
|
+
def initialize(command)
|
|
12
|
+
@command = command
|
|
13
|
+
@replies = Queue.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Called by the worker thread: run the block for this request, then reply with true, or
|
|
17
|
+
# with false when the block raised. Replies either way.
|
|
18
|
+
def reply
|
|
19
|
+
done = false
|
|
20
|
+
yield
|
|
21
|
+
done = true
|
|
22
|
+
ensure
|
|
23
|
+
@replies.push(done)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Called by the worker thread: run this command on the appender, then reply. Returns false
|
|
27
|
+
# once message processing must stop (:close).
|
|
28
|
+
def run(appender)
|
|
29
|
+
case command
|
|
30
|
+
when :flush
|
|
31
|
+
reply { appender.flush }
|
|
32
|
+
when :close
|
|
33
|
+
reply { appender.close }
|
|
34
|
+
return false
|
|
35
|
+
else
|
|
36
|
+
reply { appender.logger.warn "Async: Appender thread: Ignoring unknown command: #{command}" }
|
|
37
|
+
end
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Called by the submitting thread: wait for the reply from the worker thread.
|
|
42
|
+
#
|
|
43
|
+
# The block returns whether a worker thread that can still reply is running. Returns the
|
|
44
|
+
# reply, or false once none is, for example once the worker gave up after
|
|
45
|
+
# async_max_retries, or was replaced after a fork.
|
|
46
|
+
def wait
|
|
47
|
+
loop do
|
|
48
|
+
reply = @replies.pop(timeout: 1)
|
|
49
|
+
return reply unless reply.nil?
|
|
50
|
+
return @replies.pop(timeout: 0) || false unless yield
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module SemanticLogger
|
|
2
|
+
class QueueProcessor
|
|
3
|
+
# Internal: counts occurrences of a problem, such as a dropped log message, and has it
|
|
4
|
+
# reported at most once per interval instead of once per occurrence.
|
|
5
|
+
#
|
|
6
|
+
# It knows the total number of occurrences, how many there have been since the previous
|
|
7
|
+
# report, and when that report was made. What to report, and where, is up to the caller.
|
|
8
|
+
#
|
|
9
|
+
# Thread-safe: occurrences may be recorded from any thread.
|
|
10
|
+
class ThrottledCounter
|
|
11
|
+
# Returns [Integer] the total number of occurrences recorded.
|
|
12
|
+
attr_reader :total
|
|
13
|
+
|
|
14
|
+
# Parameters:
|
|
15
|
+
# reported_at: [Time|nil]
|
|
16
|
+
# When counting starts, so that the first report is due one interval later.
|
|
17
|
+
# nil: Report the first occurrence straight away.
|
|
18
|
+
# Default: Time.now
|
|
19
|
+
def initialize(reported_at: Time.now)
|
|
20
|
+
@reported_at = reported_at
|
|
21
|
+
@total = 0
|
|
22
|
+
@unreported = 0
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Record one or more occurrences.
|
|
27
|
+
#
|
|
28
|
+
# When the previous report was at least `interval` seconds ago, yields the number of
|
|
29
|
+
# occurrences since then (including these) and the seconds elapsed since that report (nil
|
|
30
|
+
# when there has been no report yet), then starts counting afresh for the next report.
|
|
31
|
+
def record(count = 1, interval:)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@total += count
|
|
34
|
+
@unreported += count
|
|
35
|
+
now = Time.now
|
|
36
|
+
elapsed = @reported_at && (now - @reported_at)
|
|
37
|
+
return if elapsed && elapsed < interval
|
|
38
|
+
|
|
39
|
+
yield(@unreported, elapsed)
|
|
40
|
+
@unreported = 0
|
|
41
|
+
@reported_at = now
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require "semantic_logger/queue_processor/request"
|
|
2
|
+
require "semantic_logger/queue_processor/throttled_counter"
|
|
3
|
+
|
|
1
4
|
module SemanticLogger
|
|
2
5
|
# Internal class that processes log messages from a queue on a separate thread.
|
|
3
6
|
#
|
|
@@ -12,9 +15,9 @@ module SemanticLogger
|
|
|
12
15
|
# batch_seconds have elapsed since the previous batch.
|
|
13
16
|
class QueueProcessor
|
|
14
17
|
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, :
|
|
17
|
-
:processed_count, :
|
|
18
|
+
:batch_size, :batch_seconds, :non_blocking
|
|
19
|
+
attr_reader :appender, :queue, :max_queue_size, :signal,
|
|
20
|
+
:processed_count, :async_max_retries, :retry_count
|
|
18
21
|
|
|
19
22
|
# Create a new processor and start its worker thread.
|
|
20
23
|
def self.start(**args)
|
|
@@ -63,7 +66,8 @@ module SemanticLogger
|
|
|
63
66
|
#
|
|
64
67
|
# dropped_message_report_seconds: [Integer]
|
|
65
68
|
# When non_blocking is enabled, log the count of dropped messages to the internal logger
|
|
66
|
-
# at most once every this number of seconds.
|
|
69
|
+
# at most once every this number of seconds. Also limits how often messages discarded
|
|
70
|
+
# because their content could not be written are reported, after the first.
|
|
67
71
|
# Default: 30
|
|
68
72
|
#
|
|
69
73
|
# async_max_retries: [Integer]
|
|
@@ -99,11 +103,9 @@ module SemanticLogger
|
|
|
99
103
|
@thread = nil
|
|
100
104
|
# Only batch mode parks the worker on the signal; streaming mode never touches it.
|
|
101
105
|
@signal = Concurrent::Event.new if batch
|
|
102
|
-
@
|
|
103
|
-
@
|
|
104
|
-
@dropped_message_mutex = Mutex.new
|
|
106
|
+
@dropped = ThrottledCounter.new
|
|
107
|
+
@failed = ThrottledCounter.new(reported_at: nil)
|
|
105
108
|
@processed_count = 0
|
|
106
|
-
@dropped_count = 0
|
|
107
109
|
create_queue
|
|
108
110
|
|
|
109
111
|
return unless batch? && !appender.respond_to?(:batch)
|
|
@@ -112,9 +114,7 @@ module SemanticLogger
|
|
|
112
114
|
end
|
|
113
115
|
|
|
114
116
|
# Internal logger used to report problems encountered while processing log messages.
|
|
115
|
-
def logger
|
|
116
|
-
appender.logger
|
|
117
|
-
end
|
|
117
|
+
def logger = appender.logger
|
|
118
118
|
|
|
119
119
|
# Returns [Thread] the worker thread.
|
|
120
120
|
#
|
|
@@ -126,33 +126,39 @@ module SemanticLogger
|
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
# Returns [true|false] whether the worker thread is running.
|
|
129
|
-
def active?
|
|
130
|
-
@thread&.alive?
|
|
131
|
-
end
|
|
129
|
+
def active? = @thread&.alive?
|
|
132
130
|
|
|
133
131
|
# Returns [true|false] whether the queue has a capped size.
|
|
134
|
-
def capped?
|
|
135
|
-
@capped
|
|
136
|
-
end
|
|
132
|
+
def capped? = @capped
|
|
137
133
|
|
|
138
134
|
# Returns [true|false] whether messages are dropped instead of blocking when the queue is full.
|
|
139
135
|
# Only a capped queue can drop messages.
|
|
140
|
-
def non_blocking?
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
def non_blocking? = @non_blocking && capped?
|
|
137
|
+
|
|
138
|
+
# Returns [Integer] the number of log messages dropped because the queue was full.
|
|
139
|
+
def dropped_count = @dropped.total
|
|
140
|
+
|
|
141
|
+
# Returns [Integer] the number of log messages discarded because their content could not be
|
|
142
|
+
# written (see Subscriber#message_error?).
|
|
143
|
+
def failed_count = @failed.total
|
|
143
144
|
|
|
144
145
|
# Returns [true|false] whether messages are processed in batches.
|
|
145
|
-
def batch?
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
def batch? = @batch
|
|
147
|
+
|
|
148
|
+
# Returns [true|false] whether the worker thread gave up after an error, so that queued
|
|
149
|
+
# messages are no longer being processed.
|
|
150
|
+
def stopped? = @stopped == true
|
|
148
151
|
|
|
149
152
|
# Add a log message to the queue for processing.
|
|
150
153
|
#
|
|
151
154
|
# When non-blocking and the queue is full, the message is dropped instead of blocking the
|
|
152
155
|
# calling thread, and the count of dropped messages is reported periodically.
|
|
156
|
+
#
|
|
157
|
+
# Once the worker thread has stopped for good (see async_max_retries), nothing will empty the
|
|
158
|
+
# queue, so messages are also dropped instead of blocking the calling thread forever.
|
|
153
159
|
def log(log)
|
|
154
160
|
enqueued = true
|
|
155
|
-
if non_blocking?
|
|
161
|
+
if non_blocking? || stopped?
|
|
156
162
|
begin
|
|
157
163
|
queue.push(log, true)
|
|
158
164
|
rescue ThreadError
|
|
@@ -195,14 +201,26 @@ module SemanticLogger
|
|
|
195
201
|
|
|
196
202
|
private
|
|
197
203
|
|
|
198
|
-
# Start the worker thread
|
|
204
|
+
# Start the worker thread.
|
|
199
205
|
def spawn_worker
|
|
206
|
+
@stopped = false
|
|
207
|
+
@stack_overflow = nil
|
|
200
208
|
Thread.new do
|
|
201
|
-
|
|
209
|
+
name_worker
|
|
202
210
|
process
|
|
203
211
|
end
|
|
204
212
|
end
|
|
205
213
|
|
|
214
|
+
# Name the worker thread after its appender, such as "SL File", short enough for tools that
|
|
215
|
+
# list threads, such as `top -H`, to tell the workers apart: Linux keeps only the first 15 bytes.
|
|
216
|
+
# The name is only a label, so when Ruby rejects it, for example for a class given a temporary
|
|
217
|
+
# name with a NUL byte, the thread carries on without one.
|
|
218
|
+
def name_worker
|
|
219
|
+
Thread.current.name = ["SL", appender.class.name&.split("::")&.last].compact.join(" ")
|
|
220
|
+
rescue StandardError
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
|
|
206
224
|
def create_queue
|
|
207
225
|
if max_queue_size == -1
|
|
208
226
|
@queue = Queue.new
|
|
@@ -219,22 +237,26 @@ module SemanticLogger
|
|
|
219
237
|
# or the appender is closed.
|
|
220
238
|
logger.trace "Async: Appender thread active"
|
|
221
239
|
begin
|
|
222
|
-
batch? ? process_messages_in_batches : process_messages
|
|
240
|
+
carry_on = batch? ? process_messages_in_batches : process_messages
|
|
223
241
|
rescue StandardError => e
|
|
224
242
|
if async_max_retries == -1 || retry_count < async_max_retries
|
|
225
243
|
@retry_count += 1
|
|
226
244
|
limit = async_max_retries == -1 ? "unlimited" : async_max_retries
|
|
227
|
-
safe_log(:warn,
|
|
245
|
+
safe_log(:warn,
|
|
246
|
+
"Async: Restarting due to exception, retry #{retry_count} of #{limit}, sleeping #{retry_count}s", e)
|
|
228
247
|
sleep(retry_count)
|
|
229
248
|
retry
|
|
230
249
|
else
|
|
231
|
-
|
|
250
|
+
@stopped = true
|
|
251
|
+
safe_log(:error, "Async: Stopping after #{retry_count} failed retries, dropping new messages", e)
|
|
232
252
|
end
|
|
233
253
|
rescue Exception => e
|
|
254
|
+
@stopped = true
|
|
234
255
|
safe_log(:error, "Async: Stopping due to fatal exception", e)
|
|
235
256
|
ensure
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
# A thread that overflowed its stack hands the queue over to a new one (see #stack_overflowed).
|
|
258
|
+
@thread = carry_on ? spawn_worker : nil
|
|
259
|
+
safe_log(:trace, "Async: Thread has stopped") unless carry_on
|
|
238
260
|
end
|
|
239
261
|
end
|
|
240
262
|
|
|
@@ -247,12 +269,15 @@ module SemanticLogger
|
|
|
247
269
|
end
|
|
248
270
|
|
|
249
271
|
# Streaming: write each log message to the appender as it is dequeued.
|
|
272
|
+
#
|
|
273
|
+
# Returns true when processing must carry on in a new thread (see #stack_overflowed).
|
|
250
274
|
def process_messages
|
|
251
275
|
count = 0
|
|
252
276
|
while (message = queue.pop)
|
|
253
277
|
if message.is_a?(Log)
|
|
254
|
-
|
|
255
|
-
|
|
278
|
+
write_message(message)
|
|
279
|
+
return true if @stack_overflow
|
|
280
|
+
|
|
256
281
|
@retry_count = 0 unless retry_count.zero?
|
|
257
282
|
count += 1
|
|
258
283
|
# Check every few log messages whether this appender thread is falling behind
|
|
@@ -261,20 +286,23 @@ module SemanticLogger
|
|
|
261
286
|
count = 0
|
|
262
287
|
end
|
|
263
288
|
else
|
|
264
|
-
break unless
|
|
289
|
+
break unless message.run(appender)
|
|
265
290
|
end
|
|
266
291
|
end
|
|
267
292
|
logger.trace "Async: Queue Closed"
|
|
293
|
+
false
|
|
268
294
|
end
|
|
269
295
|
|
|
270
296
|
# Batching: group up log messages before writing them via the appender's #batch method.
|
|
297
|
+
#
|
|
298
|
+
# Returns true when processing must carry on in a new thread (see #stack_overflowed).
|
|
271
299
|
def process_messages_in_batches
|
|
272
300
|
loop do
|
|
273
301
|
# Wait for batch interval or number of messages to be exceeded.
|
|
274
302
|
signal.wait(batch_seconds)
|
|
275
303
|
|
|
276
304
|
logs = []
|
|
277
|
-
|
|
305
|
+
commands = []
|
|
278
306
|
first = true
|
|
279
307
|
message_count = queue.length
|
|
280
308
|
message_count.times do
|
|
@@ -287,53 +315,122 @@ module SemanticLogger
|
|
|
287
315
|
first = false
|
|
288
316
|
end
|
|
289
317
|
else
|
|
290
|
-
|
|
318
|
+
commands << message
|
|
291
319
|
end
|
|
292
320
|
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
321
|
# Stop processing once a command (e.g. :close) signals the thread to terminate.
|
|
299
|
-
|
|
322
|
+
return false unless write_batch_then_run(logs, commands)
|
|
323
|
+
return true if @stack_overflow
|
|
300
324
|
|
|
301
325
|
signal.reset unless queue.size >= batch_size
|
|
302
326
|
end
|
|
303
327
|
end
|
|
304
328
|
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
329
|
+
# Write the log messages taken off the queue as a batch, then run the commands taken off the
|
|
330
|
+
# queue with them. Returns false once a command (:close) stops processing.
|
|
331
|
+
#
|
|
332
|
+
# When the batch cannot be written, for example because the destination is unavailable, the
|
|
333
|
+
# commands still run before the error propagates to #process to back off. Otherwise they
|
|
334
|
+
# would be lost, and a caller waiting on #flush or #close, such as the flush at exit, would
|
|
335
|
+
# wait forever.
|
|
336
|
+
def write_batch_then_run(logs, commands)
|
|
337
|
+
unless logs.empty?
|
|
338
|
+
write_batch(logs)
|
|
339
|
+
@retry_count = 0 unless retry_count.zero?
|
|
340
|
+
end
|
|
341
|
+
commands.all? { |command| command.run(appender) }
|
|
342
|
+
rescue StandardError => e
|
|
343
|
+
raise e if commands.all? { |command| process_message_after_failure(command) }
|
|
344
|
+
|
|
345
|
+
safe_log(:warn, "Async: Closed without writing #{logs.size} log messages", e)
|
|
346
|
+
false
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Run a command after a batch could not be written. Returns false once a command (:close)
|
|
350
|
+
# stops processing, even when the command itself fails.
|
|
351
|
+
def process_message_after_failure(command)
|
|
352
|
+
command.run(appender)
|
|
353
|
+
rescue StandardError => e
|
|
354
|
+
safe_log(:warn, "Async: Failed to #{command.command} the appender", e)
|
|
355
|
+
command.command != :close
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Write one log message to the appender.
|
|
359
|
+
#
|
|
360
|
+
# A message whose content cannot be written (see #message_error?) is discarded and reported,
|
|
361
|
+
# without restarting the thread. Any other error, such as the destination being unavailable,
|
|
362
|
+
# propagates to #process, which backs off before retrying. A stack overflow discards the
|
|
363
|
+
# message too (see #stack_overflowed).
|
|
364
|
+
def write_message(log)
|
|
365
|
+
appender.log(log)
|
|
366
|
+
@processed_count += 1
|
|
367
|
+
rescue SystemStackError => e
|
|
368
|
+
stack_overflowed([log], e)
|
|
369
|
+
rescue StandardError => e
|
|
370
|
+
raise unless message_error?(e)
|
|
371
|
+
|
|
372
|
+
messages_failed([log], e)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Write the log messages to the appender as a batch.
|
|
376
|
+
#
|
|
377
|
+
# When the content of a message cannot be written (see #message_error?), and the appender's
|
|
378
|
+
# #batch is all or nothing, the batch is split in half and each half written separately, so
|
|
379
|
+
# that only the offending message is discarded rather than every message in the batch.
|
|
380
|
+
# Otherwise the batch is discarded, since writing it again could duplicate the messages that
|
|
381
|
+
# the appender had already written. A stack overflow discards the batch, and the rest of the
|
|
382
|
+
# batch being split, without splitting it further (see #stack_overflowed).
|
|
383
|
+
def write_batch(logs)
|
|
384
|
+
return messages_failed(logs, @stack_overflow) if @stack_overflow
|
|
385
|
+
|
|
386
|
+
appender.batch(logs)
|
|
387
|
+
@processed_count += logs.size
|
|
388
|
+
rescue SystemStackError => e
|
|
389
|
+
stack_overflowed(logs, e)
|
|
390
|
+
rescue StandardError => e
|
|
391
|
+
raise unless message_error?(e)
|
|
392
|
+
return messages_failed(logs, e) if logs.size == 1 || !appender.batch_all_or_nothing?
|
|
393
|
+
|
|
394
|
+
logs.each_slice((logs.size + 1) / 2) { |half| write_batch(half) }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Discard log messages whose writing overflowed the stack, for example a value nested too
|
|
398
|
+
# deeply for a custom formatter. Retrying cannot write them, as for #message_error?.
|
|
399
|
+
#
|
|
400
|
+
# This thread must not overflow its stack again: on macOS a second stack overflow on the same
|
|
401
|
+
# thread crashes Ruby. So nothing else is written on it, including the rest of a batch, which
|
|
402
|
+
# is discarded rather than split up further to find the message at fault. Once any commands
|
|
403
|
+
# taken off the queue with the messages have run, a new thread carries on (see #process).
|
|
404
|
+
def stack_overflowed(logs, exception)
|
|
405
|
+
@stack_overflow = exception
|
|
406
|
+
messages_failed(logs, exception)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Whether the appender says the exception was caused by the content of a log message, rather
|
|
410
|
+
# than by the destination. An appender that cannot tell (it does not implement
|
|
411
|
+
# #message_error?) has every error retried with a back-off.
|
|
412
|
+
def message_error?(exception) = appender.respond_to?(:message_error?) && appender.message_error?(exception)
|
|
413
|
+
|
|
414
|
+
# Record log messages discarded because their content could not be written. The first is
|
|
415
|
+
# reported to the internal logger straight away, with the error. After that the count is
|
|
416
|
+
# reported, with the latest error, at most once every dropped_message_report_seconds.
|
|
417
|
+
def messages_failed(logs, exception)
|
|
418
|
+
@failed.record(logs.size, interval: dropped_message_report_seconds) do |count, seconds|
|
|
419
|
+
period = seconds ? " in the last #{seconds.round} seconds" : ""
|
|
420
|
+
safe_log(:error, "Async: Discarded #{count} log message(s)#{period} that could not be written, " \
|
|
421
|
+
"most recently from #{logs.first.name}", exception)
|
|
317
422
|
end
|
|
318
|
-
true
|
|
319
423
|
end
|
|
320
424
|
|
|
321
425
|
# Record a dropped message, reporting the running count to the internal logger at most
|
|
322
426
|
# once every dropped_message_report_seconds.
|
|
323
427
|
def message_dropped
|
|
324
|
-
@
|
|
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
|
-
|
|
428
|
+
@dropped.record(interval: dropped_message_report_seconds) do |count, seconds|
|
|
330
429
|
logger.warn(
|
|
331
|
-
"Async: Dropped #{
|
|
430
|
+
"Async: Dropped #{count} log messages in the last #{seconds.round} seconds. " \
|
|
332
431
|
"The queue is full (max_queue_size: #{max_queue_size}). " \
|
|
333
432
|
"Consider reducing the log level, increasing max_queue_size, or changing the appenders"
|
|
334
433
|
)
|
|
335
|
-
@dropped_message_count = 0
|
|
336
|
-
@dropped_message_reported_at = Time.now
|
|
337
434
|
end
|
|
338
435
|
end
|
|
339
436
|
|
|
@@ -341,7 +438,9 @@ module SemanticLogger
|
|
|
341
438
|
diff = Time.now - log.time
|
|
342
439
|
return unless diff > lag_threshold_s
|
|
343
440
|
|
|
344
|
-
logger.warn "Async: Appender thread has fallen behind by #{diff} seconds with
|
|
441
|
+
logger.warn "Async: Appender thread has fallen behind by #{diff} seconds with " \
|
|
442
|
+
"#{queue.size} messages queued up. Consider reducing the log level or " \
|
|
443
|
+
"changing the appenders"
|
|
345
444
|
end
|
|
346
445
|
|
|
347
446
|
# Submit a command to the worker thread and wait for the reply.
|
|
@@ -361,9 +460,12 @@ module SemanticLogger
|
|
|
361
460
|
# Wake up the processing thread to process this command immediately.
|
|
362
461
|
signal.set if batch?
|
|
363
462
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
463
|
+
submitted_to = queue
|
|
464
|
+
request = Request.new(command)
|
|
465
|
+
submitted_to << request
|
|
466
|
+
# A reply can come while the request is on the current queue, which #reopen replaces, and a
|
|
467
|
+
# worker thread is running it, which may be a new one after a stack overflow.
|
|
468
|
+
request.wait { queue.equal?(submitted_to) && active? }
|
|
367
469
|
end
|
|
368
470
|
end
|
|
369
471
|
end
|
|
@@ -16,10 +16,10 @@ module SemanticLogger
|
|
|
16
16
|
#
|
|
17
17
|
# Log entries similar to the following should show up in the log file:
|
|
18
18
|
#
|
|
19
|
-
# 2019-02-06 18:58:17.522467 I [84730:
|
|
20
|
-
# 2019-02-06 18:58:17.527492 I [84730:
|
|
21
|
-
# 2019-02-06 18:58:17.527835 I [84730:
|
|
22
|
-
# 2019-02-06 18:58:17.529761 I [84730:
|
|
19
|
+
# 2019-02-06 18:58:17.522467 I [84730:70256] Minitest -- START MyJob test_0001_archives file
|
|
20
|
+
# 2019-02-06 18:58:17.527492 I [84730:70256] (4.980ms) Minitest -- PASS MyJob test_0001_archives file
|
|
21
|
+
# 2019-02-06 18:58:17.527835 I [84730:70256] Minitest -- START MyJob test_0002_returns job class
|
|
22
|
+
# 2019-02-06 18:58:17.529761 I [84730:70256] (1.882ms) Minitest -- PASS MyJob test_0002_returns job class
|
|
23
23
|
class Minitest < ::Minitest::AbstractReporter
|
|
24
24
|
include SemanticLogger::Loggable
|
|
25
25
|
|
|
@@ -549,8 +549,9 @@ module SemanticLogger
|
|
|
549
549
|
# processed: [Integer] Cumulative number of log messages processed since startup.
|
|
550
550
|
# dropped: [Integer] Cumulative number of log messages dropped at the main queue.
|
|
551
551
|
# appenders: [Array<Hash>] Per-appender statistics. Appenders that run their own
|
|
552
|
-
# async thread report their queue_size and
|
|
553
|
-
# counts; appenders that log
|
|
552
|
+
# async thread report their queue_size and
|
|
553
|
+
# processed/dropped/failed counts; appenders that log
|
|
554
|
+
# inline report `async: false`.
|
|
554
555
|
#
|
|
555
556
|
# All counters are cumulative since process startup. They are thread-safe to read and
|
|
556
557
|
# are maintained without adding any locking to the logging hot path.
|
|
@@ -576,6 +577,18 @@ module SemanticLogger
|
|
|
576
577
|
Logger.processor.lag_threshold_s
|
|
577
578
|
end
|
|
578
579
|
|
|
580
|
+
# Returns [true|false] whether log messages are dropped instead of blocking
|
|
581
|
+
# when the main pipeline queue is full.
|
|
582
|
+
def self.non_blocking?
|
|
583
|
+
Logger.processor.non_blocking?
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Drop log messages instead of blocking the calling thread when the main
|
|
587
|
+
# pipeline queue is full.
|
|
588
|
+
def self.non_blocking=(non_blocking)
|
|
589
|
+
Logger.processor.non_blocking = non_blocking
|
|
590
|
+
end
|
|
591
|
+
|
|
579
592
|
def self.default_level_index
|
|
580
593
|
Thread.current[:semantic_logger_silence] || @default_level_index
|
|
581
594
|
end
|