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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -12
  3. data/Rakefile +15 -4
  4. data/docs/api.md +37 -11
  5. data/docs/appenders.md +123 -39
  6. data/docs/config.md +115 -13
  7. data/docs/index.md +31 -5
  8. data/docs/log.md +19 -11
  9. data/docs/metrics.md +5 -2
  10. data/docs/operations.md +63 -17
  11. data/docs/rails.md +77 -3
  12. data/docs/security.md +183 -16
  13. data/docs/testing.md +5 -3
  14. data/docs/upgrading.md +295 -7
  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 +122 -27
  18. data/lib/semantic_logger/appender/elasticsearch_base.rb +31 -6
  19. data/lib/semantic_logger/appender/elasticsearch_http.rb +1 -1
  20. data/lib/semantic_logger/appender/file.rb +138 -30
  21. data/lib/semantic_logger/appender/graylog.rb +22 -6
  22. data/lib/semantic_logger/appender/honeybadger.rb +1 -1
  23. data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
  24. data/lib/semantic_logger/appender/http.rb +70 -11
  25. data/lib/semantic_logger/appender/io.rb +1 -1
  26. data/lib/semantic_logger/appender/kafka.rb +26 -12
  27. data/lib/semantic_logger/appender/loki.rb +13 -3
  28. data/lib/semantic_logger/appender/mongodb.rb +20 -1
  29. data/lib/semantic_logger/appender/new_relic.rb +1 -1
  30. data/lib/semantic_logger/appender/new_relic_logs.rb +9 -11
  31. data/lib/semantic_logger/appender/open_telemetry.rb +2 -2
  32. data/lib/semantic_logger/appender/rabbitmq.rb +6 -1
  33. data/lib/semantic_logger/appender/sentry.rb +1 -1
  34. data/lib/semantic_logger/appender/sentry_ruby.rb +91 -17
  35. data/lib/semantic_logger/appender/splunk.rb +10 -1
  36. data/lib/semantic_logger/appender/syslog.rb +64 -26
  37. data/lib/semantic_logger/appender/tcp.rb +6 -1
  38. data/lib/semantic_logger/appender/udp.rb +11 -1
  39. data/lib/semantic_logger/appender/wrapper.rb +1 -1
  40. data/lib/semantic_logger/appenders.rb +9 -1
  41. data/lib/semantic_logger/base.rb +82 -30
  42. data/lib/semantic_logger/exception_details.rb +50 -0
  43. data/lib/semantic_logger/formatters/base.rb +52 -6
  44. data/lib/semantic_logger/formatters/color.rb +18 -2
  45. data/lib/semantic_logger/formatters/default.rb +22 -5
  46. data/lib/semantic_logger/formatters/logfmt.rb +153 -21
  47. data/lib/semantic_logger/formatters/loki.rb +26 -22
  48. data/lib/semantic_logger/formatters/one_line.rb +67 -3
  49. data/lib/semantic_logger/formatters/open_telemetry.rb +16 -17
  50. data/lib/semantic_logger/formatters/pattern.rb +8 -4
  51. data/lib/semantic_logger/formatters/raw.rb +4 -5
  52. data/lib/semantic_logger/formatters/syslog.rb +110 -18
  53. data/lib/semantic_logger/formatters/syslog_cee.rb +29 -16
  54. data/lib/semantic_logger/levels.rb +10 -1
  55. data/lib/semantic_logger/log.rb +183 -37
  56. data/lib/semantic_logger/loggable.rb +26 -3
  57. data/lib/semantic_logger/metric/new_relic.rb +1 -1
  58. data/lib/semantic_logger/metric/signalfx.rb +8 -6
  59. data/lib/semantic_logger/metric/statsd.rb +15 -7
  60. data/lib/semantic_logger/queue_processor/request.rb +55 -0
  61. data/lib/semantic_logger/queue_processor/throttled_counter.rb +46 -0
  62. data/lib/semantic_logger/queue_processor.rb +165 -66
  63. data/lib/semantic_logger/semantic_logger.rb +15 -2
  64. data/lib/semantic_logger/subscriber.rb +96 -10
  65. data/lib/semantic_logger/sync_processor.rb +11 -0
  66. data/lib/semantic_logger/test/capture_log_events.rb +8 -5
  67. data/lib/semantic_logger/utils.rb +244 -16
  68. data/lib/semantic_logger/version.rb +1 -1
  69. data/lib/semantic_logger.rb +1 -1
  70. metadata +21 -5
  71. data/lib/semantic_logger/core_ext/thread.rb +0 -30
data/docs/security.md CHANGED
@@ -1,9 +1,12 @@
1
1
  ---
2
2
  layout: default
3
+ title: Security
4
+ description: >-
5
+ Keeping logs trustworthy: log injection and forging, redacting sensitive
6
+ data, transport encryption, log file permissions, and the gem's dependency
7
+ footprint.
3
8
  ---
4
9
 
5
- ## Security
6
- {:.no_toc}
7
10
 
8
11
  **Contents**
9
12
 
@@ -52,6 +55,45 @@ The record-framed network appenders are already safe by default:
52
55
  If you replace the formatter on any of these appenders with a text formatter, enable
53
56
  `escape_control_chars` as shown above.
54
57
 
