semantic_logger 4.17.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -81
  3. data/Rakefile +8 -1
  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/honeybadger_insights.rb +1 -1
  12. data/lib/semantic_logger/appender/http.rb +27 -2
  13. data/lib/semantic_logger/appender/io.rb +8 -4
  14. data/lib/semantic_logger/appender/kafka.rb +2 -2
  15. data/lib/semantic_logger/appender/loki.rb +2 -4
  16. data/lib/semantic_logger/appender/mongodb.rb +3 -6
  17. data/lib/semantic_logger/appender/new_relic_logs.rb +12 -2
  18. data/lib/semantic_logger/appender/open_telemetry.rb +25 -11
  19. data/lib/semantic_logger/appender/opensearch.rb +35 -0
  20. data/lib/semantic_logger/appender/rabbitmq.rb +3 -3
  21. data/lib/semantic_logger/appender/sentry_ruby.rb +8 -3
  22. data/lib/semantic_logger/appender/splunk.rb +2 -2
  23. data/lib/semantic_logger/appender/splunk_http.rb +3 -3
  24. data/lib/semantic_logger/appender/syslog.rb +5 -4
  25. data/lib/semantic_logger/appender/tcp.rb +2 -2
  26. data/lib/semantic_logger/appender/udp.rb +2 -2
  27. data/lib/semantic_logger/appender/wrapper.rb +4 -4
  28. data/lib/semantic_logger/appender.rb +30 -19
  29. data/lib/semantic_logger/appenders.rb +26 -5
  30. data/lib/semantic_logger/base.rb +113 -21
  31. data/lib/semantic_logger/concerns/compatibility.rb +2 -2
  32. data/lib/semantic_logger/core_ext/process.rb +34 -0
  33. data/lib/semantic_logger/formatters/base.rb +46 -7
  34. data/lib/semantic_logger/formatters/color.rb +6 -3
  35. data/lib/semantic_logger/formatters/default.rb +6 -4
  36. data/lib/semantic_logger/formatters/ecs.rb +151 -0
  37. data/lib/semantic_logger/formatters/fluentd.rb +15 -4
  38. data/lib/semantic_logger/formatters/json.rb +6 -1
  39. data/lib/semantic_logger/formatters/logfmt.rb +2 -2
  40. data/lib/semantic_logger/formatters/loki.rb +4 -4
  41. data/lib/semantic_logger/formatters/new_relic_logs.rb +4 -6
  42. data/lib/semantic_logger/formatters/open_telemetry.rb +40 -2
  43. data/lib/semantic_logger/formatters/pattern.rb +235 -0
  44. data/lib/semantic_logger/formatters/raw.rb +2 -2
  45. data/lib/semantic_logger/formatters/signalfx.rb +2 -2
  46. data/lib/semantic_logger/formatters/syslog.rb +14 -3
  47. data/lib/semantic_logger/formatters/syslog_cee.rb +1 -1
  48. data/lib/semantic_logger/formatters.rb +2 -0
  49. data/lib/semantic_logger/log.rb +23 -4
  50. data/lib/semantic_logger/logger.rb +2 -2
  51. data/lib/semantic_logger/metric/new_relic.rb +2 -2
  52. data/lib/semantic_logger/metric/signalfx.rb +2 -2
  53. data/lib/semantic_logger/metric/statsd.rb +2 -2
  54. data/lib/semantic_logger/processor.rb +22 -1
  55. data/lib/semantic_logger/queue_processor.rb +369 -0
  56. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  57. data/lib/semantic_logger/semantic_logger.rb +103 -11
  58. data/lib/semantic_logger/subscriber.rb +15 -2
  59. data/lib/semantic_logger/sync_processor.rb +25 -3
  60. data/lib/semantic_logger/test/capture_log_events.rb +2 -2
  61. data/lib/semantic_logger/test/minitest.rb +8 -4
  62. data/lib/semantic_logger/test/rspec.rb +249 -0
  63. data/lib/semantic_logger/utils.rb +83 -4
  64. data/lib/semantic_logger/version.rb +1 -1
  65. data/lib/semantic_logger.rb +9 -0
  66. metadata +15 -6
  67. data/lib/semantic_logger/appender/async_batch.rb +0 -93
