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
@@ -47,7 +47,7 @@ module SemanticLogger
47
47
  hash = super
48
48
 
49
49
  result = {
50
- **newrelic_metadata,
50
+ **linking_metadata(log),
51
51
  message: hash[:message].to_s,
52
52
  tags: hash[:tags],
53
53
  metric: hash[:metric],
@@ -115,11 +115,9 @@ module SemanticLogger
115
115
 
116
116
  private
117
117
 
118
- # NOTE: This function will already include trace.id and span.id if they
119
- # are available so I believe the previous implementation of this is redundant
120
- # https://rubydoc.info/gems/newrelic_rpm/NewRelic/Agent#linking_metadata-instance_method
121
- def newrelic_metadata
122
- NewRelic::Agent.linking_metadata.transform_keys(&:to_sym)
118
+ # returns "entity.name", "trace.id" etc from NewRelic::Agent.linking_metadata
119
+ def linking_metadata(log)
120
+ (log.context && log.context[:new_relic_metadata]) || {}
123
121
  end
124
122
  end
125
123
  end
@@ -2,21 +2,59 @@ require "json"
2
2
  module SemanticLogger
3
3
  module Formatters
4
4
  class OpenTelemetry < Raw
5
+ # primitives allowed by OTLP logs in Ruby: String, Integer, Float, TrueClass, FalseClass
6
+ PRIMS = [String, Integer, Float, TrueClass, FalseClass].freeze
7
+
8
+ # Returns the attributes hash submitted to the OpenTelemetry SDK.
9
+ #
10
+ # Unlike the JSON formatters there is no single `.to_json` boundary here:
11
+ # the hash is handed to the OTLP exporter, which requires valid UTF-8. Cleanse
12
+ # the whole structure so binary / non UTF-8 strings cannot break the export.
13
+ def call(log, logger)
14
+ Utils.encode_utf8(super)
15
+ end
16
+
5
17
  # Log level
6
18
  def level
7
19
  hash[:level] = log.level.to_s
8
- hash[:level_index] = severity_number(log.level_index)
20
+ hash[:level_index] = severity_number(log.level)
9
21
  end
10
22
 
11
23
  # Payload is submitted directly as attributes
12
24
  def payload
13
25
  return unless log.payload.respond_to?(:empty?) && !log.payload.empty?
14
26
 
15
- hash[:payload] = log.payload.transform_keys!(&:to_s)
27
+ hash[:payload] = coerce_map(log.payload)
16
28
  end
17
29
 
18
30
  private
19
31
 
32
+ def coerce_value(v)
33
+ case v
34
+ when *PRIMS then v
35
+ when Array then v.map { |e| coerce_value(e) }.compact # arrays of scalars only.
36
+ when NilClass then nil # drop nils by caller.
37
+ else v.to_s # stringify objects / hashes.
38
+ end
39
+ end
40
+
41
+ def coerce_map(h)
42
+ h.each_with_object({}) do |(k, v), out|
43
+ next if v.nil?
44
+
45
+ out[k.to_s] =
46
+ if v.is_a?(Hash)
47
+ # Stringify whole hash. Cleanse here too: this runs while building the
48
+ # hash, before the top-level call cleanse, so it must not raise on its own.
49
+ Utils.to_json(
50
+ v.transform_values { |vv| coerce_value(vv) }.transform_keys!(&:to_s)
51
+ )
52
+ else
53
+ coerce_value(v)
54
+ end
55
+ end
56
+ end
57
+
20
58
  def severity_number(severity)
21
59
  case severity
22
60
  when :trace
@@ -0,0 +1,235 @@
1
+ require "time"
2
+
3
+ module SemanticLogger
4
+ module Formatters
5
+ # Formats log messages using a configurable pattern string, so a custom
6
+ # log line layout can be specified directly in the configuration without
7
+ # having to write a new formatter class.
8
+ #
9
+ # Pattern placeholders use the form `%{directive}`, where `directive` is the
10
+ # name of any of the formatting methods (inherited from Default, or defined
11
+ # below). Named tags support a parameterized form: `%{named_tags:request_id}`
12
+ # returns the value of a single named tag. Use `%%{...}` to emit a literal
13
+ # `%{...}` without interpolation.
14
+ #
15
+ # Example:
16
+ # SemanticLogger.add_appender(
17
+ # io: $stdout,
18
+ # formatter: {
19
+ # pattern: { pattern: "%{time} %{level} %{name} -- %{message}" }
20
+ # }
21
+ # )
22
+ #
23
+ # Available directives:
24
+ # time Formatted timestamp. Optionally accepts a strftime
25
+ # format, e.g. time:%Y-%m-%dT%H:%M:%S.%6N.
26
+ # level Full level name, e.g. "debug".
27
+ # level_short Single character level, e.g. "D".
28
+ # name Logger / class name.
29
+ # message Log message.
30
+ # payload Payload rendered as a string.
31
+ # exception_class Class of the logged exception, e.g. "RuntimeError".
32
+ # exception_message Message of the logged exception.
33
+ # backtrace Backtrace of the logged exception.
34
+ # duration Human readable duration, e.g. "1.2ms".
35
+ # duration_ms Duration in milliseconds (numeric).
36
+ # thread_name Name of the thread that logged the message.
37
+ # pid Process id.
38
+ # file_name Ruby file name that logged the message, e.g. "app.rb".
39
+ # line Line number within the Ruby file, e.g. 42.
40
+ # tags Tags, comma separated.
41
+ # named_tags All named tags, or one tag with named_tags:key.
42
+ # host Host name.
43
+ # application Application name.
44
+ # environment Environment name.
45
+ class Pattern < Default
46
+ attr_reader :pattern
47
+
48
+ # Approximates the Default formatter's output.
49
+ DEFAULT_PATTERN = "%{time} %{level} [%{pid}:%{thread_name}] %{name} -- %{message}".freeze
50
+
51
+ # The directives that may appear in a pattern. The value is whether the
52
+ # directive accepts a parameter, e.g. %{named_tags:request_id}.
53
+ DIRECTIVES = {
54
+ time: true,
55
+ level: false,
56
+ level_short: false,
57
+ name: false,
58
+ message: false,
59
+ payload: false,
60
+ exception_class: false,
61
+ exception_message: false,
62
+ backtrace: false,
63
+ duration: false,
64
+ duration_ms: false,
65
+ thread_name: false,
66
+ pid: false,
67
+ file_name: false,
68
+ line: false,
69
+ tags: false,
70
+ named_tags: true,
71
+ host: false,
72
+ application: false,
73
+ environment: false
74
+ }.freeze
75
+
76
+ # A single interpolated directive within a compiled pattern.
77
+ Token = Struct.new(:method_name, :arguments)
78
+ private_constant :Token
79
+
80
+ # Parameters:
81
+ # pattern: [String]
82
+ # The pattern string used to format every log entry.
83
+ # Default: DEFAULT_PATTERN
84
+ #
85
+ # Plus all the options supported by SemanticLogger::Formatters::Base.
86
+ def initialize(pattern: DEFAULT_PATTERN, **args)
87
+ @pattern = pattern
88
+ super(**args)
89
+ # Parse the pattern once, up front, so that formatting every log entry
90
+ # is just a walk over the pre-compiled tokens (no regex on the hot path).
91
+ # Unknown directives raise here, at configuration time, not per log.
92
+ @tokens = compile(pattern)
93
+ end
94
+
95
+ # Formatted timestamp. With a strftime format argument, e.g.
96
+ # %{time:%Y-%m-%dT%H:%M:%S.%6N}, the time is formatted with that string.
97
+ # Without an argument it uses the formatter's configured time_format.
98
+ def time(format = nil)
99
+ return super() if format.nil?
100
+
101
+ log.time.strftime(format)
102
+ end
103
+
104
+ # Full level name, e.g. "debug" (Default formatter uses the short "D").
105
+ def level
106
+ log.level.to_s
107
+ end
108
+
109
+ # Single character level, e.g. "D".
110
+ def level_short
111
+ log.level_to_s
112
+ end
113
+
114
+ # Log message (without the "-- " prefix the Default formatter adds).
115
+ def message
116
+ escape_control_characters(log.message)
117
+ end
118
+
119
+ # Raw payload rendered as a string.
120
+ def payload
121
+ log.payload_to_s
122
+ end
123
+
124
+ # Class of the logged exception, e.g. "RuntimeError".
125
+ def exception_class
126
+ log.exception&.class
127
+ end
128
+
129
+ # Message of the logged exception.
130
+ def exception_message
131
+ escape_control_characters(log.exception&.message)
132
+ end
133
+
134
+ # Backtrace of the logged exception.
135
+ def backtrace
136
+ log.backtrace_to_s if log.exception
137
+ end
138
+
139
+ # Human readable duration (without the Default formatter's surrounding parentheses).
140
+ def duration
141
+ log.duration_human
142
+ end
143
+
144
+ # Duration in milliseconds.
145
+ def duration_ms
146
+ log.duration
147
+ end
148
+
149
+ # Name of the Ruby file that logged the message, e.g. "app.rb".
150
+ def file_name
151
+ log.file_name_and_line(true)&.first
152
+ end
153
+
154
+ # Line number within the Ruby file that logged the message.
155
+ def line
156
+ log.file_name_and_line(true)&.last
157
+ end
158
+
159
+ # Tags joined by a comma (without the Default formatter's surrounding brackets).
160
+ def tags
161
+ return if log.tags.nil? || log.tags.empty?
162
+
163
+ log.tags.map { |tag| escape_control_characters(tag) }.join(", ")
164
+ end
165
+
166
+ # With a key: the value of a single named tag, e.g. %{named_tags:request_id}.
167
+ # Without a key: all named tags rendered as "key: value, ...".
168
+ def named_tags(key = nil)
169
+ named = log.named_tags
170
+ return if named.nil? || named.empty?
171
+
172
+ if key
173
+ escape_control_characters(named[key.to_sym] || named[key.to_s])
174
+ else
175
+ named.map { |name, value| "#{escape_control_characters(name)}: #{escape_control_characters(value)}" }.join(", ")
176
+ end
177
+ end
178
+
179
+ # Host name.
180
+ def host
181
+ logger&.host if log_host
182
+ end
183
+
184
+ # Application name.
185
+ def application
186
+ logger&.application if log_application
187
+ end
188
+
189
+ # Environment name.
190
+ def environment
191
+ logger&.environment if log_environment
192
+ end
193
+
194
+ def call(log, logger)
195
+ self.log = log
196
+ self.logger = logger
197
+
198
+ @tokens.each_with_object(+"") do |token, out|
199
+ out << (token.is_a?(Token) ? public_send(token.method_name, *token.arguments).to_s : token)
200
+ end
201
+ end
202
+
203
+ private
204
+
205
+ # Parse the pattern string into an array of tokens: frozen literal strings
206
+ # and Token structs for each %{directive} placeholder. %%{...} is an escape
207
+ # that produces a literal %{...}.
208
+ def compile(string)
209
+ tokens = []
210
+ pos = 0
211
+
212
+ string.scan(/%%?\{[^}]+\}/) do |match|
213
+ current = Regexp.last_match
214
+ tokens << string[pos...current.begin(0)].freeze if current.begin(0) > pos
215
+
216
+ if match.start_with?("%%")
217
+ tokens << match[1..].freeze
218
+ else
219
+ name, arg = match[/\{([^}]+)\}/, 1].split(":", 2)
220
+ name = name.strip.to_sym
221
+ raise(ArgumentError, "Invalid pattern directive: %{#{name}}") unless DIRECTIVES.key?(name)
222
+ raise(ArgumentError, "%{#{name}} does not accept an argument") if arg && !DIRECTIVES[name]
223
+
224
+ tokens << Token.new(name, arg ? [arg.strip] : []).freeze
225
+ end
226
+
227
+ pos = current.end(0)
228
+ end
229
+
230
+ tokens << string[pos..].freeze if pos < string.length
231
+ tokens
232
+ end
233
+ end
234
+ end
235
+ end
@@ -18,12 +18,12 @@ module SemanticLogger
18
18
 
