semantic_logger 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -10
  3. data/Rakefile +40 -0
  4. data/docs/api.md +506 -0
  5. data/docs/appenders.md +1074 -0
  6. data/docs/config.md +707 -0
  7. data/docs/index.md +223 -0
  8. data/docs/log.md +116 -0
  9. data/docs/metrics.md +171 -0
  10. data/docs/operations.md +446 -0
  11. data/docs/rails.md +890 -0
  12. data/docs/security.md +286 -0
  13. data/docs/testing.md +287 -0
  14. data/docs/upgrading.md +697 -0
  15. data/lib/semantic_logger/appender/async.rb +18 -5
  16. data/lib/semantic_logger/appender/bugsnag.rb +1 -1
  17. data/lib/semantic_logger/appender/cloudwatch_logs.rb +126 -29
  18. data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
  19. data/lib/semantic_logger/appender/elasticsearch_base.rb +48 -23
  20. data/lib/semantic_logger/appender/elasticsearch_http.rb +15 -8
  21. data/lib/semantic_logger/appender/file.rb +141 -28
  22. data/lib/semantic_logger/appender/graylog.rb +22 -6
  23. data/lib/semantic_logger/appender/honeybadger.rb +1 -1
  24. data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
  25. data/lib/semantic_logger/appender/http.rb +70 -11
  26. data/lib/semantic_logger/appender/io.rb +1 -1
  27. data/lib/semantic_logger/appender/kafka.rb +26 -12
  28. data/lib/semantic_logger/appender/loki.rb +13 -3
  29. data/lib/semantic_logger/appender/mongodb.rb +20 -1
  30. data/lib/semantic_logger/appender/new_relic.rb +7 -1
  31. data/lib/semantic_logger/appender/new_relic_logs.rb +11 -12
  32. data/lib/semantic_logger/appender/open_telemetry.rb +4 -3
  33. data/lib/semantic_logger/appender/opensearch.rb +2 -1
  34. data/lib/semantic_logger/appender/rabbitmq.rb +10 -4
  35. data/lib/semantic_logger/appender/sentry.rb +6 -1
  36. data/lib/semantic_logger/appender/sentry_ruby.rb +91 -17
  37. data/lib/semantic_logger/appender/splunk.rb +10 -1
  38. data/lib/semantic_logger/appender/syslog.rb +66 -25
  39. data/lib/semantic_logger/appender/tcp.rb +10 -4
  40. data/lib/semantic_logger/appender/udp.rb +11 -1
  41. data/lib/semantic_logger/appender/wrapper.rb +3 -2
  42. data/lib/semantic_logger/appender.rb +6 -3
  43. data/lib/semantic_logger/appenders.rb +11 -2
  44. data/lib/semantic_logger/base.rb +82 -32
  45. data/lib/semantic_logger/exception_details.rb +50 -0
  46. data/lib/semantic_logger/formatters/base.rb +52 -6
  47. data/lib/semantic_logger/formatters/color.rb +22 -3
  48. data/lib/semantic_logger/formatters/default.rb +28 -6
  49. data/lib/semantic_logger/formatters/logfmt.rb +155 -21
  50. data/lib/semantic_logger/formatters/loki.rb +26 -22
  51. data/lib/semantic_logger/formatters/one_line.rb +67 -3
  52. data/lib/semantic_logger/formatters/open_telemetry.rb +16 -17
  53. data/lib/semantic_logger/formatters/pattern.rb +11 -5
  54. data/lib/semantic_logger/formatters/raw.rb +4 -5
  55. data/lib/semantic_logger/formatters/syslog.rb +110 -17
  56. data/lib/semantic_logger/formatters/syslog_cee.rb +29 -15
  57. data/lib/semantic_logger/levels.rb +10 -1
  58. data/lib/semantic_logger/log.rb +183 -37
  59. data/lib/semantic_logger/loggable.rb +26 -3
  60. data/lib/semantic_logger/metric/new_relic.rb +1 -1
  61. data/lib/semantic_logger/metric/signalfx.rb +12 -8
  62. data/lib/semantic_logger/metric/statsd.rb +15 -7
  63. data/lib/semantic_logger/queue_processor/request.rb +55 -0
  64. data/lib/semantic_logger/queue_processor/throttled_counter.rb +46 -0
  65. data/lib/semantic_logger/queue_processor.rb +170 -68
  66. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  67. data/lib/semantic_logger/semantic_logger.rb +15 -2
  68. data/lib/semantic_logger/subscriber.rb +96 -10
  69. data/lib/semantic_logger/sync_processor.rb +11 -0
  70. data/lib/semantic_logger/test/capture_log_events.rb +9 -6
  71. data/lib/semantic_logger/utils.rb +244 -16
  72. data/lib/semantic_logger/version.rb +1 -1
  73. data/lib/semantic_logger.rb +1 -1
  74. metadata +33 -6
  75. data/lib/semantic_logger/core_ext/thread.rb +0 -30