@@ -23,6 +23,11 @@ require "semantic_logger/formatters/new_relic_logs"
23
23
  module SemanticLogger
24
24
  module Appender
25
25
  class NewRelicLogs < SemanticLogger::Subscriber
26
+ CAPTURE_CONTEXT = lambda do |log|
27
+ meta = ::NewRelic::Agent.linking_metadata
28
+ log.set_context(:new_relic_metadata, meta) unless meta.empty?
29
+ end
30
+
26
31
  # Create Appender
27
32
  #
28
33
  # Parameters
@@ -42,14 +47,19 @@ module SemanticLogger
42
47
  # The Proc must return true or false.
43
48
  def initialize(formatter: SemanticLogger::Formatters::NewRelicLogs.new, **args, &block)
44
49
  super
50
+
51
+ # Record NewRelic's "trace.id"/"entity.name"/"hostname"/etc, so we can include them later in the formatted output.
52
+ # These are thread-local, so need to be captured as soon as the log-message is created.
53
+ # https://rubydoc.info/gems/newrelic_rpm/NewRelic/Agent#linking_metadata-instance_method
54
+ SemanticLogger.on_log(CAPTURE_CONTEXT)
45
55
  end
46
56
 
47
57
  # Send an error notification to New Relic
48
58
  def log(log)
49
59
  begin
50
60
  message = formatter.call(log, self) # Generate the structured log
51
- json_message = message.to_json # Convert the log to JSON
52
- level = log.level.to_s.upcase # Determine the log level
61
+ json_message = Utils.to_json(message) # Convert the log to JSON, repairing any non UTF-8 data
62
+ level = log.level.to_s.upcase # Determine the log level
53
63
  self.class.log_newrelic(json_message, level)
54
64
  rescue JSON::GeneratorError => e
55
65
  warn("Failed to serialize log message to JSON: #{e.message}")
@@ -12,7 +12,7 @@ end
12
12
  module SemanticLogger
13
13
  module Appender
14
14
  class OpenTelemetry < SemanticLogger::Subscriber
15
- attr_reader :name, :version, :logger
15
+ attr_reader :name, :version, :provider, :logger
16
16
 
17
17
  CAPTURE_CONTEXT = ->(log) { log.set_context(:open_telemetry, ::OpenTelemetry::Context.current) }
18
18
 
@@ -29,16 +29,19 @@ module SemanticLogger
29
29
  formatter: SemanticLogger::Formatters::OpenTelemetry.new,
30
30
  metrics: true,
31
31
  **args,
32
- &block)
33
- @name = name
34
- @version = version
35
- @logger = ::OpenTelemetry.logger_provider.logger(name: @name, version: @version)
32
+ &)
33
+ # Base#initialize (via Subscriber) overwrites @name with the class name,
34
+ # so call super first and then assign our own attributes.
35
+ super(formatter: formatter, metrics: metrics, **args, &)
36
+
37
+ @name = name
38
+ @version = version
39
+ @provider = ::OpenTelemetry.logger_provider
40
+ @logger = @provider.logger(name: @name, version: @version)
36
41
 
37
42
  # Capture the current Open Telemetry context when a log entry is captured.
38
43
  # Prevents duplicate subscribers as long as it is from a constant.
39
44
  SemanticLogger.on_log(CAPTURE_CONTEXT)
40
-
41
- super(formatter: formatter, metrics: metrics, **args, &block)
42
45
  end
43
46
 
44
47
  def log(log)
@@ -56,19 +59,30 @@ module SemanticLogger
56
59
  timestamp: time,
57
60
  body: body.transform_keys!(&:to_s),
58
61
  attributes: payload,
59
- context: log.context[:open_telemetry] || ::OpenTelemetry::Context.current
62
+ context: (log.context && log.context[:open_telemetry]) || ::OpenTelemetry::Context.current
60
63
  )
61
64
  true
62
65
  end
63
66
 
64
67
  # Flush all pending logs.
65
68
  def flush
66
- @logger.logger_provider.force_flush
69
+ return unless @provider
70
+
71
+ @provider.force_flush if @provider.respond_to?(:force_flush)
72
+ rescue StandardError => e
73
+ # Swallow to avoid noisy shutdown exceptions.
74
+ SemanticLogger::Processor.logger.warn("Flush failed: #{e.class}: #{e.message}")
67
75
  end
