semantic_logger 4.18.0 → 5.1.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +51 -81
  3. data/Rakefile +39 -3
  4. data/docs/api.md +480 -0
  5. data/docs/appenders.md +990 -0
  6. data/docs/config.md +605 -0
  7. data/docs/index.md +197 -0
  8. data/docs/log.md +108 -0
  9. data/docs/metrics.md +168 -0
  10. data/docs/operations.md +400 -0
  11. data/docs/rails.md +816 -0
  12. data/docs/security.md +119 -0
  13. data/docs/testing.md +285 -0
  14. data/docs/upgrading.md +409 -0
  15. data/lib/semantic_logger/appender/async.rb +86 -173
  16. data/lib/semantic_logger/appender/cloudwatch_logs.rb +8 -6
  17. data/lib/semantic_logger/appender/elasticsearch.rb +8 -187
  18. data/lib/semantic_logger/appender/elasticsearch_base.rb +212 -0
  19. data/lib/semantic_logger/appender/elasticsearch_http.rb +16 -9
  20. data/lib/semantic_logger/appender/file.rb +25 -4
  21. data/lib/semantic_logger/appender/graylog.rb +2 -2
  22. data/lib/semantic_logger/appender/http.rb +27 -2
  23. data/lib/semantic_logger/appender/io.rb +8 -4
  24. data/lib/semantic_logger/appender/kafka.rb +2 -2
  25. data/lib/semantic_logger/appender/loki.rb +2 -4
  26. data/lib/semantic_logger/appender/mongodb.rb +3 -6
  27. data/lib/semantic_logger/appender/new_relic.rb +6 -0
  28. data/lib/semantic_logger/appender/new_relic_logs.rb +4 -3
  29. data/lib/semantic_logger/appender/open_telemetry.rb +10 -7
  30. data/lib/semantic_logger/appender/opensearch.rb +36 -0
  31. data/lib/semantic_logger/appender/rabbitmq.rb +5 -4
  32. data/lib/semantic_logger/appender/sentry.rb +5 -0
  33. data/lib/semantic_logger/appender/sentry_ruby.rb +2 -2
  34. data/lib/semantic_logger/appender/splunk.rb +2 -2
  35. data/lib/semantic_logger/appender/splunk_http.rb +3 -3
  36. data/lib/semantic_logger/appender/syslog.rb +11 -7
  37. data/lib/semantic_logger/appender/tcp.rb +4 -3
  38. data/lib/semantic_logger/appender/udp.rb +2 -2
  39. data/lib/semantic_logger/appender/wrapper.rb +5 -4
  40. data/lib/semantic_logger/appender.rb +36 -22
  41. data/lib/semantic_logger/appenders.rb +27 -5
  42. data/lib/semantic_logger/base.rb +100 -23
  43. data/lib/semantic_logger/concerns/compatibility.rb +2 -2
  44. data/lib/semantic_logger/core_ext/process.rb +34 -0
  45. data/lib/semantic_logger/formatters/base.rb +46 -7
  46. data/lib/semantic_logger/formatters/color.rb +9 -3
  47. data/lib/semantic_logger/formatters/default.rb +11 -4
  48. data/lib/semantic_logger/formatters/ecs.rb +151 -0
  49. data/lib/semantic_logger/formatters/fluentd.rb +15 -4
  50. data/lib/semantic_logger/formatters/json.rb +6 -1
  51. data/lib/semantic_logger/formatters/logfmt.rb +6 -4
  52. data/lib/semantic_logger/formatters/loki.rb +4 -4
  53. data/lib/semantic_logger/formatters/open_telemetry.rb +15 -5
  54. data/lib/semantic_logger/formatters/pattern.rb +237 -0
  55. data/lib/semantic_logger/formatters/raw.rb +2 -2
  56. data/lib/semantic_logger/formatters/signalfx.rb +2 -2
  57. data/lib/semantic_logger/formatters/syslog.rb +16 -4
  58. data/lib/semantic_logger/formatters/syslog_cee.rb +3 -2
  59. data/lib/semantic_logger/formatters.rb +2 -0
  60. data/lib/semantic_logger/log.rb +18 -4
  61. data/lib/semantic_logger/logger.rb +2 -2
  62. data/lib/semantic_logger/metric/new_relic.rb +2 -2
  63. data/lib/semantic_logger/metric/signalfx.rb +6 -4
  64. data/lib/semantic_logger/metric/statsd.rb +2 -2
  65. data/lib/semantic_logger/processor.rb +21 -0
  66. data/lib/semantic_logger/queue_processor.rb +372 -0
  67. data/lib/semantic_logger/reporters/minitest.rb +8 -8
  68. data/lib/semantic_logger/semantic_logger.rb +103 -11
  69. data/lib/semantic_logger/subscriber.rb +15 -2
  70. data/lib/semantic_logger/sync_processor.rb +25 -3
  71. data/lib/semantic_logger/test/capture_log_events.rb +3 -3
  72. data/lib/semantic_logger/test/minitest.rb +8 -4
  73. data/lib/semantic_logger/test/rspec.rb +249 -0
  74. data/lib/semantic_logger/utils.rb +83 -4
  75. data/lib/semantic_logger/version.rb +1 -1
  76. data/lib/semantic_logger.rb +9 -0
  77. metadata +28 -8
  78. data/lib/semantic_logger/appender/async_batch.rb +0 -93