@@ -7,11 +7,6 @@ module SemanticLogger
7
7
  attr_reader :formatter
8
8
  attr_writer :application, :environment, :host, :logger, :metrics
9
9
 
10
- # Returns the current log level if set, otherwise it logs everything it receives.
11
- def level
12
- @level || :trace
13
- end
14
-
15
10
  # A subscriber should implement flush if it can.
16
11
  def flush
17
12
  # NOOP
@@ -84,6 +79,26 @@ module SemanticLogger
84
79
  !console_stream.nil?
85
80
  end
86
81
 
82
+ # Returns [String] the class and object address of this appender, like Kernel#inspect, followed
83
+ # by #inspect_details, but none of its instance variables.
84
+ #
85
+ # Appenders hold credentials in their instance variables: passwords and urls holding them, api
86
+ # keys and tokens, request headers, private keys, and the options passed to a client gem.
87
+ # Printing SemanticLogger.appenders, for example in a console or an error report, must not
88
+ # show them.
89
+ def inspect
90
+ address = Kernel.instance_method(:to_s).bind_call(self).delete_suffix(">")
91
+ details = inspect_details
92
+ details ? "#{address} #{details}>" : "#{address}>"
93
+ end
94
+
95
+ # Returns [String] what #inspect shows about this appender, such as its url without its
96
+ # credentials, or nil for nothing. Only an appender knows which of its settings are safe to
97
+ # show, so each one overrides this.
98
+ def inspect_details
99
+ nil
100
+ end
101
+
87
102
  # Whether an appender that implements #batch should be wrapped in a batch
88
103
  # proxy automatically. Appenders that support batching but should only batch
89
104
  # when explicitly requested (via `batch: true`) override this to return false.
@@ -91,8 +106,77 @@ module SemanticLogger
91
106
  true
92
107
  end
93
108
 
109
+ # Raised by #format_log and #format_batch when the formatter fails, with the formatter's
110
+ # error as its cause. A formatter has no destination, so it can only fail because of the
111
+ # content of the log message, whatever it raises.
112
+ class FormatError < StandardError; end
113
+
114
+ # Errors raised by writing a log message whose content cannot be written, such as a
115
+ # value that cannot be serialized. Excludes JSON::ParserError, which comes from parsing a
116
+ # response rather than from the message, except its subclass JSON::NestingError, which
117
+ # JSON.generate raises for a value nested too deeply.
118
+ MESSAGE_ERRORS = [FormatError, JSON::GeneratorError, JSON::NestingError, EncodingError, FloatDomainError].freeze
119
+
120
+ # Whether the supplied exception, raised by #log or #batch, was caused by the content of a
121
+ # log message, rather than by the destination, for example being unavailable.
122
+ #
123
+ # Retrying cannot write such a message, so an asynchronous appender discards it and carries
124
+ # on, instead of backing off and retrying. Any other exception is retried with a back-off.
125
+ #
126
+ # Every error raised by the formatter is one (see #format_log). An appender whose client
127
+ # raises its own errors for bad content can override this to recognize them.
128
+ def message_error?(exception)
129
+ MESSAGE_ERRORS.any? { |error_class| exception.is_a?(error_class) }
130
+ end
131
+
132
+ # Whether #batch writes none of the log messages when it raises a message error (see
133
+ # #message_error?), for example because it formats every message before sending any.
134
+ #
135
+ # When true, a batch that fails with a message error is split up and each part retried,
136
+ # so that only the offending message is discarded. When false, retrying could write some
137
+ # messages twice, so the whole batch is discarded instead.
138
+ def batch_all_or_nothing?
139
+ false
140
+ end
141
+
94
142
  private
