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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -81
  3. data/Rakefile +10 -3
  4. data/lib/semantic_logger/appender/async.rb +86 -173
  5. data/lib/semantic_logger/appender/cloudwatch_logs.rb +4 -4
  6. data/lib/semantic_logger/appender/elasticsearch.rb +6 -182
  7. data/lib/semantic_logger/appender/elasticsearch_base.rb +212 -0
  8. data/lib/semantic_logger/appender/elasticsearch_http.rb +2 -2
  9. data/lib/semantic_logger/appender/file.rb +20 -4
  10. data/lib/semantic_logger/appender/graylog.rb +2 -2
  11. data/lib/semantic_logger/appender/http.rb +27 -2
  12. data/lib/semantic_logger/appender/io.rb +8 -4
  13. data/lib/semantic_logger/appender/kafka.rb +2 -2
  14. data/lib/semantic_logger/appender/loki.rb +2 -4
  15. data/lib/semantic_logger/appender/mongodb.rb +3 -6
  16. data/lib/semantic_logger/appender/new_relic_logs.rb +2 -2
  17. data/lib/semantic_logger/appender/open_telemetry.rb +8 -6
  18. data/lib/semantic_logger/appender/opensearch.rb +35 -0
  19. data/lib/semantic_logger/appender/rabbitmq.rb +3 -3
  20. data/lib/semantic_logger/appender/sentry_ruby.rb +2 -2
  21. data/lib/semantic_logger/appender/splunk.rb +2 -2
  22. data/lib/semantic_logger/appender/splunk_http.rb +3 -3
  23. data/lib/semantic_logger/appender/syslog.rb +5 -4
  24. data/lib/semantic_logger/appender/tcp.rb +2 -2
  25. data/lib/semantic_logger/appender/udp.rb +2 -2
  26. data/lib/semantic_logger/appender/wrapper.rb +4 -4
  27. data/lib/semantic_logger/appender.rb +30 -19
  28. data/lib/semantic_logger/appenders.rb +26 -5
  29. data/lib/semantic_logger/base.rb +100 -21
  30. data/lib/semantic_logger/concerns/compatibility.rb +2 -2
  31. data/lib/semantic_logger/core_ext/process.rb +34 -0
  32. data/lib/semantic_logger/formatters/base.rb +46 -7
  33. data/lib/semantic_logger/formatters/color.rb +6 -3
  34. data/lib/semantic_logger/formatters/default.rb +6 -4
  35. data/lib/semantic_logger/formatters/ecs.rb +151 -0
  36. data/lib/semantic_logger/formatters/fluentd.rb +15 -4
  37. data/lib/semantic_logger/formatters/json.rb +6 -1
  38. data/lib/semantic_logger/formatters/logfmt.rb +2 -2
  39. data/lib/semantic_logger/formatters/loki.rb +4 -4
  40. data/lib/semantic_logger/formatters/open_telemetry.rb +15 -5
  41. data/lib/semantic_logger/formatters/pattern.rb +235 -0
  42. data/lib/semantic_logger/formatters/raw.rb +2 -2
  43. data/lib/semantic_logger/formatters/signalfx.rb +2 -2
  44. data/lib/semantic_logger/formatters/syslog.rb +14 -3
  45. data/lib/semantic_logger/formatters/syslog_cee.rb +1 -1
  46. data/lib/semantic_logger/formatters.rb +2 -0
  47. data/lib/semantic_logger/log.rb +18 -4
  48. data/lib/semantic_logger/logger.rb +2 -2
  49. data/lib/semantic_logger/metric/new_relic.rb +2 -2
  50. data/lib/semantic_logger/metric/signalfx.rb +2 -2
  51. data/lib/semantic_logger/metric/statsd.rb +2 -2
  52. data/lib/semantic_logger/processor.rb +21 -0
  53. data/lib/semantic_logger/queue_processor.rb +369 -0
  54. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  55. data/lib/semantic_logger/semantic_logger.rb +103 -11
  56. data/lib/semantic_logger/subscriber.rb +15 -2
  57. data/lib/semantic_logger/sync_processor.rb +25 -3
  58. data/lib/semantic_logger/test/capture_log_events.rb +2 -2
  59. data/lib/semantic_logger/test/minitest.rb +8 -4
  60. data/lib/semantic_logger/test/rspec.rb +249 -0
  61. data/lib/semantic_logger/utils.rb +83 -4
  62. data/lib/semantic_logger/version.rb +1 -1
  63. data/lib/semantic_logger.rb +9 -0
  64. metadata +17 -8
  65. 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
- data.to_json
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
- data.to_json
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
- super()
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.
@@ -38,7 +38,7 @@ module SemanticLogger
38
38
 
39
39
  def call(log, logger)
40
40
  hash = super
41
- create_syslog_packet("@cee: #{hash.to_json}")
41
+ create_syslog_packet("@cee: #{Utils.to_json(hash)}")
42
42
  end
43
43
 
44
44
  private
@@ -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"
@@ -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+).*/.freeze
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.gsub(/(\e(\[([\d;]*[mz]?))?)?/, "").strip
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, &block)
68
+ def log(log, message = nil, progname = nil, &)
69
69
  # Compatibility with ::Logger
70
- return add(log, message, progname, &block) unless log.is_a?(SemanticLogger::Log)
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, &block)
41
+ def initialize(prefix: "Custom", **args, &)
42
42
  @prefix = prefix
43
- super(**args, &block)
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
- &block)
81
+ &)
82
82
  formatter ||= SemanticLogger::Formatters::Signalfx.new(token: token, dimensions: dimensions)
83
83
 
84
- super(url: url, formatter: formatter, **args, &block)
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.class_name} #{test.name}")
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.class_name} #{test.name}", duration: test.time * 1_000,
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.class_name} #{test.name}", duration: test.time * 1_000,
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.class_name} #{test.name}", duration: test.time * 1_000,
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