68
76
 
69
- # Flush pending logs and close the appender
77
+ # Close the appender and release resources.
70
78
  def close
71
- @logger.logger_provider.shutdown
79
+ return unless @provider
80
+
81
+ @provider.shutdown if @provider.respond_to?(:shutdown)
82
+ rescue StandardError => e
83
+ SemanticLogger::Processor.logger.warn("Shutdown failed: #{e.class}: #{e.message}")
84
+ ensure
85
+ @provider = nil
72
86
  end
73
87
 
74
88
  # For logging metrics only log events.
@@ -0,0 +1,35 @@
1
+ begin
2
+ require "opensearch-ruby"
3
+ rescue LoadError
4
+ raise LoadError,
5
+ 'Gem opensearch-ruby is required for logging to OpenSearch. Please add the gem "opensearch-ruby" to your Gemfile.'
6
+ end
7
+
8
+ require "semantic_logger/appender/elasticsearch_base"
9
+
10
+ # Forward all log messages to OpenSearch (e.g. AWS OpenSearch).
11
+ #
12
+ # OpenSearch is a fork of Elasticsearch 7.10. The client API and bulk
13
+ # protocol are identical, so this appender reuses the same pipeline as the
14
+ # Elasticsearch appender; only the backing client gem differs. Use this
15
+ # appender (with the `opensearch-ruby` gem) instead of `:elasticsearch`
16
+ # when talking to an OpenSearch server, since modern `elasticsearch` gems
17
+ # reject non-Elasticsearch servers via a product check.
18
+ #
19
+ # Example:
20
+ #
21
+ # SemanticLogger.add_appender(
22
+ # appender: :opensearch,
23
+ # url: 'http://localhost:9200'
24
+ # )
25
+ module SemanticLogger
26
+ module Appender
27
+ class Opensearch < ElasticsearchBase
28
+ private
29
+
30
+ def client_class
31
+ ::OpenSearch::Client
32
+ end
33
+ end
34
+ end
35
+ end
@@ -12,7 +12,7 @@ end
12
12
  # appender: :rabbitmq,
13
13
  #
14
14
  # # Name of the queue in RabbitMQ where to publish the logs. This queue will be bound to "amqp.direct" exchange.
15
- # queue: 'semantic_logger',
15
+ # queue_name: 'semantic_logger',
16
16
  #
17
17
  # # This host will be used for RabbitMQ connection.
18
18
  # # NOTE this is different than :host option which is used by the logger directly.
@@ -82,13 +82,13 @@ module SemanticLogger
82
82
  # more parameters supported by Bunny: http://rubybunny.info/articles/connecting.html
83
83
  def initialize(queue_name: "semantic_logger", rabbitmq_host: nil,
84
84
  level: nil, formatter: nil, filter: nil, application: nil, environment: nil, host: nil, metrics: true,
85
- **args, &block)
85
+ **args, &)
86
86
  @queue_name = queue_name
87
87
  @rabbitmq_args = args.dup
88
88
  @rabbitmq_args[:host] = rabbitmq_host
89
89
  @rabbitmq_args[:logger] = logger
90
90
 
91
- super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &block)
91
+ super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &)
92
92
  reopen
93
93
  end
94
94
 
@@ -40,7 +40,12 @@ module SemanticLogger
40
40
  def initialize(level: :error, **args, &block)
41
41
  # Replace the Sentry Ruby logger so that we can identify its log
42
42
  # messages and not forward them to Sentry
43
- ::Sentry.init { |config| config.logger = SemanticLogger[::Sentry] } unless ::Sentry.initialized?
43
+ unless ::Sentry.initialized?
44
+ ::Sentry.init do |config|
45
+ logger = SemanticLogger[::Sentry]
46
+ config.respond_to?(:sdk_logger=) ? config.sdk_logger = logger : config.logger = logger
47
+ end
48
+ end
44
49
  super
45
50
  end
46
51
 
@@ -128,10 +133,10 @@ module SemanticLogger
128
133
  #
129
134
  def extract_tags!(context)
130
135
  named_tags = context.delete(:named_tags) || {}