58
+ ### Logging untrusted hashes
59
+
60
+ A hash passed after the message is the payload, but a few of its keys are also log arguments:
61
+ `duration`, `min_duration`, `metric`, `metric_amount`, `exception`, `log_exception`, and so on.
62
+ That is convenient for code you write, but when the hash holds untrusted data, such as parsed
63
+ request parameters or a webhook body with Symbol keys, it can set those arguments too. For
64
+ example `{duration: 0, min_duration: 1}` would suppress the entry.
65
+
66
+ Semantic Logger only uses such a key as an argument when its value is valid for it: for example
67
+ `duration` must be a finite real number, `backtrace` an Array of Strings, and `on_exception_level` a
68
+ valid level. Any other value stays in the payload as data, and a `nil` value is ignored, so an
69
+ untrusted hash passed to a level method such as `info` can never make the logging call raise.
70
+
71
+ To make sure none of its keys are treated as arguments, pass it as the explicit `payload:`. Any other
72
+ keys supplied alongside an explicit `payload:` are merged into it, and win over the explicit
73
+ payload's values of the same name, so that the untrusted data cannot replace a value your code
74
+ supplies. A String key, as `JSON.parse` returns without `symbolize_names`, has the same name as the
75
+ Symbol key it spells:
76
+
77
+ ~~~ruby
78
+ # The hash is logged as-is, and none of its keys are interpreted. Its own account_id, if any,
79
+ # cannot replace the one supplied here:
80
+ logger.info("Webhook received", payload: JSON.parse(body, symbolize_names: true), account_id: account.id)
81
+ ~~~
82
+
83
+ A hash returned from a logging block, such as
84
+ `logger.debug("Webhook received") { JSON.parse(body, symbolize_names: true) }`, is checked the
85
+ same way, and never replaces a value the log call supplied. A key is only used as an argument when
86
+ its value is valid for it and the log entry does not have that argument yet: an empty `message`
87
+ counts as none, and a backtrace captured at the `backtrace_level` counts as supplied. Its `payload`
88
+ is only used when it is a Hash. Every other key, including an argument the entry already has, is
89
+ added to the payload without replacing the log call's values, and wins over the block's own
90
+ `payload` values of the same name. The caller's own payload Hash is left unchanged.
91
+
92
+ The options of a `measure_*` call are checked the same way: an option with a value that is not valid
93
+ for it is ignored, and its default is used. Keys that are not measure options are not logged at all,
94
+ so pass untrusted data to a measure call as its `payload:` too. A measure call without a block still
95
+ raises unless it is given a valid `:duration`.
96
+
55
97
  ### Redacting sensitive data
56
98
 
57
99
  Semantic Logger does not automatically redact secrets or personal information; it logs
@@ -60,37 +102,144 @@ numbers, and similar values in the first place.
60
102
 
61
103
  When sensitive values can reach the logs indirectly, redact them. A global `on_log`
62
104
  subscriber runs inline, once per log event, before the event is handed to any appender,
63
- so it can scrub the message or payload for every destination:
105
+ so it can scrub the payload for every destination. The example below matches keys at any depth,
106
+ whether they are Strings, as in parsed JSON and Rails params, or Symbols, in any case and style,
107
+ such as `api_key`, `apiKey`, or `X-Api-Key`. It builds a new payload rather than changing the logged
108
+ one, which can be the application's own Hash. It fails closed: a payload that it cannot redact is
109
+ replaced, never written as it is.
64
110
 
65
111
  ~~~ruby
66
- SECRET_KEYS = %i[password token authorization secret].freeze
112
+ module LogRedaction
113
+ # Matched against each key in lower case with "_" between its words, see sensitive_key?.
114
+ SENSITIVE_KEY = Regexp.union(
115
+ # Anywhere in the key, like the default filter_parameters of Rails.
116
+ /passw|passphrase|secret|token|_key|apikey|credential|authorization|cookie|crypt|certificate|email/,
117
+ # Only as a whole word, so that "otp" does not match "footprint", nor "ssn" "business_name".
118
+ /(?<![a-z])(?:auth|pwd|otp|ssn|salt|cvv|cvc)(?![a-z])/
119
+ )
120
+ REDACTED = "[REDACTED]".freeze
121
+ MAX_DEPTH = 32
67
122
 
68
- SemanticLogger.on_log do |log|
69
- log.payload&.each_key do |key|
70
- log.payload[key] = "[REDACTED]" if SECRET_KEYS.include?(key)
123
+ # Returns a copy of the value with the value of every sensitive key replaced, at any depth.
124
+ #
125
+ # copies maps each structure already walked to its copy, so that a structure held more than once
126
+ # is walked only once, and one that holds itself is cut off.
127
+ def self.redact(value, copies = {}.compare_by_identity, depth = 0)
128
+ return copies[value] || "[REDACTED: circular]" if copies.key?(value)
129
+
130
+ members = members(value)
131
+ return value if members.nil?
132
+ return "[REDACTED: nested too deeply]" if depth >= MAX_DEPTH
133
+
134
+ copies[value] = nil # Until its copy is complete.
135
+ copies[value] =
136
+ if members.is_a?(Hash)
137
+ members.each_with_object({}) do |(key, val), copy|
138
+ copy[key] = sensitive_key?(key) ? REDACTED : redact(val, copies, depth + 1)
139
+ end
140
+ else
141
+ members.map { |val| redact(val, copies, depth + 1) }
142
+ end
143
+ end
144
+
145
+ def self.sensitive_key?(key)
146
+ # As valid UTF-8, since a key with invalid bytes, as from a query string, makes a match raise.
147
+ name = key.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
148
+ # "apiKey", "API_KEY", and "X-Api-Key" become "api_key", "api_key", and "x_api_key".
149
+ name = name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.gsub(/[^a-z\d]+/, "_")
150
+ SENSITIVE_KEY.match?(name)
151
+ end
152
+
153
+ # Returns the members of a Hash, Array, Struct, Data, Set, OpenStruct, or
154
+ # ActionController::Parameters, as a Hash or an Array. Returns nil for any other value.
155
+ def self.members(value)
156
+ case value
157
+ when Hash, Array then value
158
+ when Struct, Data then value.to_h
159
+ when Set then value.to_a
160
+ else
161
+ if defined?(OpenStruct) && value.is_a?(OpenStruct)
162
+ value.to_h
163
+ elsif value.respond_to?(:to_unsafe_h) # ActionController::Parameters
164
+ value.to_unsafe_h
165
+ end
166
+ end
71
167
  end