95
143
 
144
+ # Returns the log message formatted by this appender's formatter.
145
+ #
146
+ # Any StandardError the formatter raises is raised as a FormatError, with the formatter's
147
+ # error as its cause, so that #message_error? recognizes it: an asynchronous appender then
148
+ # discards the message instead of backing off. Appenders call this rather than formatter.call.
149
+ #
150
+ # A SystemStackError is raised as it is, so that rescuing StandardError does not swallow it:
151
+ # the thread that overflowed must not keep writing (see Appenders#log).
152
+ #
153
+ # The block, when given, turns the formatter's output into what the appender sends, such as
154
+ # its JSON. An error doing so is also caused by the content of the message.
155
+ def format_log(log)
156
+ formatted = formatter.call(log, self)
157
+ block_given? ? yield(formatted) : formatted
158
+ rescue StandardError => e
159
+ raise FormatError, "Could not format the log message (#{e.class})"
160
+ end
161
+
162
+ # Returns the log messages formatted by the formatter's #batch, as for #format_log.
163
+ #
164
+ # A formatter that cannot format a batch is a configuration error rather than bad content, so
165
+ # it raises ArgumentError instead, which is retried with a back-off and reported like any
166
+ # other. That includes an appender that is its own formatter (it implements #call), whose
167
+ # #batch is the appender's own.
168
+ def format_batch(logs)
169
+ unless formatter.respond_to?(:batch) && !formatter.equal?(self)
170
+ raise(ArgumentError, "#{formatter.class.name} cannot format a batch. It must implement #batch(logs, logger)")
171
+ end
172
+
173
+ begin
174
+ formatter.batch(logs, self)
175
+ rescue StandardError => e
176
+ raise FormatError, "Could not format the batch of log messages (#{e.class})"
177
+ end
178
+ end
179
+
96
180
  # Initializer for Abstract Class SemanticLogger::Subscriber
97
181
  #
98
182
  # Parameters
@@ -134,11 +218,13 @@ module SemanticLogger
134
218
  super(self.class, level, filter)
135
219
  end
136
220
 
137
- # Return the level index for fast comparisons.
138
- # Returns the lowest level index if the level has not been explicitly
139
- # set for this instance.
140
- def level_index
141
- @level_index || 0
221
+ # Without a level of its own, a subscriber logs everything it receives.
222
+ def fallback_level
223
+ :trace
224
+ end
225
+
226
+ def fallback_level_index
227
+ 0
142
228
  end
143
229
 
144
230
  # Whether to log metric only entries with this subscriber
@@ -8,11 +8,16 @@ module SemanticLogger
8
8
  @monitor.synchronize { @appenders.add(...) }
9
9
  end
10
10
 
11
+ # Logs on the calling thread, which belongs to the application, so a stack overflow while
12
+ # writing (see Appenders#log) is reported rather than raised from the log call.
11
13
  def log(...)
12
14
  @monitor.synchronize do
13
15
  @processed_count += 1
14
16
  @appenders.log(...)
15
17
  end
18
+ rescue SystemStackError => e
19
+ @appenders.logger.error("Failed to log a message: the stack overflowed", e)
20
+ false
16
21
  end
17
22
 
18
23
  # Returns [Hash] operational statistics for the logging pipeline.
@@ -67,6 +72,12 @@ module SemanticLogger
67
72
 
68
73
  attr_reader :appenders
69
74
 
75
+ # Returns the internal logger of the appenders, for reporting a problem while logging, such as
76
+ # an exception raised by an on_log subscriber. The same logger as the asynchronous Processor's.
77
+ def logger
78
+ @appenders.logger
79
+ end
80
+
70
81
  def initialize(appenders = nil)
71
82
  @monitor = Monitor.new
72
83
  @appenders = appenders || Appenders.new(self.class.logger.dup)
@@ -24,7 +24,7 @@ module SemanticLogger
24
24
 
25
25
  # By default collect all log levels, and collect metric only log events.
26
26
  def initialize(level: :trace, metrics: true)
27
- super(level: level, metrics: true)
27
+ super
28
28
  @events = []
29
29
  end
30
30
 
@@ -42,14 +42,17 @@ module SemanticLogger
42
42
  @events.clear
43
43
  end
44
44
 