131
- named_tags = named_tags.map { |k, v| [k.to_s, v.to_s] }.to_h
136
+ named_tags = named_tags.to_h { |k, v| [k.to_s, v.to_s] }
132
137
  tags = context.delete(:tags)
133
138
  named_tags.merge!("tag" => tags.join(", ")) { |_, v1, v2| "#{v1}, #{v2}" } if tags
134
- named_tags.map { |k, v| [k[0...32], v[0...256]] }.to_h
139
+ named_tags.to_h { |k, v| [k[0...32], v[0...256]] }
135
140
  end
136
141
  end
137
142
  end
@@ -89,11 +89,11 @@ module SemanticLogger
89
89
  # regular expression. All other messages will be ignored.
90
90
  # Proc: Only include log messages where the supplied Proc returns true
91
91
  # The Proc must return true or false.
92
- def initialize(index: "main", source_type: nil, **args, &block)
92
+ def initialize(index: "main", source_type: nil, **args, &)
93
93
  @index = index
94
94
  @source_type = source_type
95
95
 
96
- super(**args, &block)
96
+ super(**args, &)
97
97
  reopen
98
98
  end
99
99
 
@@ -73,11 +73,11 @@ module SemanticLogger
73
73
  index: nil,
74
74
  compress: true,
75
75
  **args,
76
- &block)
76
+ &)
77
77
  @source_type = source_type
78
78
  @index = index
79
79
 
80
- super(compress: compress, **args, &block)
80
+ super(compress: compress, **args, &)
81
81
 
82
82
  # Put splunk auth token in the header of every HTTP post.
83
83
  @header["Authorization"] = "Splunk #{token}"
@@ -98,7 +98,7 @@ module SemanticLogger
98
98
  }
99
99
  message[:sourcetype] = source_type if source_type
100
100
  message[:index] = index if index
101
- message.to_json
101
+ Utils.to_json(message)
102
102
  end
103
103
  end
104
104
  end
@@ -130,7 +130,7 @@ module SemanticLogger
130
130
  options: ::Syslog::LOG_PID | ::Syslog::LOG_CONS,
131
131
  tcp_client: {},
132
132
  **args,
133
- &block)
133
+ &)
134
134
  @options = options
135
135
  @facility = facility
136
136
  @max_size = max_size
@@ -165,7 +165,7 @@ module SemanticLogger
165
165
  end
166
166
  end
167
167
 
168
- super(**args, &block)
168
+ super(**args, &)
169
169
  reopen
170
170
  end
171
171
 
@@ -213,8 +213,9 @@ module SemanticLogger
213
213
  # Returns [SemanticLogger::Formatters::Base] default formatter for this Appender depending on the protocal selected
214
214
  def default_formatter
215
215
  if protocol == :syslog
216
- # Format is text output without the time
217
- SemanticLogger::Formatters::Default.new(time_format: :notime)
216
+ # Format is text output without the time.
217
+ # Escape control characters so untrusted log data cannot forge syslog entries.
218
+ SemanticLogger::Formatters::Default.new(time_format: :notime, escape_control_chars: true)
218
219
  else
219
220
  SemanticLogger::Formatters::Syslog.new(facility: facility, level_map: level_map, max_size: max_size)
220
221
  end
@@ -184,7 +184,7 @@ module SemanticLogger
184
184
  # )
185
185
  def initialize(separator: "\n",
186
186
  level: nil, formatter: nil, filter: nil, application: nil, environment: nil, host: nil, metrics: false,
187
- **tcp_client_args, &block)
187
+ **tcp_client_args, &)
188
188
  @separator = separator
189
189
  @tcp_client_args = tcp_client_args
190
190
 
@@ -192,7 +192,7 @@ module SemanticLogger
192
192
  Net::TCPClient.logger = logger
193
193
  Net::TCPClient.logger.name = "Net::TCPClient"
194
194
 
195
- super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &block)
195
+ super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &)
196
196
  reopen
197
197
  end
198
198
 
@@ -61,11 +61,11 @@ module SemanticLogger
61
61
  # appender: :udp,
62
62
  # server: 'server:3300'
63
63
  # )
64
- def initialize(server:, udp_flags: 0, metrics: true, **args, &block)
64
+ def initialize(server:, udp_flags: 0, metrics: true, **args, &)
65
65
  @server = server