72
168
  end
169
+
170
+ # Register it before any other on_log subscriber.
171
+ SemanticLogger.on_log do |log|
172
+ log.payload = LogRedaction.redact(log.payload) unless log.payload.nil?
173
+ rescue Exception => e
174
+ # Fail closed. Any Exception, since Semantic Logger rescues whatever an on_log subscriber raises
175
+ # and goes on to write the entry.
176
+ log.payload = {redaction_failed: e.class.name}
177
+ end
73
178
  ~~~
74
179
 
75
- Redaction can also be applied to a single logger or appender with a `filter` that
76
- mutates the log and returns `true`. See [Filtering](config.html#filtering) for examples.
180
+ `SENSITIVE_KEY` is a starting point: adapt it to the keys that your application logs. Like the
181
+ default `filter_parameters` of Rails, it also matches some keys that are not secret, such as
182
+ `max_tokens` or `cache_key`.
183
+
184
+ The example looks inside Hashes and Arrays, and inside the Struct, Data, Set, and OpenStruct values
185
+ that the JSON formatters write as objects and arrays, which it replaces with Hashes and Arrays. It
186
+ reads `ActionController::Parameters` with `to_unsafe_h`. Any other object is left as it is, and is
187
+ written the way the formatter writes it, which for some objects includes all of their attributes:
188
+ for example a JSON formatter writes an Active Record model with `as_json`. Log a Hash of the values
189
+ you need instead.
190
+
191
+ Register it before any other `on_log` subscriber that reads the payload. Subscribers run in the
192
+ order they were registered, so one registered before it, such as the [Rollbar](appenders.html#rollbar)
193
+ integration that sends `log.to_h`, receives the payload before it is redacted.
194
+
195
+ It only redacts the payload. The message, the tags and named tags, the message of the exception and
196
+ those of its causes, the context that `on_log` subscribers add, and metric dimensions are written
197
+ as they are, so keep secrets out of them. That includes a value other than a Hash passed after the
198
+ message, such as `logger.info("Login", user)`, which is added to the message: pass it as
199
+ `payload: user` instead.
200
+
201
+ It copies the payload of every entry that is logged, even when there is nothing to redact, on the
202
+ thread that logs it, so its cost grows with the size of the payloads.
203
+
204
+ A `filter` on a logger instance can redact the same way for that logger's entries; see
205
+ [Filtering](config.html#filtering). Do not redact in an appender's `filter`: every appender writes
206
+ the same log entry, so that only protects the appenders added after it, while the ones added before
207
+ it have already written the secret.
77
208
 
78
209
  Rails applications should use the sister gem
79
210
  [rails_semantic_logger](https://github.com/reidmorrison/rails_semantic_logger), which
80
211
  honours `config.filter_parameters` so that parameters Rails already considers sensitive
81
- are filtered out of the logs.
212
+ are filtered out of the logs. To match the payload's keys against the same list, `sensitive_key?`
213
+ can return `!filter.filter_param(name, nil).nil?`, where `filter` is
214
+ `ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)`, created once the
215
+ application has initialized, since one created from an incomplete list filters less. Proc filters
216
+ in the list are not applied this way. Do not call its `#filter` in place of `redact`: it only looks
217
+ inside Hashes and Arrays, raises for a key with invalid UTF-8, and has no protection against a Hash
218
+ that holds itself or is held more than once.
82
219
 
83
220
  ### Transport encryption
84
221
 
85
222
  Appenders that send logs over the network support TLS. The appenders built on the
86
- `:http` appender (`:http`, `:splunk_http`, and `:elasticsearch_http`) default to
223
+ `:http` appender (`:http`, `:splunk_http`, `:elasticsearch_http`, and `:loki`) default to
87
224
  verifying the server certificate (`OpenSSL::SSL::VERIFY_PEER`) when an `https` URL is
88
- used.
225
+ used. The `:tcp` appender, and the `:syslog` appender over TCP, verify both the certificate
226
+ and the host name when `ssl:` is enabled.
227
+
228
+ The deprecated `:splunk` appender is the exception: its `splunk-sdk-ruby` gem never verifies
229
+ the certificate. Use `:splunk_http` instead.
89
230
 
90
- Do not disable certificate verification in production. Settings such as
231
+ Do not disable certificate verification. Settings such as
91
232
  `ssl: {verify_mode: OpenSSL::SSL::VERIFY_NONE}` expose the connection to
92
- man-in-the-middle attacks and should be limited to local development against
93
- self-signed certificates.
233
+ man-in-the-middle attacks. To connect to a server with a self-signed certificate, or one issued
234
+ by a private certificate authority, trust that certificate instead, for example with
235
+ `ssl: {ca_file: "/path/to/ca.pem"}`.
236
+
237
+ Credentials can be supplied in the url of the HTTP based appenders, for example
238
+ `https://user:password@host`, or as a query parameter. They are removed from the url
239
+ written to the internal logger when the server rejects a request, and from the error raised
240
+ for an invalid url. `#inspect` on any appender, for example when `SemanticLogger.appenders` is
241
+ printed in a console, shows only what that appender says is safe, such as its url without
242
+ credentials, and none of its passwords, tokens, keys, or client options.
94
243
 
95
244
  ### Log file permissions
96
245
 
@@ -104,9 +253,27 @@ an existing log file:
104
253
  SemanticLogger.add_appender(file_name: "production.log", permissions: 0o640)
105
254
  ~~~
106
255
 
256
+ With `permissions`, the log file is opened without following a symbolic link at its name, so a
257
+ link planted in the log directory can neither create nor open the file it points at. The file must
258
+ be a regular file owned by the process, with a single hard link, that the file name still names
259
+ once it is open. Otherwise, or when the file cannot be opened, nothing is written to it:
260
+ `add_appender` raises `IOError` giving the reason, with the error from opening the file, if any, as
261
+ its `cause`. When a file only fails the check once it is reopened later, on a rotation or after a
262
+ fork, the appender reports the reason once for each file, drops the log messages instead of
263
+ raising, and tries the file again every few seconds and at the next scheduled reopen, so that
264
+ logging resumes once the file is fixed. A symbolic link, hard link, or file planted in a shared log
265
+ directory, for example under the name of tomorrow's log file, can therefore neither loosen the
266
+ permissions of another file nor receive the log messages. A FIFO planted there fails to open
267
+ instead of blocking the process.
268
+
269
+ Without `permissions`, the file appender opens the log file the way Ruby's own `Logger` does,
270
+ following symbolic links, so that a log file can for example be linked to `/dev/stdout`. Keep the
271
+ log directory writable only by the application.
272
+
107
273
  ### Dependencies and supply chain
108
274
 
109
- Semantic Logger has a single runtime dependency, `concurrent-ruby`. Appenders for
275
+ Semantic Logger has two runtime dependencies, `concurrent-ruby` and `logger` (part of
276
+ Ruby's standard library until Ruby 4.0 moved it to the bundled gems). Appenders for
110
277
  third-party services (Kafka, MongoDB, Sentry, etc.) keep their backing gem optional: it
111
278
  is required lazily inside the appender only when that appender is used, and is never
112
279
  added to the gemspec. This keeps the dependency surface of an application that only uses
data/docs/testing.md CHANGED
@@ -1,9 +1,11 @@
1
1
  ---
2
2
  layout: default
3
+ title: Testing
4
+ description: >-
5
+ Assert that your code logged the expected message, level, payload or metric,
6
+ using the capture helpers for Minitest, RSpec, and other test frameworks.
3
7
  ---
4
8
 
5
- ## Testing
6
- {:.no_toc}
7
9
 
8
10
  **Contents**
9
11
 
@@ -106,7 +108,7 @@ test. The available checks are:
106
108
  | `time` | When the entry was created. |
107
109
  | `duration` | Duration of a measure call, in milliseconds. |
108
110
  | `backtrace` | The captured backtrace, if any. |
109
- | `thread_name` | Name of the thread that logged the entry. |
111
+ | `thread_name` | Name of the thread that logged the entry, or its object id when the thread has no name. |
110
112
 
111
113
  ### Match part of a message or payload
112
114
 
data/docs/upgrading.md CHANGED
@@ -1,22 +1,290 @@
1
1
  ---
2
2
  layout: default
3
+ title: Upgrading
4
+ description: >-
5
+ Version-by-version upgrade notes for Semantic Logger, from v4.0 through the
6
+ v5.2 security fixes, remote syslog facility fix, and deprecation of the Splunk SDK appender.
3
7
  ---
4
8
 
5
- ## Upgrading
6
- {:.no_toc}
7
9
 
8
10
  **Contents**
9
11
 
10
12
  * TOC
11
13
  {:toc}
12
14
 
15
+ ### Upgrading to Semantic Logger v5.2
16
+
17
+ v5.2 sends the configured facility to remote syslog, deprecates the `:splunk` appender, stops
18
+ replacing Ruby's `Thread#name`, and fixes several security issues. None of the security fixes change
19
+ the public API, but a few change the output, which matters if you parse the logs or alert on them,
20
+ and the file appender's `permissions:` option can now make `add_appender` raise for a log file that
21
+ worked in v5.1.
22
+
23
+ #### Set the Sentry user and fingerprint with named tags
24
+
25
+ The `:sentry_ruby` appender takes the Sentry user (`user_id`, `username`, `user_email`, `ip_address`,
26
+ and `user`) and `fingerprint` from named tags or the payload. Taking them from the payload is
27
+ deprecated and will be **removed in v6**, since the payload often holds untrusted data, such as a
28
+ request body, which could then set the user an error is reported for. It emits a deprecation warning
29
+ the first time it happens. Until then, the named tags win over the payload, which no longer replaces
30
+ a user field they set, and only a String, Symbol, or Integer is taken from the payload for a user
31
+ field. Set them with named tags instead, which can now also hold the `fingerprint`:
32
+
33
+ ~~~ruby
34
+ # Before:
35
+ logger.error("Payment failed", user_id: user.id, fingerprint: ["payment-failed"])
36
+
37
+ # After:
38
+ SemanticLogger.tagged(user_id: user.id, fingerprint: ["payment-failed"]) do
39
+ logger.error("Payment failed")
40
+ end
41
+ ~~~
42
+
43
+ #### Replace the `:splunk` appender with `:splunk_http`
44
+
45
+ The `:splunk` appender depends on the `splunk-sdk-ruby` gem, which was last released in 2015 and
46
+ does not verify the server's TLS certificate. It now emits a deprecation warning and will be
47
+ **removed in v6**. Send logs to the Splunk HTTP Event Collector instead:
48
+
49
+ ~~~ruby
50
+ # Before:
51
+ SemanticLogger.add_appender(appender: :splunk, index: "main")
52
+
53
+ # After:
54
+ SemanticLogger.add_appender(
55
+ appender: :splunk_http,
56
+ url: "https://splunk.example.com:8088/services/collector/event",
57
+ token: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
58
+ index: "main"
59
+ )
60
+ ~~~
61
+
62
+ #### Remote syslog sends the configured facility
63
+
64
+ When logging to a remote syslog server over TCP or UDP, earlier versions sent the wrong facility.
65
+ The `facility:` option is documented as one of Ruby's `::Syslog::LOG_*` facility constants, but the
66
+ value was sent as if it were already the syslog protocol's facility number. Those constants are the
67
+ facility number shifted left by 3 bits, so the default `::Syslog::LOG_USER` (8) went out as facility
68
+ 8, which is `uucp`, instead of facility 1, `user`. For example a `:info` message, at the default
69
+ `LOG_NOTICE` severity, was sent as `<69>` rather than `<13>`. Logging to a local syslog daemon was
70
+ not affected.
71
+
72
+ v5.2 sends the facility the constant names. If your syslog server filters or routes on facility,
73
+ for example an rsyslog rule for `uucp.*`, change it to the facility you configured: `user` unless
74
+ you set one.
75
+
76
+ The `facility:` option now only accepts the `::Syslog::LOG_*` facility constants, and raises
77
+ `ArgumentError` for other values when the appender is created. Previously a constant other than
78
+ `::Syslog::LOG_KERN`, `::Syslog::LOG_USER`, or `::Syslog::LOG_MAIL` raised `ArgumentError` while
79
+ logging. If you passed a bare facility number to get around that, switch to the constant. Check
80
+ this even if nothing raises: a bare 0, 8, or 16 is now read as `::Syslog::LOG_KERN`,
81
+ `::Syslog::LOG_USER`, or `::Syslog::LOG_MAIL`, so `16`, which used to send `local0`, now sends
82
+ `mail`:
83
+
84
+ ~~~ruby
85
+ # Before (16 was sent as facility local0, and is now read as ::Syslog::LOG_MAIL):
86
+ SemanticLogger.add_appender(appender: :syslog, url: "udp://myloghost:514", facility: 16)
87
+
88
+ # After:
89
+ SemanticLogger.add_appender(appender: :syslog, url: "udp://myloghost:514", facility: ::Syslog::LOG_LOCAL0)
90
+ ~~~
91
+
92
+ This applies to the `:syslog` and `:syslog_cee` formatters. An application that logs only to a remote
93
+ server, and so does not need the `syslog` gem, can use the same constants from
94
+ `SemanticLogger::Formatters::Syslog`, for example `SemanticLogger::Formatters::Syslog::LOG_LOCAL0`.
95
+
96
+ #### The file appender's `permissions:` refuses a log file it cannot secure
97
+
98
+ With `permissions:`, the file appender only writes to a regular file owned by the process, with a
99
+ single hard link, that the file name names directly rather than through a symbolic link.
100
+ `SemanticLogger.add_appender` now raises `IOError`, giving the reason, for any other log file,
101
+ which v5.1 wrote to, changing the permissions of whatever a link pointed at. So a configuration
102
+ that worked in v5.1 can now fail at boot, for example `permissions:` on a log file that is a
103
+ symbolic link to `/dev/stdout`, or on a log file that another user created. With `permissions:`,
104
+ a log file that cannot be opened also raises `IOError`, instead of the operating system's error,
105
+ such as `Errno::ENOENT`, which is its `cause`. A log file that fails the check, or to open, when it
106
+ is reopened later, on a rotation or after a fork, is reported to the internal logger and not
107
+ written to until it is fixed, instead of raising from the logging thread.
108
+
109
+ If you set `permissions:` on such a log file, either fix the file or remove `permissions:`. To log
110
+ to standard output, use `io: $stdout`:
111
+
112
+ ~~~ruby
113
+ # Before, with log/production.log a symbolic link to /dev/stdout:
114
+ SemanticLogger.add_appender(file_name: "log/production.log", permissions: 0o640)
115
+
116
+ # After:
117
+ SemanticLogger.add_appender(io: $stdout)
118
+ ~~~
119
+
120
+ #### Ruby's own `Thread#name` is no longer replaced
121
+
122
+ Earlier versions replaced `Thread#name` and `Thread#name=` with Semantic Logger's own versions,
123
+ which were written before Ruby had them and kept the name away from Ruby itself. v5.2 leaves Ruby's
124
+ own in place, so the name now also reaches the operating system, profilers, and `Thread#inspect`.
125
+ On MRI, log entries do not change: a thread that has no name is still logged with its object id.
126
+
127
+ Code anywhere in the application that calls `Thread#name` or `Thread#name=` now gets Ruby's
128
+ behaviour:
129
+
130
+ * `Thread#name` returns `nil` for a thread that has no name, where it returned the thread's object
131
+ id as a String (on JRuby, its JVM thread name). The main thread has no name on MRI; JRuby names it
132
+ `main`.
133
+ * `Thread#name=` is Ruby's built-in implementation again, with Ruby's standard behaviour, instead of
134
+ Semantic Logger's version, which accepted any value and converted it with `to_s`. Ruby's version
135
+ raises `TypeError` for a value that is not a String or `nil`, such as a Symbol or an Integer. It
136
+ raises `ArgumentError` for a String that contains a NUL byte (`"\0"`), or whose encoding is not
137
+ ASCII-compatible, such as UTF-16. Setting `nil` leaves the thread with no name, where it set the
138
+ name to `""`.
139
+
140
+ ~~~ruby
141
+ # Before:
142
+ Thread.current.name = worker_number
143
+ Thread.current.name.start_with?("worker")
144
+
145
+ # After:
146
+ Thread.current.name = "worker-#{worker_number}"
147
+ Thread.current.name&.start_with?("worker")
148
+ ~~~
149
+
150
+ Tests that expect `thread_name: Thread.current.name` from the
151
+ [test helpers](testing.html) no longer work on a thread that has no name, such as the main thread
152
+ on MRI: `Thread.current.name` is now `nil`, while the log entry still holds the thread's object id.
153
+ The RSpec matchers `be_a_semantic_logger_event` and `log_semantic_logger_event` then fail. Worse,
154
+ `assert_semantic_logger_event` in Minitest takes `nil` to mean "do not check", so the test still
155
+ passes but no longer checks the thread name. Expect the object id when the thread has no name:
156
+
157
+ ~~~ruby
158
+ # Before:
159
+ assert_semantic_logger_event(event, thread_name: Thread.current.name)
160
+
161
+ # After:
162
+ assert_semantic_logger_event(event, thread_name: Thread.current.name || Thread.current.object_id.to_s)
163
+ ~~~
164
+
165
+ Semantic Logger's own threads are now named after their appender, such as `SL File`, with
166
+ `SL Appenders` for the main one, instead of `SemanticLogger::Appender::File` and
167
+ `SemanticLogger::Appenders`. Linux keeps only the first 15 bytes of a thread name, so the longer
168
+ names all looked the same in tools that list threads, such as `top -H`. The new names also appear
169
+ in the thread column of the messages Semantic Logger logs about its own problems, such as an
170
+ appender that cannot write.
171
+
172
+ On JRuby:
173
+
174
+ * A Ruby thread that has no name is logged with its object id, instead of its JVM thread name, such
175
+ as `Ruby-0-Thread-1: app/worker.rb:12`. Name the thread to give it a readable name in the logs. A
176
+ thread created by Java code, such as a servlet container's request thread, is still logged with
177
+ its JVM thread name, which now also stays current when Java code renames the thread.
178
+ * Naming a Ruby thread sets its JVM thread name in JRuby's own format, such as
179
+ `Ruby-0-Thread-2@My Worker: app/worker.rb:12`, instead of exactly `My Worker`. Update any JVM
180
+ monitoring that matches thread names exactly.
181
+
182
+ #### Output changes
183
+
184
+ * `:logfmt`: spaces, control characters, invisible format characters (such as a zero width space),
185
+ `=`, `"`, and Unicode line separators in keys are replaced with `_`, and invalid UTF-8 bytes are
186
+ dropped. A tag such as `second breakfast` is now emitted as `second_breakfast=true`. A tag,
187
+ named tag, or payload key named like one of the entry's own fields, which are the time key
188
+ (`timestamp` by default), `level`, `name`, `message`, `duration`, `duration_ms`, `tag`, and the
189
+ exception fields, is emitted with its source as a prefix, such as `payload.name="shrek"` or
190
+ `tags.message=true`, instead of replacing the field, and so is a key that starts with `tags.`,
191
+ `named_tags.`, or `payload.`. A logged exception's class and message are emitted as
192
+ `exception_name` and `exception_message` (followed by `stack_trace` and `cause` as before),
193
+ instead of replacing the entry's `name` and `message`, and the detail that
194
+ `log_exception_details: true` adds as `exception_details`. Other keys that are the same once
195
+ encoded, such as `:user` and `"user"`, are emitted once, with the value from the later source:
196
+ the payload over named tags, and named tags over tags. A payload that is not a Hash is emitted
197
+ as a single `payload` field. A nested value (a Hash, Array, `Struct`,
198
+ `Data`, `Set`, or any other value whose JSON is an object or an array, such as the exception
199
+ backtrace) is emitted as quoted JSON, `user="{\"id\":7}"`, instead of its `#inspect` text.
200
+ * `:syslog`: an entry with an exception stays on one line; the newlines between the exception and
201
+ its backtrace are escaped as `\n`.
202
+ * `:default`, `:color`, `:pattern`, and `:syslog`: with `escape_control_chars`, the logger name,
203
+ thread name, file name, payload, the messages of the exceptions that caused the logged exception,
204
+ and the backtrace lines are escaped too. Invalid UTF-8 bytes are dropped instead of the entry failing to
205
+ format. With escaping on, `:color` renders the payload plain and on one line, since Amazing
206
+ Print's colors and line breaks cannot be told apart from control characters in the data.
207
+ * `:one_line`: line breaks are removed from every field (name, thread name, file name, tags, named
208
+ tags, payload, message, and exception), carriage returns as well as newlines, and
209
+ `escape_control_chars` is honoured.
210
+ * JSON based formatters: `Infinity` and `NaN` payload values are serialized as strings, and
211
+ structures nested deeper than 32 levels are replaced with a placeholder such as
212
+ `#<Hash nested too deeply>`, instead of the whole entry being dropped. A `Struct`, `Data`, or
213
+ `OpenStruct` value is written as an object of its members, and a `Set` as an array, as Rails
214
+ already does, instead of its `#inspect` text. An exception in the payload is written as its
215
+ message, and any other object whose JSON fails because of invalid UTF-8 as its `#to_s`, with
216
+ the invalid bytes dropped. The `:elasticsearch` and `:opensearch` appenders write their bulk lines the same way.
217
+ * JSON based formatters and `:one_line`: invalid UTF-8 bytes in the message are dropped, and a
218
+ message in another encoding is converted to UTF-8, instead of the entry failing to format.
219
+ * `:loki`: a nested Hash, Array, `Struct`, or `Set` in the payload is written once as JSON, keeping
220
+ the JSON types of its values, so `{number: 1}` is written as `{"number":1}` rather than
221
+ `{"number":"1"}`, and `[1, 2, 3]` as `[1,2,3]`. A payload that is not a Hash is written as a
222
+ single `payload` field.
223
+ * `:open_telemetry`: a `Struct`, `Set`, or other structure in the payload, or in an Array in it, is
224
+ written as JSON instead of its `#inspect`. A payload that is not a Hash is submitted as a single
225
+ `payload` attribute.
226
+ * Hashes logged as the payload, for example `logger.info("Webhook", params)`: a key that is also
227
+ a log argument (`duration`, `metric_amount`, `backtrace`, `log_exception`, ...) is only used as
228
+ that argument when its value is valid for it. Otherwise it stays in the payload, where it was
229
+ previously consumed or made the call raise. A `nil` value is ignored. When an explicit
230
+ `payload:` is also given, the other keys win over its values of the same name, a String key such
231
+ as `"account_id"` having the same name as `:account_id`.
232
+ * Hashes returned from a logging block, for example `logger.info("Webhook") { {user: "bob"} }`, are
233
+ checked the same way. A log argument from the block, such as `message`, `duration`, `exception`,
234
+ or `backtrace`, is only used when the log entry does not have it yet, and is otherwise kept in the
235
+ payload. A backtrace captured because the level is at or above `backtrace_level` counts, so the
236
+ block's `backtrace` no longer replaces it. The block's keys no longer replace values the log call
237
+ supplied under the same name, whether as a String or a Symbol, and win over the values of the
238
+ block's own `payload` with the same name.
239
+ * The HTTP appender and the appenders built on it (`:elasticsearch_http`, `:splunk_http`, `:loki`,
240
+ `:signalfx`): the url in the error logged for a rejected request shows only the scheme, host,
241
+ and port, and `#inspect` shows only the object address and that url.
242
+ * The `:statsd` metrics appender sends a counter as a single `metric:amount|c` packet.
243
+
244
+ #### Other behaviour changes
245
+
246
+ * An async or batching appender discards a log message that it cannot format, instead of
247
+ retrying it. Discarded messages are reported to the internal logger and counted in the new
248
+ `failed` key of the appender's stats. Every error raised by a built-in appender's formatter
249
+ counts, as does a message too large for a UDP datagram, a MongoDB document that BSON cannot
250
+ serialize, and a message GELF cannot send. A custom appender can format through
251
+ `format_log(log)` (or `format_batch(logs)`) instead of `formatter.call` to get the same
252
+ treatment, or override `#message_error?` for errors its own client raises.
253
+ * The text formatters write a payload nested more than 32 levels deep as the JSON formatters
254
+ write it, cut off with their placeholder, such as `"#<Hash nested too deeply>"`.
255
+ * A log message that overflows the stack while it is written is discarded, and the worker
256
+ thread carries on in a new thread, instead of the worker stopping for good.
257
+ * `#inspect` on every appender shows only its class, object address, and what it says is safe to
258
+ show, such as `url="https://es.example.com:9200"`, `queue_name="logs"`, or `group="/my/app"`,
259
+ instead of its instance variables. A custom appender can show more by overriding
260
+ `#inspect_details`. An appender added with `async: true` or `batch: true` shows as
261
+ `#<SemanticLogger::Appender::Async:0x... batch=true appender=#<...>>`.
262
+ * `:cloudwatch_logs` sends the buffered messages in as many requests as the PutLogEvents limits
263
+ need, oldest first, cuts short a message larger than 1 MB, and reports to the internal logger a
264
+ request that fails, and the messages that AWS rejects as too new, too old, or expired. It formats each message when it is logged, writes invalid UTF-8 as valid
265
+ UTF-8, and discards a message that its formatter cannot write, or writes as something other than
266
+ a String with content, instead of failing the whole request.
267
+ * `:sentry_ruby` leaves the log entry's payload and named tags unchanged, so every appender after
268
+ it, and your own Hash, keep the keys it reads. A user field, or `user` Hash, in the payload no
269
+ longer replaces one from the named tags. A payload user field that is not a String, Symbol, or
270
+ Integer, a `fingerprint` that is not an Array of Strings, Symbols, or numbers, and a payload that
271
+ is not a Hash, are sent as data instead.
272
+
273
+ #### Stricter `logger_measure_method` arguments
274
+
275
+ `logger_measure_method` now raises `ArgumentError` for a method name that is not a valid Ruby
276
+ method name, a `min_duration` that is not a finite number, and an invalid `log_exception` or
277
+ level. Numeric Strings, such as `min_duration: "100"`, are still accepted.
278
+
13
279
  ### Upgrading to Semantic Logger v5.1
14
280
 
15
- v5.1 deprecates several legacy appenders and one appender option. They all still work in v5.1, but
16
- each now emits a Ruby deprecation warning and will be **removed in v6**. The warnings use
17
- `category: :deprecated`, so they are silent by default and appear when deprecation warnings are
18
- enabled (for example `ruby -W:deprecated`, or Rails development mode). Switch away now to be ready
19
- for v6.
281
+ v5.1 deprecates several legacy appenders and one appender option, and changes when the file appender
282
+ opens its log file.
283
+
284
+ The deprecated appenders and option all still work in v5.1, but each now emits a Ruby deprecation
285
+ warning and will be **removed in v6**. The warnings use `category: :deprecated`, so they are silent
286
+ by default and appear when deprecation warnings are enabled (for example `ruby -W:deprecated`, or
287
+ Rails development mode). Switch away now to be ready for v6.
20
288
 
21
289
  #### Replace the `:sentry` appender with `:sentry_ruby`
22
290
 
@@ -75,6 +343,26 @@ SemanticLogger.add_appender(appender: :elasticsearch, url: "http://localhost:920
75
343
  SemanticLogger.add_appender(appender: :elasticsearch, url: "http://localhost:9200")
76
344
  ~~~
77
345
 
346
+ #### The file appender opens its log file when it is created
347
+
348
+ The file appender now opens its log file at creation rather than on the first write, matching the
349
+ other appenders. A bad path or insufficient permissions therefore raises immediately from
350
+ `SemanticLogger.add_appender`, instead of surfacing later on the appender thread while log messages
351
+ are lost. This restores the pre-4.9 behavior.
352
+
353
+ As a result the log file is created as soon as the appender is added, even if nothing has been
354
+ logged yet, the same as the standard Ruby and Rails loggers. If you add appenders inside a `rescue`
355
+ or a conditional that assumed creation could never fail, review it: the failure now happens earlier
356
+ and in the calling thread.
357
+
358
+ #### Rails: log file failures are caught at boot
359
+
360
+ If you use [rails_semantic_logger](rails.html), the companion gem's v5.1 release pairs with the
361
+ eager open above: a log file that cannot be opened is now caught during boot and the app falls back
362
+ to standard error at the `:warn` level, rather than booting against a silently broken appender. v5.1
363
+ also drops support for Sidekiq v4, v5, and v6. See
364
+ [Upgrading to v5.1](rails.html#upgrading-to-v51) for details.
365
+
78
366
  ### Upgrading to Semantic Logger v5.0
79
367
 
80
368
  #### Minimum Ruby version is now 3.2
@@ -21,13 +21,14 @@ module SemanticLogger
21
21
  # Methods forwarded to the queue processor that owns the thread and queue.
22
22
  # Tuning options (max_queue_size, batch_size, non_blocking, ...) are set once at
23
23
  # construction via SemanticLogger.add_appender, so only the values that callers actually
24
- # read back are re-exposed here. lag_* backs the public SemanticLogger.lag_* API.
24
+ # read back are re-exposed here. lag_* and non_blocking= back the public
25
+ # SemanticLogger.lag_* and SemanticLogger.non_blocking= API on the main Processor.
25
26
  def_delegators :@processor,
26
27
  :log, :flush, :close, :thread, :active?, :queue, :max_queue_size,
27
- :capped?, :non_blocking?, :batch?,
28
+ :capped?, :non_blocking?, :non_blocking=, :batch?,
28
29
  :lag_check_interval, :lag_check_interval=,
29
30
  :lag_threshold_s, :lag_threshold_s=,
30
- :processed_count, :dropped_count
31
+ :processed_count, :dropped_count, :failed_count
31
32
 
32
33
  # Appender proxy to allow an existing appender to run asynchronously in a separate thread.
33
34
  #
@@ -73,7 +74,8 @@ module SemanticLogger
73
74
  #
74
75
  # dropped_message_report_seconds: [Integer]
75
76
  # When non_blocking is enabled, log the count of dropped messages to the internal logger
76
- # at most once every this number of seconds.
77
+ # at most once every this number of seconds. Also limits how often messages discarded
78
+ # because their content could not be written are reported, after the first.
77
79
  # Default: 30
78
80
  #
79
81
  # async_max_retries: [Integer]
@@ -87,6 +89,14 @@ module SemanticLogger
87
89
  @processor = QueueProcessor.start(appender: appender, **args)
88
90
  end
89
91
 
92
+ # Returns [String] the class and object address of this proxy, whether it batches, and the
93
+ # wrapped appender's own #inspect, which shows only what that appender says is safe, but none of
94
+ # the instance variables of this proxy or its queue processor. See Subscriber#inspect.
95
+ def inspect
96
+ address = Kernel.instance_method(:to_s).bind_call(self).delete_suffix(">")
97
+ "#{address} batch=#{batch?} appender=#{appender.inspect}>"
98
+ end
99
+
90
100
  # Re-open appender after a fork.
91
101
  def reopen
92
102
  appender.reopen if appender.respond_to?(:reopen)
@@ -104,6 +114,8 @@ module SemanticLogger
104
114
  # processed: [Integer] Cumulative number of log messages written since startup.
105
115
  # dropped: [Integer] Cumulative number of log messages dropped because the queue
106
116
  # was full (only possible when non_blocking is enabled).
117
+ # failed: [Integer] Cumulative number of log messages discarded because their
118
+ # content could not be written (see Subscriber#message_error?).
107
119
  def stats
108
120
  {
109
121
  name: name,
@@ -113,7 +125,8 @@ module SemanticLogger
113
125
  capped: capped?,
114
126
  max_queue_size: capped? ? max_queue_size : nil,
115
127
  processed: processed_count,
116
- dropped: dropped_count
128
+ dropped: dropped_count,
129
+ failed: failed_count
117
130
  }
118
131
  end
119
132
  end