45
- # Support silencing of log messages
46
- def level_index
47
- @level_index || SemanticLogger.default_level_index
48
- end
49
-
50
45
  def to_h
51
46
  events.map(&:to_h)
52
47
  end
48
+
49
+ private
50
+
51
+ # Without a level of its own, follow the global default level the way a logger does,
52
+ # so that `SemanticLogger.silence` applies.
53
+ def fallback_level_index
54
+ SemanticLogger.default_level_index
55
+ end
53
56
  end
54
57
  end
55
58
  end
@@ -1,4 +1,5 @@
1
1
  require "json"
2
+ require "uri"
2
3
 
3
4
  module SemanticLogger
4
5
  # Internal-use only utility functions for Semantic Logger.
@@ -25,6 +26,38 @@ module SemanticLogger
25
26
  constant
26
27
  end
27
28
 
29
+ # Returns [URI] the parsed url, supplied as the named option.
30
+ #
31
+ # Raises URI::InvalidURIError for an invalid url, including nil, as .without_url_in_errors does.
32
+ def self.parse_url(url, option: ":url", hint: nil)
33
+ without_url_in_errors(option: option, hint: hint) { URI.parse(url) }
34
+ end
35
+
36
+ # Returns the result of the block, which uses the url supplied as the named option, for example
37
+ # to build a client gem's connection.
38
+ #
39
+ # Raises any of the errors from the block again, of the same class, without the url in its
40
+ # message, or as its cause, since a url can hold a user and password, or a token in its path or
41
+ # query string, and URI and client gems put the url in the error for an invalid one.
42
+ def self.without_url_in_errors(option: ":url", hint: nil, errors: [URI::InvalidURIError])
43
+ yield
44
+ rescue *errors => e
45
+ raise(e.class, ["Invalid #{option}, it is not a valid URI.", hint].compact.join(" "), cause: nil)
46
+ end
47
+
48
+ # Returns [String] the scheme, host, and any non-default port of the url, which is safe to log.
49
+ #
50
+ # Everything else is left out, since log services carry credentials in each part of a url: the
51
+ # user and password, the query string (for example a Datadog api key), and the path (for example
52
+ # the token at the end of a Sumo Logic HTTP source url). An invalid url is not shown at all.
53
+ def self.redacted_url(url)
54
+ uri = URI.parse(url.to_s)
55
+ port = uri.port == uri.default_port ? nil : uri.port
56
+ uri.class.build(scheme: uri.scheme, host: uri.host, port: port).to_s
57
+ rescue URI::Error
58
+ "(invalid url)"
59
+ end
60
+
28
61
  # Borrow from Rails, when not running Rails
29
62
  def self.camelize(term)
30
63
  string = term.to_s
@@ -48,11 +81,9 @@ module SemanticLogger
48
81
 
49
82
  # Extract the backtrace stripping off the leading semantic logger entries.
50
83
  # Leaves all other system and gem path entries in place.
84
+ # Returns a new Array, leaving the supplied stack unchanged since it can belong to the caller.
51
85
  def self.extract_backtrace(stack = caller)
52
- while (first = stack.first) && extract_path?(first)
53
- stack.shift
54
- end
55
- stack
86
+ stack.drop_while { |path| extract_path?(path) }
56
87
  end
57
88
 
58
89
  def self.extract_paths
@@ -90,24 +121,221 @@ module SemanticLogger
90
121
  strip_paths.any? { |exclude| path.start_with?(exclude) }
91
122
  end
92
123
 
93
- # Serializes the value to JSON, repairing invalid UTF-8 only when necessary.
124
+ # Serializes the value to JSON, repairing the data only when necessary.
94
125
  #