66
66
  @udp_flags = udp_flags
67
67
 
68
- super(metrics: metrics, **args, &block)
68
+ super(metrics: metrics, **args, &)
69
69
  reopen
70
70
  end
71
71
 
@@ -38,17 +38,17 @@ module SemanticLogger
38
38
  # logger.info('Hello World', some: :payload)
39
39
  #
40
40
  # Install the `rails_semantic_logger` gem to replace the Rails logger with Semantic Logger.
41
- def initialize(logger:, **args, &block)
41
+ def initialize(logger:, **args, &)
42
42
  @logger = logger
43
43
 
44
44
  # Check if the custom appender responds to all the log levels. For example Ruby ::Logger
45
- does_not_implement = LEVELS[1..-1].find { |i| !@logger.respond_to?(i) }
45
+ does_not_implement = LEVELS[1..].find { |i| !@logger.respond_to?(i) }
46
46
  if does_not_implement
47
47
  raise(ArgumentError,
48
- "Supplied logger does not implement:#{does_not_implement}. It must implement all of #{LEVELS[1..-1].inspect}")
48
+ "Supplied logger does not implement:#{does_not_implement}. It must implement all of #{LEVELS[1..].inspect}")
49
49
  end
50
50
 
51
- super(**args, &block)
51
+ super(**args, &)
52
52
  end
53
53
 
54
54
  # Pass log calls to the underlying Rails, log4j or Ruby logger
@@ -2,10 +2,10 @@ module SemanticLogger
2
2
  module Appender
3
3
  # @formatter:off
4
4
  autoload :Async, "semantic_logger/appender/async"
5
- autoload :AsyncBatch, "semantic_logger/appender/async_batch"
6
5
  autoload :Bugsnag, "semantic_logger/appender/bugsnag"
7
6
  autoload :CloudwatchLogs, "semantic_logger/appender/cloudwatch_logs"
8
7
  autoload :Elasticsearch, "semantic_logger/appender/elasticsearch"
8
+ autoload :ElasticsearchBase, "semantic_logger/appender/elasticsearch_base"
9
9
  autoload :ElasticsearchHttp, "semantic_logger/appender/elasticsearch_http"
10
10
  autoload :File, "semantic_logger/appender/file"
11
11
  autoload :Graylog, "semantic_logger/appender/graylog"
@@ -18,6 +18,7 @@ module SemanticLogger
18
18
  autoload :MongoDB, "semantic_logger/appender/mongodb"
19
19
  autoload :NewRelic, "semantic_logger/appender/new_relic"
20
20
  autoload :NewRelicLogs, "semantic_logger/appender/new_relic_logs"
21
+ autoload :Opensearch, "semantic_logger/appender/opensearch"
21
22
  autoload :OpenTelemetry, "semantic_logger/appender/open_telemetry"
22
23
  autoload :Rabbitmq, "semantic_logger/appender/rabbitmq"
23
24
  autoload :Splunk, "semantic_logger/appender/splunk"
@@ -34,27 +35,37 @@ module SemanticLogger
34
35
  def self.factory(async: false, batch: nil,
35
36
  max_queue_size: 10_000, lag_check_interval: 1_000, lag_threshold_s: 30,
36
37
  batch_size: 300, batch_seconds: 5,
38
+ non_blocking: false, dropped_message_report_seconds: 30,
39
+ async_max_retries: 100,
37
40
  **args,
38
- &block)
39
- appender = build(**args, &block)
41
+ &)
42
+ appender = build(**args, &)
40
43
 
41
- # If appender implements #batch, then it should use the batch proxy by default.
42
- batch = true if batch.nil? && appender.respond_to?(:batch)
44
+ # If appender implements #batch, then it should use the batch proxy by default,
45
+ # unless the appender opts out of batching by default (e.g. the HTTP appender).
46
+ batch = true if batch.nil? && appender.respond_to?(:batch) && appender.batch_by_default?
43
47
 
44
48
  if batch == true