@@ -10,7 +10,7 @@ module SemanticLogger
10
10
  self.logger = logger
11
11
  self.log = log
12
12
 
13
- {streams: [build_stream]}.to_json
13
+ Utils.to_json({streams: [build_stream]})
14
14
  end
15
15
 
16
16
  # Returns [String] a JSON batch of logs
@@ -22,7 +22,7 @@ module SemanticLogger
22
22
  build_stream
23
23
  end
24
24
 
25
- {streams: streams}.to_json
25
+ Utils.to_json({streams: streams})
26
26
  end
27
27
 
28
28
  private
@@ -82,7 +82,7 @@ module SemanticLogger
82
82
 
83
83
  log.context.each do |key, value|
84
84
  serialized_value = if value.is_a?(Hash)
85
- value.to_json
85
+ Utils.to_json(value)
86
86
  else
87
87
  value.to_s
88
88
  end
@@ -144,7 +144,7 @@ module SemanticLogger
144
144
 
145
145
  result[string_key] = case value
146
146
  when Hash
147
- JSON.generate(stringify_hash(value))
147
+ Utils.to_json(stringify_hash(value))
148
148
  else
149
149
  value.to_s
150
150
  end
@@ -5,10 +5,19 @@ module SemanticLogger
5
5
  # primitives allowed by OTLP logs in Ruby: String, Integer, Float, TrueClass, FalseClass
6
6
  PRIMS = [String, Integer, Float, TrueClass, FalseClass].freeze
7
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
+
8
17
  # Log level
9
18
  def level
10
19
  hash[:level] = log.level.to_s
11
- hash[:level_index] = severity_number(log.level_index)
20
+ hash[:level_index] = severity_number(log.level)
12
21
  end
13
22
 
14
23
  # Payload is submitted directly as attributes
@@ -35,10 +44,11 @@ module SemanticLogger
35
44
 
36
45
  out[k.to_s] =
37
46
  if v.is_a?(Hash)
38
- # Stringify whole hash.
39
- v.transform_values { |vv| coerce_value(vv) }.
40
- transform_keys!(&:to_s).
41
- to_json
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
+ )
42
52
  else
43
53
  coerce_value(v)
44
54
  end
@@ -0,0 +1,237 @@
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 do |name, value|
176
+ "#{escape_control_characters(name)}: #{escape_control_characters(value)}"
177
+ end.join(", ")
178
+ end
179
+ end
180
+
181
+ # Host name.
182
+ def host
183
+ logger&.host if log_host
184
+ end
185
+
186
+ # Application name.
187
+ def application
188
+ logger&.application if log_application
189
+ end
190
+
191
+ # Environment name.
192
+ def environment
193
+ logger&.environment if log_environment
194
+ end
195
+
196
+ def call(log, logger)
197
+ self.log = log
198
+ self.logger = logger
199
+
200
+ @tokens.each_with_object(+"") do |token, out|
201
+ out << (token.is_a?(Token) ? public_send(token.method_name, *token.arguments).to_s : token)
202
+ end
203
+ end
204
+
205
+ private
206
+
207
+ # Parse the pattern string into an array of tokens: frozen literal strings
208
+ # and Token structs for each %{directive} placeholder. %%{...} is an escape
209
+ # that produces a literal %{...}.
210
+ def compile(string)
211
+ tokens = []
212
+ pos = 0
213
+
214
+ string.scan(/%%?\{[^}]+\}/) do |match|
215
+ current = Regexp.last_match
216
+ tokens << string[pos...current.begin(0)].freeze if current.begin(0) > pos
217
+
218
+ if match.start_with?("%%")
219
+ tokens << match[1..].freeze
220
+ else
221
+ name, arg = match[/\{([^}]+)\}/, 1].split(":", 2)
222
+ name = name.strip.to_sym
223
+ raise(ArgumentError, "Invalid pattern directive: %{#{name}}") unless DIRECTIVES.key?(name)
224
+ raise(ArgumentError, "%{#{name}} does not accept an argument") if arg && !DIRECTIVES[name]
225
+
226
+ tokens << Token.new(name, arg ? [arg.strip] : []).freeze
227
+ end
228
+
229
+ pos = current.end(0)
230
+ end
231
+
232
+ tokens << string[pos..].freeze if pos < string.length
233
+ tokens
234
+ end
235
+ end
236
+ end
237
+ 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
@@ -2,7 +2,8 @@ begin
2
2
  require "syslog_protocol"