95
- # Non UTF-8 data appears in well under 1% of log events, so it is wasteful to
96
- # walk and reallocate the entire structure (see .encode_utf8) on every call.
97
- # Instead this attempts `.to_json` directly and only falls back to cleansing
98
- # when serialization fails because of an encoding problem.
126
+ # Data that JSON cannot represent appears in well under 1% of log events, so it
127
+ # is wasteful to walk and reallocate the entire structure (see .json_safe) on
128
+ # every call. Instead this attempts `.to_json` directly and only falls back to a
129
+ # repaired copy when serialization fails because of the data:
130
+ # * Invalid UTF-8. Depending on the json gem version this raises
131
+ # Encoding::UndefinedConversionError (an EncodingError) or JSON::GeneratorError.
132
+ # * Infinity and NaN, which raise JSON::GeneratorError.
133
+ # * Structures nested deeper than the json gem allows, which raise
134
+ # JSON::NestingError (a JSON::ParserError, not a GeneratorError).
135
+ # * A Struct, Data, Set, or OpenStruct instance. The json gem writes one as the String
136
+ # returned by its #to_s, which for each is #inspect: every member is written with
137
+ # #inspect, exposing the instance variables of any object it holds, and without a
138
+ # depth limit, so a deep enough structure raises SystemStackError. The output is
139
+ # checked for that rendering (a JSON String starting with "#<", or "Set[" for a Set on
140
+ # Ruby 4.0), and SystemStackError is rescued, so .json_safe can write the members as a JSON object
141
+ # or array instead. The json gem offers no hook for these short of strict mode,
142
+ # which also rejects common values such as Time, and finding one up front would mean
143
+ # walking every payload on the hot path. ActiveSupport already writes them through
144
+ # #as_json, and escapes "<", so its output never matches the check.
99
145
  #
100
- # The exception raised for non UTF-8 data depends on the json gem version:
101
- # older versions raise Encoding::UndefinedConversionError (an EncodingError),
102
- # newer versions wrap it as JSON::GeneratorError, so both are rescued. The
103
- # retry is attempted only once: if it still fails (for example a
104
- # JSON::GeneratorError caused by something other than encoding, such as NaN),
105
- # the error propagates unchanged rather than being swallowed.
146
+ # Log data can come from untrusted input, for example a float parsed from a request
147
+ # parameter or a deeply nested request body. Without the repair, such a value would
148
+ # make the whole log event impossible to serialize, and so silently dropped.
149
+ #
150
+ # The retry is attempted only once: if it still fails (for example an object whose
151
+ # own #to_json raises), the error propagates unchanged rather than being swallowed.
106
152
  def self.to_json(value)