19
19
  # Application name
20
20
  def application
21
- hash[:application] = logger.application if log_application && logger && logger.application
21
+ hash[:application] = logger.application if log_application && logger&.application
22
22
  end
23
23
 
24
24
  # Environment
25
25
  def environment
26
- hash[:environment] = logger.environment if log_environment && logger && logger.environment
26
+ hash[:environment] = logger.environment if log_environment && logger&.environment
27
27
  end
28
28
 
29
29
  # Date & time
@@ -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"
@@ -93,6 +93,11 @@ module SemanticLogger
93
93
  self.dimensions = dimensions
94
94
 
95
95
  if exception
96
+ unless exception.is_a?(Exception)
97
+ exception = ArgumentError.new("Invalid value for logger exception: #{exception.inspect}")
98
+ exception.set_backtrace(Utils.extract_backtrace(caller))
99
+ end
100
+
96
101
  case log_exception
97
102
  when :full
98
103
  self.exception = exception
@@ -124,11 +129,17 @@ module SemanticLogger
124
129
  true
125
130
  end
126
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
+
127
138
  # Assign known keys to self, all other keys to the payload.
128
139
  def assign_hash(hash)
129
140
  self.payload ||= {}
130
141
  hash.each_pair do |key, value|
131
- if respond_to?(:"#{key}=")
142
+ if ASSIGNABLE_KEYS.include?(key) && respond_to?(:"#{key}=")
132
143
  public_send(:"#{key}=", value)
133
144
  else
134
145
  payload[key] = value
@@ -245,7 +256,7 @@ module SemanticLogger
245
256
  "#{$$}:#{format("%.#{thread_name_length}s", thread_name)}#{file_name}"
246
257
  end
247
258
 
248
- CALLER_REGEXP = /^(.*):(\d+).*/.freeze
259
+ CALLER_REGEXP = /^(.*):(\d+).*/
249
260
 
250
261
  # Extract the filename and line number from the last entry in the supplied backtrace
251
262
  def extract_file_and_line(stack, short_name = false)
@@ -264,9 +275,17 @@ module SemanticLogger
264
275
  extract_file_and_line(stack, short_name)
265
276
  end
266
277
 
267
- # 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.
268
284
  def cleansed_message
269
- 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
270
289
  end
271
290
 
272
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
@@ -23,7 +23,7 @@ module SemanticLogger
23
23
 
24
24
  attr_reader :appenders
25
25
 
26
- def initialize(max_queue_size: -1)
26
+ def initialize(max_queue_size: 10_000)
27
27
  @appenders = Appenders.new(self.class.logger.dup)
28
28
  super(appender: @appenders, max_queue_size: max_queue_size)
29
29
  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