3
3
  rescue LoadError
4
4
  raise LoadError,
5
- 'Gem syslog_protocol is required for remote logging using the Syslog protocol. Please add the gem "syslog_protocol" to your Gemfile.'
5
+ "Gem syslog_protocol is required for remote logging using the Syslog protocol. " \
6
+ 'Please add the gem "syslog_protocol" to your Gemfile.'
6
7
  end
7
8
 
8
9
  module SemanticLogger
@@ -49,14 +50,25 @@ module SemanticLogger
49
50
  # level_map: [Hash | SemanticLogger::Formatters::Syslog::LevelMap]
50
51
  # Supply a custom map of SemanticLogger levels to syslog levels.
51
52
  #
53
+ # escape_control_chars: [Boolean]
54
+ # Replace control characters (newlines, the ANSI escape, etc.) in
55
+ # untrusted log data with a printable escaped form so that they cannot
56
+ # forge or split syslog records.
57
+ # Default: true (unlike other formatters, since syslog frames records
58
+ # with a separator)
59
+ #
52
60
  # Example:
53
61
  # # Change the warn level to LOG_NOTICE level instead of a the default of LOG_WARNING.
54
62
  # 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)
63
+ def initialize(facility: ::Syslog::LOG_USER, level_map: LevelMap.new, max_size: Integer,
64
+ escape_control_chars: true, **args)
56
65
  @facility = facility
57
- @level_map = level_map.is_a?(LevelMap) ? level_map : LevelMap.new(level_map)
66
+ @level_map = level_map.is_a?(LevelMap) ? level_map : LevelMap.new(**level_map)
58
67
  @max_size = max_size
59
- super()
68
+ # Syslog frames records with a separator, so embedded newlines or other
69
+ # control characters in untrusted log data can forge or split records.
70
+ # Default to escaping them, overridable for backwards compatibility.
71
+ super(escape_control_chars: escape_control_chars, **args)
60
72
  end
61
73
 
62
74
  # Time is part of the syslog packet and is not included in the formatted message.
@@ -2,7 +2,8 @@ begin
2
2
  require "syslog_protocol"
3
3
  rescue LoadError
4
4
  raise LoadError,
5
- 'Gem syslog_protocol is required for remote logging using the Syslog protocol. Please add the gem "syslog_protocol" to your Gemfile.'
5
+ "Gem syslog_protocol is required for remote logging using the Syslog protocol. " \
6
+ 'Please add the gem "syslog_protocol" to your Gemfile.'
6
7
  end
7
8
 
8
9
  module SemanticLogger
@@ -38,7 +39,7 @@ module SemanticLogger
38
39
 
39
40
  def call(log, logger)
40
41
  hash = super
41
- create_syslog_packet("@cee: #{hash.to_json}")
42
+ create_syslog_packet("@cee: #{Utils.to_json(hash)}")
42
43
  end
43
44
 
44
45
  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.
@@ -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, dimensions: {class: 'Common::User', action: 'authorize'})
70
- # logger.info(metric: 'Application.counter', metric_amount: 1, dimensions: {class: 'Common::User', action: 'authorize'})
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
@@ -78,10 +80,10 @@ module SemanticLogger
78
80
  url: "https://ingest.signalfx.com",
79
81
  formatter: nil,
80
82
  **args,
81
- &block)
83
+ &)
82
84
  formatter ||= SemanticLogger::Formatters::Signalfx.new(token: token, dimensions: dimensions)
83
85
 
84
- super(url: url, formatter: formatter, **args, &block)
86
+ super(url: url, formatter: formatter, **args, &)
85
87
 
86
88
  @header["X-SF-TOKEN"] = token
87
89
  @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