153
+ clean_json(value) || json_safe(value).to_json
154
+ end
155
+
156
+ # Returns the value when JSON writes it as it is, otherwise a .json_safe copy, for a writer
157
+ # that serializes the value itself, such as GELF. The check costs one #to_json; only the
158
+ # rare value that needs repairing is copied.
159
+ def self.json_ready(value)
160
+ clean_json(value) ? value : json_safe(value)
161
+ end
162
+
163
+ # Returns [String] the value's JSON, or nil when .to_json has to repair the data.
164
+ def self.clean_json(value)
165
+ json = value.to_json
166
+ # Almost no JSON holds a "<", and Ruby finds one byte several times faster than a longer
167
+ # substring, so it is looked for first. A String that merely starts with the same text is
168
+ # also repaired; that only costs time, since .json_safe leaves such a String as it is.
169
+ return if json.include?("<") && json.include?(INSPECTED_OBJECT)
170
+ return if INSPECTED_SET && json.include?(INSPECTED_SET)
171
+
172
+ json
173
+ rescue JSON::GeneratorError, JSON::NestingError, EncodingError, SystemStackError
174
+ nil
175
+ end
176
+ private_class_method :clean_json
177
+
178
+ # How the json gem writes an object through its #to_s: a JSON String starting with "#<", as
179
+ # for a Struct, Data, Set, or OpenStruct, whose #to_s is its #inspect.
180
+ INSPECTED_OBJECT = "\"#<".freeze
181
+
182
+ # How the json gem writes a Set where Set#inspect is "Set[...]" (Ruby 4.0), which holds no
183
+ # "#<" unless the Set holds an object. Nil where Set#inspect starts with "#<" too.
184
+ INSPECTED_SET = (::Set.new.inspect.start_with?("#<") ? nil : "\"Set[").freeze
185
+
186
+ # Nesting depth after which .json_safe replaces a Hash, Array, Struct, or Data with a
187
+ # placeholder. Well below the json gem's default max_nesting of 100, leaving room for
188
+ # the levels that a formatter wraps the payload in.
189
+ MAX_JSON_SAFE_DEPTH = 32
190
+
191
+ # Returns [String] value#inspect, or what the block renders the value as (for example with
192
+ # Amazing Print), without overflowing the stack for a value nested too deeply.
193
+ #
194
+ # #inspect has no depth limit: a Hash a few hundred levels deep overflows the stack of a
195
+ # thread. Rescuing the SystemStackError is not enough, since on macOS a second such overflow
196
+ # on the same thread crashes Ruby instead. A value that holds a structure nested deeper than
197
+ # MAX_JSON_SAFE_DEPTH is therefore rendered from a .json_safe copy, as the JSON formatters
198
+ # write it, cut off at that depth with their placeholder. Only that case pays for the copy.
199
+ def self.safe_inspect(value)
200
+ value = json_safe(value) if nested_too_deeply?(value)
201
+ block_given? ? yield(value) : value.inspect
202
+ end
203
+
204
+ # Returns [true|false] whether the value holds a Hash, Array, Struct, or Data nested deeper
205
+ # than MAX_JSON_SAFE_DEPTH, which .json_safe would replace with a placeholder. Never recurses
206
+ # deeper than that itself, and follows the same rules as .json_safe.
207
+ def self.nested_too_deeply?(value, depth = 0)
208
+ case value
209
+ when String, Symbol, Integer, NilClass
210
+ # The most common values, ruled out before the slower checks below.
211
+ false
212
+ when Hash
213
+ depth >= MAX_JSON_SAFE_DEPTH ||
214
+ value.any? { |key, val| nested_too_deeply?(key, depth + 1) || nested_too_deeply?(val, depth + 1) }
215
+ when Array, ::Set
216
+ depth >= MAX_JSON_SAFE_DEPTH || value.any? { |element| nested_too_deeply?(element, depth + 1) }
217
+ when Struct, ::Data
218
+ members_nested_too_deeply?(value, depth)
219
+ else
220
+ open_struct?(value) && members_nested_too_deeply?(value, depth)
221
+ end
222
+ end
223
+
224
+ # Whether the members of a Struct, Data, or OpenStruct are nested too deeply. As in
225
+ # .json_safe_members, a #to_h that does not return a Hash is one level deeper.
226
+ def self.members_nested_too_deeply?(value, depth)
227
+ return true if depth >= MAX_JSON_SAFE_DEPTH
228
+
229
+ members = value.to_h
230
+ members.is_a?(Hash) ? nested_too_deeply?(members, depth) : nested_too_deeply?(members, depth + 1)
231
+ end
232
+ private_class_method :members_nested_too_deeply?
233
+
234
+ # Returns [true|false] whether .json_safe writes the value as a JSON object or array: a Hash,
235
+ # Array, Set, Struct, Data, or OpenStruct. Every one of them would otherwise be written by its
236
+ # #to_s, which is its #inspect.
237
+ def self.structure?(value)
238
+ case value
239
+ when Hash, Array, ::Set, Struct, ::Data
240
+ true
241
+ else
242
+ open_struct?(value)
243
+ end
244
+ end
245
+
246
+ # Whether the value is an OpenStruct. The ostruct gem is only loaded by the application, and
247
+ # is a bundled gem since Ruby 4.0, so Semantic Logger never requires it.
248
+ def self.open_struct?(value)
249
+ # Only recognizes an OpenStruct, so that it is not written through its #inspect.
250
+ defined?(::OpenStruct) && value.is_a?(::OpenStruct) # rubocop:disable Style/OpenStructUse
251
+ end
252
+ private_class_method :open_struct?
253
+
254
+ # Returns a copy of the supplied value that JSON can represent:
255
+ # * Every String is converted to valid UTF-8, as by .encode_utf8.
256
+ # * Infinity and NaN floats become the Strings "Infinity", "-Infinity", and "NaN".
257
+ # * A Struct, Data, or OpenStruct instance becomes a Hash of its members (from #to_h),
258
+ # and a Set an Array, as ActiveSupport's #as_json does, so that its members follow
259
+ # these same rules.
260
+ # * A Hash key that is itself a structure becomes the JSON of its copy, since JSON writes a
261
+ # key with #to_s, which for a structure is its #inspect.
262
+ # * Any other object whose JSON raises because of invalid UTF-8, such as an exception
263
+ # message or a Pathname built from an uploaded file name, becomes its #to_s converted to
264
+ # valid UTF-8.
265
+ # * A Hash, Array, Set, Struct, or Data nested deeper than MAX_JSON_SAFE_DEPTH is replaced
266
+ # with a placeholder String naming its class, such as "#<Hash nested too deeply>".
267
+ # So is one that holds itself, at the point where it would repeat.
268
+ # Any other value is returned as-is.
269
+ #
270
+ # The recursion never goes deeper than MAX_JSON_SAFE_DEPTH, and the placeholder does
271
+ # not look inside the structure it replaces. Rendering it instead (for example with
272
+ # #inspect) would recurse without limit through an arbitrarily deep structure,
273
+ # raising SystemStackError (or crashing Ruby) on the appender's thread, and would
274
+ # expose the instance variables of any object inside it. Ending a self-referencing
275
+ # structure where it repeats, rather than at that depth, keeps one that holds itself
276
+ # more than once from being copied an exponential number of times.
277
+ def self.json_safe(value, depth = 0, ancestors = nil)
278
+ case value
279
+ when String
280
+ encode_utf8_string(value)
281
+ when Float
282
+ value.finite? ? value : value.to_s
283
+ when Symbol, Integer, NilClass, TrueClass, FalseClass
284
+ value
285
+ when Hash, Array, ::Set, Struct, ::Data
286
+ json_safe_structure(value, depth, ancestors)
287
+ else
288
+ open_struct?(value) ? json_safe_structure(value, depth, ancestors) : json_safe_object(value)
289
+ end
290
+ end
291
+
292
+ # Returns a copy of a Hash, Array, Set, Struct, Data, or OpenStruct, or the placeholder for
293
+ # one nested too deeply or that holds itself.
294
+ def self.json_safe_structure(value, depth, ancestors)
295
+ ancestors ||= []
296
+ if depth >= MAX_JSON_SAFE_DEPTH || ancestors.any? { |ancestor| ancestor.equal?(value) }
297
+ return "#<#{value.class} nested too deeply>"
298
+ end
299
+
300
+ ancestors.push(value)
301
+ begin
302
+ json_safe_members(value, depth + 1, ancestors)
303
+ ensure
304
+ ancestors.pop
305
+ end
306
+ end
307
+ private_class_method :json_safe_structure
308
+
309
+ # Returns any other object as it is, when JSON can write it, otherwise its #to_s converted
310
+ # to valid UTF-8.
311
+ def self.json_safe_object(value)
107
312
  value.to_json
