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.
- checksums.yaml +4 -4
- data/README.md +20 -10
- data/Rakefile +40 -0
- data/docs/api.md +506 -0
- data/docs/appenders.md +1074 -0
- data/docs/config.md +707 -0
- data/docs/index.md +223 -0
- data/docs/log.md +116 -0
- data/docs/metrics.md +171 -0
- data/docs/operations.md +446 -0
- data/docs/rails.md +890 -0
- data/docs/security.md +286 -0
- data/docs/testing.md +287 -0
- data/docs/upgrading.md +697 -0
- data/lib/semantic_logger/appender/async.rb +18 -5
- data/lib/semantic_logger/appender/bugsnag.rb +1 -1
- data/lib/semantic_logger/appender/cloudwatch_logs.rb +126 -29
- data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
- data/lib/semantic_logger/appender/elasticsearch_base.rb +48 -23
- data/lib/semantic_logger/appender/elasticsearch_http.rb +15 -8
- data/lib/semantic_logger/appender/file.rb +141 -28
- data/lib/semantic_logger/appender/graylog.rb +22 -6
- data/lib/semantic_logger/appender/honeybadger.rb +1 -1
- data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
- data/lib/semantic_logger/appender/http.rb +70 -11
- data/lib/semantic_logger/appender/io.rb +1 -1
- data/lib/semantic_logger/appender/kafka.rb +26 -12
- data/lib/semantic_logger/appender/loki.rb +13 -3
- data/lib/semantic_logger/appender/mongodb.rb +20 -1
- data/lib/semantic_logger/appender/new_relic.rb +7 -1
- data/lib/semantic_logger/appender/new_relic_logs.rb +11 -12
- data/lib/semantic_logger/appender/open_telemetry.rb +4 -3
- data/lib/semantic_logger/appender/opensearch.rb +2 -1
- data/lib/semantic_logger/appender/rabbitmq.rb +10 -4
- data/lib/semantic_logger/appender/sentry.rb +6 -1
- data/lib/semantic_logger/appender/sentry_ruby.rb +91 -17
- data/lib/semantic_logger/appender/splunk.rb +10 -1
- data/lib/semantic_logger/appender/syslog.rb +66 -25
- data/lib/semantic_logger/appender/tcp.rb +10 -4
- data/lib/semantic_logger/appender/udp.rb +11 -1
- data/lib/semantic_logger/appender/wrapper.rb +3 -2
- data/lib/semantic_logger/appender.rb +6 -3
- data/lib/semantic_logger/appenders.rb +11 -2
- data/lib/semantic_logger/base.rb +82 -32
- data/lib/semantic_logger/exception_details.rb +50 -0
- data/lib/semantic_logger/formatters/base.rb +52 -6
- data/lib/semantic_logger/formatters/color.rb +22 -3
- data/lib/semantic_logger/formatters/default.rb +28 -6
- data/lib/semantic_logger/formatters/logfmt.rb +155 -21
- data/lib/semantic_logger/formatters/loki.rb +26 -22
- data/lib/semantic_logger/formatters/one_line.rb +67 -3
- data/lib/semantic_logger/formatters/open_telemetry.rb +16 -17
- data/lib/semantic_logger/formatters/pattern.rb +11 -5
- data/lib/semantic_logger/formatters/raw.rb +4 -5
- data/lib/semantic_logger/formatters/syslog.rb +110 -17
- data/lib/semantic_logger/formatters/syslog_cee.rb +29 -15
- data/lib/semantic_logger/levels.rb +10 -1
- data/lib/semantic_logger/log.rb +183 -37
- data/lib/semantic_logger/loggable.rb +26 -3
- data/lib/semantic_logger/metric/new_relic.rb +1 -1
- data/lib/semantic_logger/metric/signalfx.rb +12 -8
- data/lib/semantic_logger/metric/statsd.rb +15 -7
- data/lib/semantic_logger/queue_processor/request.rb +55 -0
- data/lib/semantic_logger/queue_processor/throttled_counter.rb +46 -0
- data/lib/semantic_logger/queue_processor.rb +170 -68
- data/lib/semantic_logger/reporters/minitest.rb +4 -4
- data/lib/semantic_logger/semantic_logger.rb +15 -2
- data/lib/semantic_logger/subscriber.rb +96 -10
- data/lib/semantic_logger/sync_processor.rb +11 -0
- data/lib/semantic_logger/test/capture_log_events.rb +9 -6
- data/lib/semantic_logger/utils.rb +244 -16
- data/lib/semantic_logger/version.rb +1 -1
- data/lib/semantic_logger.rb +1 -1
- metadata +33 -6
- 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
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
124
|
+
# Serializes the value to JSON, repairing the data only when necessary.
|
|
94
125
|
#
|
|
95
|
-
#
|
|
96
|
-
# walk and reallocate the entire structure (see .
|
|
97
|
-
# Instead this attempts `.to_json` directly and only falls back to
|
|
98
|
-
# when serialization fails because of
|
|
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
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
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
|
-
|
|
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
|
#
|
data/lib/semantic_logger.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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/
|
|
116
|
-
documentation_uri: https://logger.
|
|
117
|
-
source_code_uri: https://github.com/reidmorrison/semantic_logger/tree/v5.
|
|
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
|