semantic_logger 5.1.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 +13 -12
- data/Rakefile +15 -4
- data/docs/api.md +37 -11
- data/docs/appenders.md +123 -39
- data/docs/config.md +115 -13
- data/docs/index.md +31 -5
- data/docs/log.md +19 -11
- data/docs/metrics.md +5 -2
- data/docs/operations.md +63 -17
- data/docs/rails.md +77 -3
- data/docs/security.md +183 -16
- data/docs/testing.md +5 -3
- data/docs/upgrading.md +295 -7
- 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 +122 -27
- data/lib/semantic_logger/appender/elasticsearch_base.rb +31 -6
- data/lib/semantic_logger/appender/elasticsearch_http.rb +1 -1
- data/lib/semantic_logger/appender/file.rb +138 -30
- 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 +1 -1
- data/lib/semantic_logger/appender/new_relic_logs.rb +9 -11
- data/lib/semantic_logger/appender/open_telemetry.rb +2 -2
- data/lib/semantic_logger/appender/rabbitmq.rb +6 -1
- data/lib/semantic_logger/appender/sentry.rb +1 -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 +64 -26
- data/lib/semantic_logger/appender/tcp.rb +6 -1
- data/lib/semantic_logger/appender/udp.rb +11 -1
- data/lib/semantic_logger/appender/wrapper.rb +1 -1
- data/lib/semantic_logger/appenders.rb +9 -1
- data/lib/semantic_logger/base.rb +82 -30
- 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 +18 -2
- data/lib/semantic_logger/formatters/default.rb +22 -5
- data/lib/semantic_logger/formatters/logfmt.rb +153 -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 +8 -4
- data/lib/semantic_logger/formatters/raw.rb +4 -5
- data/lib/semantic_logger/formatters/syslog.rb +110 -18
- data/lib/semantic_logger/formatters/syslog_cee.rb +29 -16
- 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 +8 -6
- 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 +165 -66
- 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 +8 -5
- data/lib/semantic_logger/utils.rb +244 -16
- data/lib/semantic_logger/version.rb +1 -1
- data/lib/semantic_logger.rb +1 -1
- metadata +21 -5
- data/lib/semantic_logger/core_ext/thread.rb +0 -30
|
@@ -12,13 +12,16 @@ module SemanticLogger
|
|
|
12
12
|
|
|
13
13
|
# Name of the thread that logged the message.
|
|
14
14
|
def thread_name
|
|
15
|
-
format("%.30s", log.thread_name)
|
|
15
|
+
escape_control_characters(format("%.30s", log.thread_name))
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Ruby file name and line number that logged the message.
|
|
19
|
+
#
|
|
20
|
+
# The file name is escaped like the other fields, since it comes from the first line of the
|
|
21
|
+
# backtrace, which a log call can supply from untrusted data.
|
|
19
22
|
def file_name_and_line
|
|
20
23
|
file, line = log.file_name_and_line(true)
|
|
21
|
-
"#{file}:#{line}" if file
|
|
24
|
+
"#{escape_control_characters(file)}:#{line}" if file
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Returns [String] the available process info
|
|
@@ -56,7 +59,7 @@ module SemanticLogger
|
|
|
56
59
|
|
|
57
60
|
# Class / app name
|
|
58
61
|
def name
|
|
59
|
-
log.name
|
|
62
|
+
escape_control_characters(log.name)
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
# Log message
|
|
@@ -69,15 +72,22 @@ module SemanticLogger
|
|
|
69
72
|
pl = log.payload_to_s
|
|
70
73
|
return unless pl
|
|
71
74
|
|
|
72
|
-
"-- #{pl}"
|
|
75
|
+
"-- #{escape_control_characters(pl)}"
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
# Exception
|
|
76
79
|
def exception
|
|
77
80
|
return unless log.exception
|
|
78
81
|
|
|
82
|
+
details = exception_details
|
|
79
83
|
"-- Exception: #{log.exception.class}: #{escape_control_characters(log.exception.message)}\n" \
|
|
80
|
-
"#{
|
|
84
|
+
"#{"#{details}\n" if details}#{exception_backtrace}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The detail Ruby adds to the exception's message, such as did_you_mean suggestions.
|
|
88
|
+
# See Base#initialize for the `log_exception_details` option.
|
|
89
|
+
def exception_details
|
|
90
|
+
escape_control_characters(exception_details_for(log.exception)) if log.exception
|
|
81
91
|
end
|
|
82
92
|
|
|
83
93
|
# Default text log format
|
|
@@ -89,6 +99,13 @@ module SemanticLogger
|
|
|
89
99
|
|
|
90
100
|
[time, level, process_info, tags, named_tags, duration, name, message, payload, exception].compact.join(" ")
|
|
91
101
|
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Text output is read by people, who benefit most from exception details.
|
|
106
|
+
def default_log_exception_details
|
|
107
|
+
true
|
|
108
|
+
end
|
|
92
109
|
end
|
|
93
110
|
end
|
|
94
111
|
end
|
|
@@ -13,15 +13,69 @@ module SemanticLogger
|
|
|
13
13
|
#
|
|
14
14
|
# All timestamps are ISO8601 formatteed
|
|
15
15
|
# All user supplied values are escaped and surrounded by double quotes to avoid ambiguious message delimeters
|
|
16
|
-
# `tags` are treated as keys with boolean values.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# `payload`
|
|
16
|
+
# `tags` are treated as keys with boolean values.
|
|
17
|
+
# In every key (tags, named tags, payload), spaces, control and format characters, `=`, and `"`
|
|
18
|
+
# are replaced with `_`, so that untrusted data cannot inject fields or lines, or disguise a key.
|
|
19
|
+
# `tags`, `named_tags`, and `payload` are flattened into the top level fields. A key named like
|
|
20
|
+
# one of the entry's own fields (the time key, `level`, `name`, `message`, `duration`,
|
|
21
|
+
# `duration_ms`, `tag`) or an exception field is prefixed with its source, as in `payload.level`,
|
|
22
|
+
# so that logged data cannot replace those fields. So is a key that starts with `tags.`,
|
|
23
|
+
# `named_tags.`, or `payload.`, so that it cannot forge another source's field. Other keys that
|
|
24
|
+
# are the same once encoded, such as `:user` and `"user"`, or `"a b"` and `"a=b"`, are written
|
|
25
|
+
# once, with the value from the later source: `payload` values take precedence over
|
|
26
|
+
# `named_tags`, and `named_tags` over `tags`.
|
|
27
|
+
# An exception is emitted as `exception_name`, `exception_message`, `stack_trace`, and `cause`,
|
|
28
|
+
# and with `log_exception_details` also `exception_details`.
|
|
20
29
|
#
|
|
21
30
|
# Futher Reading https://brandur.org/logfmt
|
|
22
31
|
class Logfmt < Raw
|
|
32
|
+
# The entry's own fields in the Raw hash, written in this order after the time key.
|
|
33
|
+
ENTRY_FIELDS = %i[level name message duration duration_ms].freeze
|
|
34
|
+
|
|
35
|
+
# The field that tells an entry with an exception from one without.
|
|
36
|
+
TAG_FIELD = "tag".freeze
|
|
37
|
+
|
|
38
|
+
# The exception's fields in the Raw hash, mapped to their logfmt names, so that the exception's
|
|
39
|
+
# name and message do not replace the entry's own.
|
|
40
|
+
EXCEPTION_FIELDS = {
|
|
41
|
+
name: "exception_name",
|
|
42
|
+
message: "exception_message",
|
|
43
|
+
details: "exception_details",
|
|
44
|
+
stack_trace: "stack_trace",
|
|
45
|
+
cause: "cause"
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Prefixes that mark a key from logged data with its source, when the key alone would name
|
|
49
|
+
# one of the entry's own fields, or look like a key marked this way.
|
|
50
|
+
TAGS_PREFIX = "tags.".freeze
|
|
51
|
+
NAMED_TAGS_PREFIX = "named_tags.".freeze
|
|
52
|
+
PAYLOAD_PREFIX = "payload.".freeze
|
|
53
|
+
SOURCE_PREFIXES = [TAGS_PREFIX, NAMED_TAGS_PREFIX, PAYLOAD_PREFIX].freeze
|
|
54
|
+
|
|
55
|
+
# Characters that cannot appear in a logfmt key: spaces and control characters (including
|
|
56
|
+
# NEL), which would end the key or the line, the `=` and `"` delimiters, LINE SEPARATOR and
|
|
57
|
+
# PARAGRAPH SEPARATOR, which some readers treat as newlines, and the invisible format
|
|
58
|
+
# characters, such as ZERO WIDTH SPACE and RIGHT-TO-LEFT OVERRIDE, which would let a key pass
|
|
59
|
+
# for another one, such as "level" followed by a ZERO WIDTH SPACE for "level".
|
|
60
|
+
UNSAFE_KEY_CHARS = /[\p{Cc}\p{Cf} ="\u2028\u2029]/
|
|
61
|
+
|
|
62
|
+
# The ASCII subset of UNSAFE_KEY_CHARS, which is much faster to match against the ASCII keys
|
|
63
|
+
# that make up nearly every log event.
|
|
64
|
+
UNSAFE_ASCII_KEY_CHARS = /[\x00-\x20="\x7f]/
|
|
65
|
+
|
|
66
|
+
# The most Symbol keys to remember the names of, so that untrusted keys cannot grow the cache
|
|
67
|
+
# without bound.
|
|
68
|
+
PLAIN_SYMBOL_CACHE_SIZE = 512
|
|
69
|
+
|
|
23
70
|
def initialize(time_format: :iso_8601, time_key: :timestamp, **args)
|
|
24
71
|
super
|
|
72
|
+
name_entry_fields
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Also writes and reserves the new time key in place of the old one.
|
|
76
|
+
def time_key=(time_key)
|
|
77
|
+
super
|
|
78
|
+
name_entry_fields
|
|
25
79
|
end
|
|
26
80
|
|
|
27
81
|
def call(log, logger)
|
|
@@ -32,8 +86,12 @@ module SemanticLogger
|
|
|
32
86
|
|
|
33
87
|
private
|
|
34
88
|
|
|
89
|
+
# @parsed maps the logfmt name of each field to its value. Logged data is added after the
|
|
90
|
+
# entry's own fields, and a field with the same name as an earlier one replaces its value, so
|
|
91
|
+
# the payload takes precedence over named tags, and named tags over tags.
|
|
35
92
|
def raw_to_logfmt
|
|
36
|
-
@parsed
|
|
93
|
+
@parsed = @raw.slice(*@entry_keys).transform_keys(@entry_names)
|
|
94
|
+
@parsed[TAG_FIELD] = "success"
|
|
37
95
|
handle_tags
|
|
38
96
|
handle_payload
|
|
39
97
|
handle_exception
|
|
@@ -42,37 +100,111 @@ module SemanticLogger
|
|
|
42
100
|
end
|
|
43
101
|
|
|
44
102
|
def handle_tags
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@parsed = @parsed.merge(tags).
|
|
49
|
-
merge(@raw.fetch(:named_tags) { {} })
|
|
103
|
+
@raw[:tags]&.each { |tag| add_field(TAGS_PREFIX, tag, true) }
|
|
104
|
+
@raw[:named_tags]&.each_pair { |key, value| add_field(NAMED_TAGS_PREFIX, key, value) }
|
|
50
105
|
end
|
|
51
106
|
|
|
52
107
|
def handle_payload
|
|
53
108
|
return unless @raw.key? :payload
|
|
54
109
|
|
|
55
|
-
|
|
110
|
+
payload = @raw[:payload]
|
|
111
|
+
# A payload is normally a Hash, but a log call can supply any value, so emit anything else
|
|
112
|
+
# as a single field rather than failing to format the whole entry.
|
|
113
|
+
if payload.is_a?(Hash)
|
|
114
|
+
payload.each_pair { |key, value| add_field(PAYLOAD_PREFIX, key, value) }
|
|
115
|
+
else
|
|
116
|
+
add_field(PAYLOAD_PREFIX, "payload", payload)
|
|
117
|
+
end
|
|
56
118
|
end
|
|
57
119
|
|
|
58
120
|
def handle_exception
|
|
59
121
|
return unless @raw.key? :exception
|
|
60
122
|
|
|
61
|
-
@parsed[
|
|
62
|
-
@
|
|
123
|
+
@parsed[TAG_FIELD] = "exception"
|
|
124
|
+
@raw[:exception].each_pair { |key, value| @parsed[EXCEPTION_FIELDS[key] || logfmt_key(key)] = value }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Adds a field from logged data, under the name returned by #field_name.
|
|
128
|
+
def add_field(prefix, key, value)
|
|
129
|
+
name = (key.is_a?(Symbol) && @plain_symbols[key]) || field_name(prefix, key)
|
|
130
|
+
@parsed[name] = value
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns [String] the logfmt name of a key from logged data: the key encoded by #logfmt_key,
|
|
134
|
+
# prefixed with its source, as in "payload.level", when it names one of the entry's own fields
|
|
135
|
+
# or starts with a source's prefix, so that untrusted data cannot replace or forge that field,
|
|
136
|
+
# or a field from another source.
|
|
137
|
+
def field_name(prefix, key)
|
|
138
|
+
name = key.is_a?(Symbol) ? key.name : key.to_s
|
|
139
|
+
name = logfmt_key(name) unless plain_name?(name)
|
|
140
|
+
return prefix + name if @reserved_names.key?(name) || name.start_with?(*SOURCE_PREFIXES)
|
|
141
|
+
|
|
142
|
+
remember_symbol(key, name) if key.is_a?(Symbol)
|
|
143
|
+
name
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Remembers the name of a Symbol key that is not reserved, while there is room. The cache is
|
|
147
|
+
# replaced rather than updated, since a formatter can be shared by several threads.
|
|
148
|
+
def remember_symbol(symbol, name)
|
|
149
|
+
plain_symbols = @plain_symbols
|
|
150
|
+
@plain_symbols = plain_symbols.merge(symbol => name).freeze if plain_symbols.size < PLAIN_SYMBOL_CACHE_SIZE
|
|
63
151
|
end
|
|
64
152
|
|
|
65
153
|
def flatten_log
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
154
|
+
line = +""
|
|
155
|
+
@parsed.each_pair do |name, value|
|
|
156
|
+
line << " " unless line.empty?
|
|
157
|
+
line << name << "=" << format_value(value)
|
|
158
|
+
end
|
|
159
|
+
line
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Integers, true, false, and nil have a single JSON form, so they skip the cost of `to_json`.
|
|
163
|
+
# Strings, the most common values, are checked first.
|
|
164
|
+
def format_value(value)
|
|
165
|
+
return Utils.to_json(value) if value.is_a?(String)
|
|
166
|
+
|
|
167
|
+
case value
|
|
168
|
+
when Integer, true, false
|
|
169
|
+
value.to_s
|
|
170
|
+
when nil
|
|
171
|
+
"null"
|
|
172
|
+
else
|
|
173
|
+
# The JSON form rather than #inspect, which can print an object's instance variables
|
|
174
|
+
# and has no depth limit. A structure is quoted, since its JSON holds spaces and quotes.
|
|
175
|
+
json = Utils.to_json(value)
|
|
176
|
+
json.start_with?("{", "[") ? Utils.to_json(json) : json
|
|
73
177
|
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Names the entry's own fields from the time key, and reserves those names and the
|
|
181
|
+
# exception's, so that logged data cannot use them.
|
|
182
|
+
def name_entry_fields
|
|
183
|
+
@entry_names = [time_key, *ENTRY_FIELDS].to_h { |key| [key, logfmt_key(key)] }.freeze
|
|
184
|
+
@entry_keys = @entry_names.keys.freeze
|
|
185
|
+
@reserved_names = [*@entry_names.values, TAG_FIELD, *EXCEPTION_FIELDS.values].
|
|
186
|
+
to_h { |name| [name, true] }.freeze
|
|
187
|
+
# Symbol keys from logged data that need no prefix, mapped to their logfmt names.
|
|
188
|
+
@plain_symbols = {}.freeze
|
|
189
|
+
end
|
|
74
190
|
|
|
75
|
-
|
|
191
|
+
# Whether the name is already a valid logfmt key, made of ASCII characters that are all safe.
|
|
192
|
+
def plain_name?(name)
|
|
193
|
+
!name.empty? && name.ascii_only? && !name.match?(UNSAFE_ASCII_KEY_CHARS)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Returns [String] the key as valid UTF-8, with every character that would break the logfmt
|
|
197
|
+
# structure replaced by an underscore. Keys come from tags, named tags, and payloads, which
|
|
198
|
+
# can hold untrusted data; unescaped, they could inject fields or forge an entire line.
|
|
199
|
+
def logfmt_key(key)
|
|
200
|
+
key = key.is_a?(Symbol) ? key.name : key.to_s
|
|
201
|
+
unsafe = UNSAFE_ASCII_KEY_CHARS
|
|
202
|
+
unless key.ascii_only?
|
|
203
|
+
key = Utils.encode_utf8_string(key)
|
|
204
|
+
unsafe = UNSAFE_KEY_CHARS
|
|
205
|
+
end
|
|
206
|
+
key = key.gsub(unsafe, "_") if key.match?(unsafe)
|
|
207
|
+
key.empty? ? "_" : key
|
|
76
208
|
end
|
|
77
209
|
end
|
|
78
210
|
end
|
|
@@ -81,13 +81,7 @@ module SemanticLogger
|
|
|
81
81
|
return unless log.context && !log.context.empty?
|
|
82
82
|
|
|
83
83
|
log.context.each do |key, value|
|
|
84
|
-
|
|
85
|
-
Utils.to_json(value)
|
|
86
|
-
else
|
|
87
|
-
value.to_s
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
stream[:stream].merge!(key.to_s => serialized_value)
|
|
84
|
+
stream[:stream].merge!(key.to_s => loki_value(value))
|
|
91
85
|
end
|
|
92
86
|
end
|
|
93
87
|
|
|
@@ -104,11 +98,14 @@ module SemanticLogger
|
|
|
104
98
|
end
|
|
105
99
|
|
|
106
100
|
def payload
|
|
107
|
-
self.payload_value = if
|
|
108
|
-
|
|
101
|
+
self.payload_value = if !log.payload?
|
|
102
|
+
{}
|
|
103
|
+
elsif log.payload.is_a?(Hash)
|
|
109
104
|
stringify_hash(log.payload)
|
|
110
105
|
else
|
|
111
|
-
|
|
106
|
+
# A log call can supply any value as the payload, such as a String
|
|
107
|
+
# parsed from a request body.
|
|
108
|
+
{"payload" => loki_value(log.payload)}
|
|
112
109
|
end
|
|
113
110
|
end
|
|
114
111
|
|
|
@@ -136,21 +133,28 @@ module SemanticLogger
|
|
|
136
133
|
)
|
|
137
134
|
end
|
|
138
135
|
|
|
136
|
+
# Loki only accepts Strings as keys and values.
|
|
139
137
|
def stringify_hash(hash)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
hash.each do |key, value|
|
|
143
|
-
string_key = key.to_s
|
|
144
|
-
|
|
145
|
-
result[string_key] = case value
|
|
146
|
-
when Hash
|
|
147
|
-
Utils.to_json(stringify_hash(value))
|
|
148
|
-
else
|
|
149
|
-
value.to_s
|
|
150
|
-
end
|
|
138
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
139
|
+
result[key.to_s] = loki_value(value)
|
|
151
140
|
end
|
|
141
|
+
end
|
|
152
142
|
|
|
153
|
-
|
|
143
|
+
# Returns [String] the value as Loki accepts it.
|
|
144
|
+
#
|
|
145
|
+
# A structure is written once, as JSON, depth-limited and repaired by Utils.to_json.
|
|
146
|
+
# Encoding each nested level into a String that the next level encodes again doubled the
|
|
147
|
+
# escaping at every level, so a request parameter 30 levels deep produced gigabytes. A
|
|
148
|
+
# structure's #to_s is its #inspect, which would also expose the instance variables of any
|
|
149
|
+
# object it holds.
|
|
150
|
+
def loki_value(value)
|
|
151
|
+
if value.is_a?(String)
|
|
152
|
+
value
|
|
153
|
+
elsif Utils.structure?(value)
|
|
154
|
+
Utils.to_json(value)
|
|
155
|
+
else
|
|
156
|
+
value.to_s
|
|
157
|
+
end
|
|
154
158
|
end
|
|
155
159
|
end
|
|
156
160
|
end
|
|
@@ -3,15 +3,79 @@ module SemanticLogger
|
|
|
3
3
|
# Only output one line for each log entry.
|
|
4
4
|
#
|
|
5
5
|
# Notes:
|
|
6
|
-
# * New lines are stripped from
|
|
6
|
+
# * New lines and carriage returns are stripped from every text field: the name, thread name,
|
|
7
|
+
# file name, tags, named tags, message, payload and exception message.
|
|
8
|
+
# With `escape_control_chars: true` they, and all other control characters, are escaped
|
|
9
|
+
# instead, like the other text formatters.
|
|
7
10
|
# * Exceptions only include the class and message, the stack trace is not shown.
|
|
11
|
+
# * Exception details, when enabled, are joined onto the same line.
|
|
8
12
|
class OneLine < Default
|
|
13
|
+
def thread_name
|
|
14
|
+
format("%.30s", one_line(log.thread_name))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def tags
|
|
18
|
+
return if log.tags.nil? || log.tags.empty?
|
|
19
|
+
|
|
20
|
+
"[#{log.tags.map { |tag| one_line(tag) }.join('] [')}]"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def named_tags
|
|
24
|
+
named_tags = log.named_tags
|
|
25
|
+
return if named_tags.nil? || named_tags.empty?
|
|
26
|
+
|
|
27
|
+
list = named_tags.map { |name, value| "#{one_line(name)}: #{one_line(value)}" }
|
|
28
|
+
"{#{list.join(', ')}}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def name
|
|
32
|
+
one_line(log.name) if log.name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A log call can supply the backtrace that the file name comes from.
|
|
36
|
+
def file_name_and_line
|
|
37
|
+
file, line = log.file_name_and_line(true)
|
|
38
|
+
"#{one_line(file)}:#{line}" if file
|
|
39
|
+
end
|
|
40
|
+
|
|
9
41
|
def message
|
|
10
|
-
"-- #{log.message
|
|
42
|
+
"-- #{one_line(log.message)}" if log.message
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def payload
|
|
46
|
+
pl = log.payload_to_s
|
|
47
|
+
"-- #{one_line(pl)}" if pl
|
|
11
48
|
end
|
|
12
49
|
|
|
13
50
|
def exception
|
|
14
|
-
|
|
51
|
+
return unless log.exception
|
|
52
|
+
|
|
53
|
+
details = exception_details
|
|
54
|
+
"-- Exception: #{log.exception.class}: #{one_line(log.exception.message)}#{" -- #{details}" if details}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The exception details, with each line trimmed and joined by a space.
|
|
58
|
+
def exception_details
|
|
59
|
+
details = exception_details_for(log.exception) if log.exception
|
|
60
|
+
return unless details
|
|
61
|
+
|
|
62
|
+
one_line(Utils.encode_utf8_string(details).split(/\s*\n\s*/).reject(&:empty?).join(" ").strip)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Off by default, so that existing one line output does not change.
|
|
68
|
+
def default_log_exception_details
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns [String] the value without line breaks, so that the entry stays on one line.
|
|
73
|
+
#
|
|
74
|
+
# A String with invalid bytes, such as a request parameter decoded from "%FF", or in another
|
|
75
|
+
# encoding, such as a binary request body, is converted to valid UTF-8 first, since deleting
|
|
76
|
+
# from it, or joining it with UTF-8 text, would raise and lose the entry.
|
|
77
|
+
def one_line(value)
|
|
78
|
+
Utils.encode_utf8_string(escape_control_characters(value.to_s)).delete("\r\n")
|
|
15
79
|
end
|
|
16
80
|
end
|
|
17
81
|
end
|
|
@@ -20,21 +20,29 @@ module SemanticLogger
|
|
|
20
20
|
hash[:level_index] = severity_number(log.level)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Payload is submitted directly as attributes
|
|
23
|
+
# Payload is submitted directly as attributes, which must be a Hash. A log call can supply
|
|
24
|
+
# any value as the payload, such as a String parsed from a request body, so any other value
|
|
25
|
+
# is submitted as a single "payload" attribute.
|
|
24
26
|
def payload
|
|
25
|
-
return unless log.payload
|
|
27
|
+
return unless log.payload?
|
|
26
28
|
|
|
27
|
-
hash[:payload] = coerce_map(log.payload)
|
|
29
|
+
hash[:payload] = coerce_map(log.payload.is_a?(Hash) ? log.payload : {payload: log.payload})
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
private
|
|
31
33
|
|
|
34
|
+
# An attribute value: a primitive, or an Array of them.
|
|
32
35
|
def coerce_value(v)
|
|
36
|
+
v.is_a?(Array) ? v.map { |e| coerce_element(e) }.compact : coerce_element(v)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A primitive as-is, and anything else as a String: a structure (such as a Hash, Struct, or
|
|
40
|
+
# Set) as JSON, depth-limited and repaired by Utils.to_json, rather than its #to_s, which is
|
|
41
|
+
# its #inspect and would expose the instance variables of any object it holds.
|
|
42
|
+
def coerce_element(v)
|
|
33
43
|
case v
|
|
34
|
-
when *PRIMS then v
|
|
35
|
-
|
|
36
|
-
when NilClass then nil # drop nils by caller.
|
|
37
|
-
else v.to_s # stringify objects / hashes.
|
|
44
|
+
when *PRIMS, NilClass then v
|
|
45
|
+
else Utils.structure?(v) ? Utils.to_json(v) : v.to_s
|
|
38
46
|
end
|
|
39
47
|
end
|
|
40
48
|
|
|
@@ -42,16 +50,7 @@ module SemanticLogger
|
|
|
42
50
|
h.each_with_object({}) do |(k, v), out|
|
|
43
51
|
next if v.nil?
|
|
44
52
|
|
|
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
|
|
53
|
+
out[k.to_s] = coerce_value(v)
|
|
55
54
|
end
|
|
56
55
|
end
|
|
57
56
|
|
|
@@ -30,6 +30,8 @@ module SemanticLogger
|
|
|
30
30
|
# payload Payload rendered as a string.
|
|
31
31
|
# exception_class Class of the logged exception, e.g. "RuntimeError".
|
|
32
32
|
# exception_message Message of the logged exception.
|
|
33
|
+
# exception_details Detail Ruby adds to the exception's message, such as
|
|
34
|
+
# did_you_mean suggestions (see the log_exception_details option).
|
|
33
35
|
# backtrace Backtrace of the logged exception.
|
|
34
36
|
# duration Human readable duration, e.g. "1.2ms".
|
|
35
37
|
# duration_ms Duration in milliseconds (numeric).
|
|
@@ -59,6 +61,7 @@ module SemanticLogger
|
|
|
59
61
|
payload: false,
|
|
60
62
|
exception_class: false,
|
|
61
63
|
exception_message: false,
|
|
64
|
+
exception_details: false,
|
|
62
65
|
backtrace: false,
|
|
63
66
|
duration: false,
|
|
64
67
|
duration_ms: false,
|
|
@@ -118,7 +121,7 @@ module SemanticLogger
|
|
|
118
121
|
|
|
119
122
|
# Raw payload rendered as a string.
|
|
120
123
|
def payload
|
|
121
|
-
log.payload_to_s
|
|
124
|
+
escape_control_characters(log.payload_to_s)
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
# Class of the logged exception, e.g. "RuntimeError".
|
|
@@ -133,7 +136,7 @@ module SemanticLogger
|
|
|
133
136
|
|
|
134
137
|
# Backtrace of the logged exception.
|
|
135
138
|
def backtrace
|
|
136
|
-
|
|
139
|
+
exception_backtrace if log.exception
|
|
137
140
|
end
|
|
138
141
|
|
|
139
142
|
# Human readable duration (without the Default formatter's surrounding parentheses).
|
|
@@ -146,9 +149,10 @@ module SemanticLogger
|
|
|
146
149
|
log.duration
|
|
147
150
|
end
|
|
148
151
|
|
|
149
|
-
# Name of the Ruby file that logged the message, e.g. "app.rb".
|
|
152
|
+
# Name of the Ruby file that logged the message, e.g. "app.rb". Escaped like the other
|
|
153
|
+
# fields, since a log call can supply the backtrace it comes from.
|
|
150
154
|
def file_name
|
|
151
|
-
log.file_name_and_line(true)&.first
|
|
155
|
+
escape_control_characters(log.file_name_and_line(true)&.first)
|
|
152
156
|
end
|
|
153
157
|
|
|
154
158
|
# Line number within the Ruby file that logged the message.
|
|
@@ -96,11 +96,10 @@ module SemanticLogger
|
|
|
96
96
|
root = hash
|
|
97
97
|
log.each_exception do |exception, i|
|
|
98
98
|
name = i.zero? ? :exception : :cause
|
|
99
|
-
root[name] = {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
99
|
+
root[name] = {name: exception.class.name, message: exception.message}
|
|
100
|
+
details = exception_details_for(exception)
|
|
101
|
+
root[name][:details] = details if details
|
|
102
|
+
root[name][:stack_trace] = exception.backtrace
|
|
104
103
|
root = root[name]
|
|
105
104
|
end
|
|
106
105
|
end
|