313
+ value
108
314
  rescue JSON::GeneratorError, EncodingError
109
- encode_utf8(value).to_json
315
+ encode_utf8_string(value.to_s)
316
+ end
317
+ private_class_method :json_safe_object
318
+
319
+ # Returns a copy of the elements of a Hash, Array, Set, Struct, or Data, each made safe
320
+ # by .json_safe at the supplied depth.
321
+ def self.json_safe_members(value, depth, ancestors)
322
+ case value
323
+ when Hash
324
+ value.each_with_object({}) do |(key, val), hash|
325
+ key = json_safe(key, depth, ancestors)
326
+ key = key.to_json if key.is_a?(Hash) || key.is_a?(Array)
327
+ hash[key] = json_safe(val, depth, ancestors)
328
+ end
329
+ when Array, ::Set
330
+ value.map { |element| json_safe(element, depth, ancestors) }
331
+ else
332
+ # A Struct, Data, or OpenStruct. A #to_h that does not return a Hash is made safe one
333
+ # level deeper, so that the recursion still ends.
334
+ members = value.to_h
335
+ members.is_a?(Hash) ? json_safe_members(members, depth, ancestors) : json_safe(members, depth, ancestors)
336
+ end
110
337
  end
338
+ private_class_method :json_safe_members
111
339
 
112
340
  # Returns a copy of the supplied value with every String converted to valid UTF-8.
113
341
  #
@@ -1,3 +1,3 @@
1
1
  module SemanticLogger
2
- VERSION = "5.0.0".freeze
2
+ VERSION = "5.2.0".freeze
3
3
  end
@@ -1,11 +1,11 @@
1
1
  require "concurrent"
2
- require "semantic_logger/core_ext/thread"
3
2
  require "semantic_logger/version"
4
3
  require "semantic_logger/utils"
5
4
  require "semantic_logger/ansi_colors"
6
5
  require "semantic_logger/levels"
7
6
  require "semantic_logger/base"
8
7
  require "semantic_logger/formatters"