45
- Appender::AsyncBatch.new(
46
- appender: appender,
47
- max_queue_size: max_queue_size,
48
- lag_threshold_s: lag_threshold_s,
49
- batch_size: batch_size,
50
- batch_seconds: batch_seconds
49
+ Appender::Async.new(
50
+ appender: appender,
51
+ batch: true,
52
+ max_queue_size: max_queue_size,
53
+ lag_threshold_s: lag_threshold_s,
54
+ batch_size: batch_size,
55
+ batch_seconds: batch_seconds,
56
+ non_blocking: non_blocking,
57
+ dropped_message_report_seconds: dropped_message_report_seconds,
58
+ async_max_retries: async_max_retries
51
59
  )
52
60
  elsif async == true
53
61
  Appender::Async.new(
54
- appender: appender,
55
- max_queue_size: max_queue_size,
56
- lag_check_interval: lag_check_interval,
57
- lag_threshold_s: lag_threshold_s
62
+ appender: appender,
63
+ max_queue_size: max_queue_size,
64
+ lag_check_interval: lag_check_interval,
65
+ lag_threshold_s: lag_threshold_s,
66
+ non_blocking: non_blocking,
67
+ dropped_message_report_seconds: dropped_message_report_seconds,
68
+ async_max_retries: async_max_retries
58
69
  )
59
70
  else
60
71
  appender
@@ -62,13 +73,13 @@ module SemanticLogger
62
73
  end
63
74
 
64
75
  # Returns [Subscriber] instance from the supplied options.
65
- def self.build(io: nil, file_name: nil, appender: nil, metric: nil, logger: nil, **args, &block)
76
+ def self.build(io: nil, file_name: nil, appender: nil, metric: nil, logger: nil, **args, &)
66
77
  if file_name
67
- SemanticLogger::Appender::File.new(file_name, **args, &block)
78
+ SemanticLogger::Appender::File.new(file_name, **args, &)
68
79
  elsif io
69
- SemanticLogger::Appender::IO.new(io, **args, &block)
80
+ SemanticLogger::Appender::IO.new(io, **args, &)
70
81
  elsif logger
71
- SemanticLogger::Appender::Wrapper.new(logger: logger, **args, &block)
82
+ SemanticLogger::Appender::Wrapper.new(logger: logger, **args, &)
72
83
  elsif appender
73
84
  if appender.is_a?(Symbol)
74
85
  SemanticLogger::Utils.constantize_symbol(appender).new(**args)
@@ -9,11 +9,12 @@ module SemanticLogger
9
9
  super()
10
10
  end
11
11
 
12
- def add(**args, &block)
13
- appender = SemanticLogger::Appender.factory(**args, &block)
12
+ def add(**args, &)
13
+ appender = SemanticLogger::Appender.factory(**args, &)
14
14
 
15
- if appender.respond_to?(:console_output?) && appender.console_output? && console_output?
16
- logger.warn "Ignoring attempt to add a second console appender: #{appender.class.name} since it would result in duplicate console output."
15
+ stream = appender.respond_to?(:console_stream) && appender.console_stream
16
+ if stream && console_streams.include?(stream)
17
+ logger.warn "Ignoring attempt to add a second #{stream} console appender since it would result in duplicate console output."
17
18
  return
18
19
  end
19
20
 
@@ -21,10 +22,30 @@ module SemanticLogger
21
22
  appender
22
23
  end
23
24
 
25
+ # The console streams (:stdout and/or :stderr) already being written to by the existing appenders.
26
+ def console_streams
27
+ filter_map { |appender| appender.console_stream if appender.respond_to?(:console_stream) }
28
+ end
29
+
24
30
  # Whether any of the existing appenders already output to the console?
25
31
  # I.e. Writes to stdout or stderr.
26
32
  def console_output?
27
- any? { |appender| appender.respond_to?(:console_output?) && appender.console_output? }
33
+ console_streams.any?
34
+ end
35
+
36
+ # Returns [Array<Hash>] operational statistics for each appender.
37
+ #
38
+ # Appenders that run asynchronously (see SemanticLogger::Appender::Async#stats) report
39
+ # their queue size and processed/dropped counts. Appenders that log inline on the
40
+ # processor thread report only their name with `async: false`.
41
+ def stats
42
+ map do |appender|
43
+ if appender.respond_to?(:stats)
44
+ appender.stats
45
+ else
46
+ {name: appender.name, async: false}
47
+ end
48
+ end
28
49
  end
29
50
 
30
51
  def log(log)