8
+ require "semantic_logger/exception_details"
9
9
  require "semantic_logger/log"
10
10
  require "semantic_logger/subscriber"
11
11
  require "semantic_logger/loggable"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: semantic_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: logger
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.5'
26
40
  description: Semantic Logger is a high-performance, asynchronous structured logging
27
41
  framework for Ruby & Rails. It logs to multiple destinations via a background thread,
28
42
  preserving structured (semantic) payloads.
@@ -33,6 +47,17 @@ files:
33
47
  - LICENSE.txt
34
48
  - README.md
35
49
  - Rakefile
50
+ - docs/api.md
51
+ - docs/appenders.md
52
+ - docs/config.md
53
+ - docs/index.md
54
+ - docs/log.md
55
+ - docs/metrics.md
56
+ - docs/operations.md
57
+ - docs/rails.md
58
+ - docs/security.md
59
+ - docs/testing.md
60
+ - docs/upgrading.md
36
61
  - lib/semantic_logger.rb
37
62
  - lib/semantic_logger/ansi_colors.rb
38
63
  - lib/semantic_logger/appender.rb
@@ -68,8 +93,8 @@ files:
68
93
  - lib/semantic_logger/base.rb
69
94
  - lib/semantic_logger/concerns/compatibility.rb
70
95
  - lib/semantic_logger/core_ext/process.rb
71
- - lib/semantic_logger/core_ext/thread.rb
72
96
  - lib/semantic_logger/debug_as_trace_logger.rb
97
+ - lib/semantic_logger/exception_details.rb
73
98
  - lib/semantic_logger/formatters.rb
74
99
  - lib/semantic_logger/formatters/base.rb
75
100
  - lib/semantic_logger/formatters/color.rb
@@ -97,6 +122,8 @@ files:
97
122
  - lib/semantic_logger/metric/statsd.rb
98
123
  - lib/semantic_logger/processor.rb
99
124
  - lib/semantic_logger/queue_processor.rb
125
+ - lib/semantic_logger/queue_processor/request.rb
126
+ - lib/semantic_logger/queue_processor/throttled_counter.rb
100
127
  - lib/semantic_logger/reporters/minitest.rb
101
128
  - lib/semantic_logger/semantic_logger.rb
102
129
  - lib/semantic_logger/subscriber.rb
@@ -107,14 +134,14 @@ files:
107
134
  - lib/semantic_logger/test/rspec.rb
108
135
  - lib/semantic_logger/utils.rb
109
136
  - lib/semantic_logger/version.rb
110
- homepage: https://logger.rocketjob.io
137
+ homepage: https://logger.reidmorrison.com
111
138
  licenses:
112
139
  - Apache-2.0
113
140
  metadata:
114
141
  bug_tracker_uri: https://github.com/reidmorrison/semantic_logger/issues
115
- changelog_uri: https://github.com/reidmorrison/semantic_logger/releases
116
- documentation_uri: https://logger.rocketjob.io
117
- source_code_uri: https://github.com/reidmorrison/semantic_logger/tree/v5.0.0
142
+ changelog_uri: https://github.com/reidmorrison/semantic_logger/blob/main/CHANGELOG.md
143
+ documentation_uri: https://logger.reidmorrison.com
144
+ source_code_uri: https://github.com/reidmorrison/semantic_logger/tree/v5.2.0
118
145
  rubygems_mfa_required: 'true'
119
146
  rdoc_options: []
120
147
  require_paths:
@@ -1,30 +0,0 @@
1
- class Thread
2
- undef :name if method_defined? :name
3
- undef :name= if method_defined? :name=
4
-
5
- # Returns the name of the current thread
6
- # Default:
7
- # JRuby: The underlying Java thread name
8
- # Other: String representation of this thread's object_id
9
- if defined? JRuby
10
- def name
11
- @name ||= JRuby.reference(self).native_thread.name
12
- end
13
- else
14
- def name
15
- @name ||= object_id.to_s
16
- end
17
- end
18
-
19
- # Set the name of this thread
20
- # On JRuby it also sets the underlying Java Thread name
21
- if defined? JRuby
22
- def name=(name)
23
- JRuby.reference(self).native_thread.name = @name = name.to_s
24
- end
25
- else
26
- def name=(name)
27
- @name = name.to_s
28
- end
29
- end
30
- end