semantic_logger 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -10
  3. data/Rakefile +40 -0
  4. data/docs/api.md +506 -0
  5. data/docs/appenders.md +1074 -0
  6. data/docs/config.md +707 -0
  7. data/docs/index.md +223 -0
  8. data/docs/log.md +116 -0
  9. data/docs/metrics.md +171 -0
  10. data/docs/operations.md +446 -0
  11. data/docs/rails.md +890 -0
  12. data/docs/security.md +286 -0
  13. data/docs/testing.md +287 -0
  14. data/docs/upgrading.md +697 -0
  15. data/lib/semantic_logger/appender/async.rb +18 -5
  16. data/lib/semantic_logger/appender/bugsnag.rb +1 -1
  17. data/lib/semantic_logger/appender/cloudwatch_logs.rb +126 -29
  18. data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
  19. data/lib/semantic_logger/appender/elasticsearch_base.rb +48 -23
  20. data/lib/semantic_logger/appender/elasticsearch_http.rb +15 -8
  21. data/lib/semantic_logger/appender/file.rb +141 -28
  22. data/lib/semantic_logger/appender/graylog.rb +22 -6
  23. data/lib/semantic_logger/appender/honeybadger.rb +1 -1
  24. data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
  25. data/lib/semantic_logger/appender/http.rb +70 -11
  26. data/lib/semantic_logger/appender/io.rb +1 -1
  27. data/lib/semantic_logger/appender/kafka.rb +26 -12
  28. data/lib/semantic_logger/appender/loki.rb +13 -3
  29. data/lib/semantic_logger/appender/mongodb.rb +20 -1
  30. data/lib/semantic_logger/appender/new_relic.rb +7 -1
  31. data/lib/semantic_logger/appender/new_relic_logs.rb +11 -12
  32. data/lib/semantic_logger/appender/open_telemetry.rb +4 -3
  33. data/lib/semantic_logger/appender/opensearch.rb +2 -1
  34. data/lib/semantic_logger/appender/rabbitmq.rb +10 -4
  35. data/lib/semantic_logger/appender/sentry.rb +6 -1
  36. data/lib/semantic_logger/appender/sentry_ruby.rb +91 -17
  37. data/lib/semantic_logger/appender/splunk.rb +10 -1
  38. data/lib/semantic_logger/appender/syslog.rb +66 -25
  39. data/lib/semantic_logger/appender/tcp.rb +10 -4
  40. data/lib/semantic_logger/appender/udp.rb +11 -1
  41. data/lib/semantic_logger/appender/wrapper.rb +3 -2
  42. data/lib/semantic_logger/appender.rb +6 -3
  43. data/lib/semantic_logger/appenders.rb +11 -2
  44. data/lib/semantic_logger/base.rb +82 -32
  45. data/lib/semantic_logger/exception_details.rb +50 -0
  46. data/lib/semantic_logger/formatters/base.rb +52 -6
  47. data/lib/semantic_logger/formatters/color.rb +22 -3
  48. data/lib/semantic_logger/formatters/default.rb +28 -6
  49. data/lib/semantic_logger/formatters/logfmt.rb +155 -21
  50. data/lib/semantic_logger/formatters/loki.rb +26 -22
  51. data/lib/semantic_logger/formatters/one_line.rb +67 -3
  52. data/lib/semantic_logger/formatters/open_telemetry.rb +16 -17
  53. data/lib/semantic_logger/formatters/pattern.rb +11 -5
  54. data/lib/semantic_logger/formatters/raw.rb +4 -5
  55. data/lib/semantic_logger/formatters/syslog.rb +110 -17
  56. data/lib/semantic_logger/formatters/syslog_cee.rb +29 -15
  57. data/lib/semantic_logger/levels.rb +10 -1
  58. data/lib/semantic_logger/log.rb +183 -37
  59. data/lib/semantic_logger/loggable.rb +26 -3
  60. data/lib/semantic_logger/metric/new_relic.rb +1 -1
  61. data/lib/semantic_logger/metric/signalfx.rb +12 -8
  62. data/lib/semantic_logger/metric/statsd.rb +15 -7
  63. data/lib/semantic_logger/queue_processor/request.rb +55 -0
  64. data/lib/semantic_logger/queue_processor/throttled_counter.rb +46 -0
  65. data/lib/semantic_logger/queue_processor.rb +170 -68
  66. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  67. data/lib/semantic_logger/semantic_logger.rb +15 -2
  68. data/lib/semantic_logger/subscriber.rb +96 -10
  69. data/lib/semantic_logger/sync_processor.rb +11 -0
  70. data/lib/semantic_logger/test/capture_log_events.rb +9 -6
  71. data/lib/semantic_logger/utils.rb +244 -16
  72. data/lib/semantic_logger/version.rb +1 -1
  73. data/lib/semantic_logger.rb +1 -1
  74. metadata +33 -6
  75. data/lib/semantic_logger/core_ext/thread.rb +0 -30
data/docs/config.md ADDED
@@ -0,0 +1,707 @@
1
+ ---
2
+ layout: default
3
+ title: Configuration
4
+ description: >-
5
+ Configure Semantic Logger once at startup: global settings, per-appender
6
+ levels and formats, filtering, and writing your own formatters and
7
+ appenders.
8
+ ---
9
+
10
+
11
+ **Contents**
12
+
13
+ * TOC
14
+ {:toc}
15
+
16
+ Semantic Logger is configured once, when your application starts, by setting a few global values
17
+ and adding one or more appenders (destinations). This page walks through that configuration from
18
+ the simplest, most common settings at the top to formatter and appender customization further down.
19
+
20
+ Production concerns such as process forking, performance tuning, signals, and log rotation are
21
+ covered separately in [Operations](operations.html).
22
+
23
+ If you are using Rails, most of this is handled for you by the companion gem
24
+ [rails_semantic_logger](rails.html); configure it through `config.semantic_logger` and
25
+ `config.rails_semantic_logger` instead.
26
+
27
+ A minimal configuration looks like this:
28
+
29
+ ~~~ruby
30
+ require "semantic_logger"
31
+
32
+ # 1. Choose the lowest level to log.
33
+ SemanticLogger.default_level = :info
34
+
35
+ # 2. Add at least one destination.
36
+ SemanticLogger.add_appender(io: $stdout, formatter: :color)
37
+
38
+ # 3. Get a logger and use it.
39
+ logger = SemanticLogger["MyApp"]
40
+ logger.info("Ready")
41
+ ~~~
42
+
43
+ The sections below explain each piece, and everything you can tune around it.
44
+
45
+ ---
46
+
47
+ ## Global settings
48
+
49
+ These module level settings apply to the whole process. Set them once, before or just after adding
50
+ your appenders.
51
+
52
+ ### Default log level
53
+
54
+ Semantic Logger logs `:info` and above by default. The levels, from most detail to least, are:
55
+
56
+ :trace, :debug, :info, :warn, :error, :fatal
57
+
58
+ Setting the level to `:debug` includes `:info`, `:warn`, `:error`, and `:fatal`, but not `:trace`.
59
+ To log everything, set the global default to `:trace`:
60
+
61
+ ~~~ruby
62
+ SemanticLogger.default_level = :trace
63
+ ~~~
64
+
65
+ Every logger and appender uses this global default unless it has been given its own level. Once a
66
+ logger or appender has an explicit level, changing `SemanticLogger.default_level` no longer affects
67
+ it. The global default can also be changed at runtime (see [Signals](operations.html#linux-signals)
68
+ for changing it in a running process without a restart):
69
+
70
+ ~~~ruby
71
+ SemanticLogger.default_level = :debug
72
+ ~~~
73
+
74
+ ### Application, environment, and host name
75
+
76
+ Semantic Logger can include the application name, environment, and host name in every log entry.
77
+ Not every appender uses these fields, but structured appenders (JSON, Elasticsearch, Splunk, and so
78
+ on) and centralized logging systems rely on them to tell apart logs coming from different
79
+ applications and servers:
80
+
81
+ ~~~ruby
82
+ SemanticLogger.application = "my_app"
83
+ SemanticLogger.environment = "production"
84
+ SemanticLogger.host = "web-server-1"
85
+ ~~~
86
+
87
+ When not set explicitly, these default to:
88
+
89
+ - `application`: the `SEMANTIC_LOGGER_APP` environment variable, otherwise `"Semantic Logger"`.
90
+ - `environment`: the first of `SEMANTIC_LOGGER_ENV`, `RAILS_ENV`, or `RACK_ENV` that is set.
91
+ - `host`: the machine's host name.
92
+
93
+ Each value can also be overridden for a single appender by passing `application:`, `environment:`,
94
+ or `host:` to `add_appender` (see [Per-appender settings](#per-appender-settings)).
95
+
96
+ ### Capturing backtraces
97
+
98
+ Semantic Logger can capture the file name and line number where each log entry was created, include
99
+ it in the output, and forward it to error services such as Bugsnag.
100
+
101
+ Capturing a backtrace is expensive, so it is controlled by its own level, which defaults to
102
+ `:error`. Only entries at this level or higher capture a backtrace:
103
+
104
+ ~~~ruby
105
+ # Capture backtraces for :error and :fatal entries (the default)
106
+ SemanticLogger.backtrace_level = :error
107
+ ~~~
108
+
109
+ To capture a backtrace for every entry, set it to `:trace`. To turn backtrace capture off entirely,
110
+ set it to `nil`. It is strongly recommended to leave this at `:error` or higher in production.
111
+
112
+ ### Caching loggers
113
+
114
+ By default `SemanticLogger[...]` returns a brand new logger instance on every call. Enable logger
115
+ caching to have a single shared logger returned per class:
116
+
117
+ ~~~ruby
118
+ SemanticLogger.cache_loggers = true
119
+
120
+ SemanticLogger[MyClass].equal?(SemanticLogger[MyClass]) # => true
121
+ ~~~
122
+
123
+ This makes it possible to obtain a logger once and later change its level (or filter) so that every
124
+ holder of that logger sees the change:
125
+
126
+ ~~~ruby
127
+ SemanticLogger[MyClass].level = :debug
128
+ ~~~
129
+
130
+ Notes:
131
+
132
+ - Caching is opt-in and disabled by default.
133
+ - Only Classes and Modules are cached. A String always returns a new instance, since string call
134
+ sites commonly want an independent logger (for example to set a different level per call site).
135
+ - Anonymous classes (those without a name) are never cached.
136
+ - With caching enabled, `SemanticLogger[MyClass]` and the `SemanticLogger::Loggable` mixin's
137
+ `MyClass.logger` return the same instance.
138
+ - Setting `SemanticLogger.cache_loggers = false` clears the cache. It can also be cleared explicitly
139
+ with `SemanticLogger.clear_logger_cache`, for example after redefining a class.
140
+
141
+ ---
142
+
143
+ ## Per-appender settings
144
+
145
+ Each destination is added with `SemanticLogger.add_appender`. The full catalogue of destinations,
146
+ and the options specific to each, is in [Appenders](appenders.html). In addition to its own
147
+ settings, almost every appender accepts these common options:
148
+
149
+ | Option | Description |
150
+ |--------|-------------|
151
+ | `level` | Only write entries at this level or higher to this appender. Defaults to `SemanticLogger.default_level`. |
152
+ | `formatter` | How to format the output, for example `:default`, `:color`, or `:json`. See [Custom formatters](#custom-formatters). |
153
+ | `filter` | A `Regexp` or `Proc` selecting which entries this appender accepts. See [Filtering](#filtering). |
154
+ | `application`, `environment`, `host` | Override the global values for this appender only. |
155
+
156
+ A per-appender `level` lets each destination keep a different subset of the logs. For example, keep
157
+ a full trace log and a separate warnings-only log:
158
+
159
+ ~~~ruby
160
+ require "semantic_logger"
161
+
162
+ SemanticLogger.default_level = :info
163
+
164
+ # Everything at :trace and above:
165
+ SemanticLogger.add_appender(file_name: "log/trace.log", level: :trace)
166
+
167
+ # Only warnings and above:
168
+ SemanticLogger.add_appender(file_name: "log/warnings.log", level: :warn)
169
+ ~~~
170
+
171
+ ---
172
+
173
+ ## Filtering
174
+
175
+ A filter decides, for each log entry, whether it should be written. Use a filter to:
176
+
177
+ * Quiet a noisy library without modifying its code.
178
+ * Send only certain messages to a particular destination (for example, a dedicated audit file).
179
+ * Strip sensitive data out of a message before it is written.
180
+
181
+ ### A filter is one of three things
182
+
183
+ 1. **A regular expression.** It is matched against the **class name** of the logger (the value you
184
+ passed to `SemanticLogger["..."]`). The entry is kept only if the class name matches.
185
+
186
+ ```ruby
187
+ filter: /MyClass/
188
+ ```
189
+
190
+ 2. **A Proc (or lambda).** It receives the whole log event and must return `true` to **keep** the
191
+ entry. Returning anything else (`false`, `nil`, a string, ...) **drops** it.
192
+
193
+ ```ruby
194
+ filter: ->(log) { log.message !~ /heartbeat/ }
195
+ ```
196
+
197
+ 3. **A module or object that responds to `.call`.** Same contract as a Proc, but in a named,
198
+ reusable, testable place. Reach for this when the logic grows beyond a one-liner.
199
+
200
+ ```ruby
201
+ module ExcludeHealthChecks
202
+ def self.call(log)
203
+ !log.message.to_s.start_with?("GET /health")
204
+ end
205
+ end
206
+
207
+ filter: ExcludeHealthChecks
208
+ ```
209
+
210
+ > **The most common gotcha:** a Proc or `.call` filter must return **exactly `true`** to keep an
211
+ > entry. `0`, `"yes"`, or any truthy-but-not-`true` value will silently drop the entry. When in
212
+ > doubt, end the filter with an explicit boolean expression.
213
+
214
+ The log event passed to a Proc or module filter carries every attribute of the message: `name`,
215
+ `message`, `level`, `payload`, `tags`, `named_tags`, `duration`, `exception`, and more. The full
216
+ list is in [Log Event](log.html).
217
+
218
+ ### Where a filter can be attached
219
+
220
+ * **On an appender** (a destination). The filter affects only what *that one destination* writes.
221
+ Use this to give one file or service a curated subset of the logs.
222
+ * **On a logger instance.** The filter affects *every* appender, but only for entries coming
223
+ *through that one logger*. Use this to quiet a single class or library across all destinations.
224
+
225
+ ### Example: appender filter with a regular expression
226
+
227
+ Keep a full log in `development.log`, and additionally maintain a `my_class.log` that contains
228
+ **only** the messages from `MyClass`:
229
+
230
+ ```ruby
231
+ require "semantic_logger"
232
+
233
+ # Step 1: a catch-all appender that records everything.
234
+ SemanticLogger.add_appender(file_name: "development.log")
235
+
236
+ # Step 2: a second appender that only keeps entries whose logger class name matches /MyClass/.
237
+ SemanticLogger.add_appender(file_name: "my_class.log", filter: /MyClass/)
238
+
239
+ # Step 3: log from two different classes.
240
+ SemanticLogger["MyClass"].info "Written to BOTH development.log and my_class.log"
241
+ SemanticLogger["OtherClass"].info "Written ONLY to development.log"
242
+ ```
243
+
244
+ You can also set the filter after the appender exists:
245
+
246
+ ```ruby
247
+ appender = SemanticLogger.add_appender(file_name: "my_class.log")
248
+ appender.filter = /MyClass/
249
+ ```
250
+
251
+ ### Example: appender filter with a Proc
252
+
253
+ A `summary.log` that contains everything **except** the (very chatty) messages from `MyClass`:
254
+
255
+ ```ruby
256
+ SemanticLogger.add_appender(
257
+ file_name: "summary.log",
258
+ # Keep the entry (return true) unless it came from MyClass.
259
+ filter: ->(log) { log.name != "MyClass" }
260
+ )
261
+ ```
262
+
263
+ Because the Proc receives the whole log event, you can filter on anything:
264
+
265
+ ```ruby
266
+ # Drop entries below a duration threshold (only keep slow measure calls).
267
+ filter: ->(log) { log.duration.to_f >= 100 }
268
+
269
+ # Drop a specific noisy message regardless of which class logged it.
270
+ filter: ->(log) { log.message !~ /\Aheartbeat/ }
271
+ ```
272
+
273
+ ### Example: rewriting a message inside a filter
274
+
275
+ A filter can also **modify** the log event before it is written, as long as it still returns `true`
276
+ so the (now edited) entry is kept. This is handy for redacting sensitive data in a logger's filter,
277
+ as below, or in `on_log` (see [Redacting sensitive data](security.html#redacting-sensitive-data)).
278
+ Do not redact in an appender's filter: every appender writes the same log event, so the appenders
279
+ added before it have already written the data. Resque, for example, logs the entire job payload,
280
+ which may contain private information:
281
+
282
+ ```ruby
283
+ Resque.logger.filter = ->(log) do
284
+ if log.name == "Resque" && (match = log.message.to_s.match(/\A(got|done): /))
285
+ log.message = match[1] # replace the full payload with just the action
286
+ end
287
+ true # always return true so the (edited) message is still logged
288
+ end
289
+ ```
290
+
291
+ ### Example: logger filter to quiet a library
292
+
293
+ When a library lets you replace its logger, attach a filter to a logger instance to suppress its
294
+ noise everywhere it is logged, without touching any appender:
295
+
296
+ ```ruby
297
+ logger = SemanticLogger[Resque]
298
+ logger.filter = ->(log) { log.message !~ /\A\*\*\* Checking/ }
299
+ Resque.logger = logger
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Custom formatters
305
+
306
+ The formatter turns each log event into the text or JSON that an appender writes. Pass a
307
+ `formatter:` when adding an appender. The simplest options are the built-in formatters selected by
308
+ name (`:default`, `:color`, `:json`, `:logfmt`, and others; see [Appenders](appenders.html)). For
309
+ anything beyond those, you have three choices, in increasing order of effort: a pattern string, a
310
+ Proc, or a formatter class.
311
+
312
+ ### Pattern formatter
313
+
314
+ For simple layout changes there is no need to write any code. The built-in `:pattern` formatter
315
+ builds each log line from a pattern string. Placeholders use the form `%{directive}`; to emit a
316
+ literal `%{...}`, escape it as `%%{...}`.
317
+
318
+ ~~~ruby
319
+ # A message-only format on stdout, for end users:
320
+ SemanticLogger.add_appender(
321
+ io: $stdout,
322
+ formatter: {pattern: {pattern: "%{message}"}}
323
+ )
324
+
325
+ # A timestamped format to a file:
326
+ SemanticLogger.add_appender(
327
+ file_name: "application.log",
328
+ formatter: {pattern: {pattern: "%{time} %{level} %{name} -- %{message}"}}
329
+ )
330
+ ~~~
331
+
332
+ The pattern is parsed once when the appender is created, so formatting every entry is fast. An
333
+ unknown directive (or an argument supplied to a directive that does not take one) raises an error
334
+ immediately, when the appender is configured.
335
+
336
+ Available directives:
337
+
338
+ | Directive | Description |
339
+ |------------------------|----------------------------------------------------------|
340
+ | `%{time}` | Formatted timestamp. A strftime format may be supplied, e.g. `%{time:%Y-%m-%dT%H:%M:%S.%6N}`. |
341
+ | `%{level}` | Full level name, e.g. `debug`. |
342
+ | `%{level_short}` | Single character level, e.g. `D`. |
343
+ | `%{name}` | Logger / class name. |
344
+ | `%{message}` | Log message. |
345
+ | `%{payload}` | Payload rendered as a string. |
346
+ | `%{exception_class}` | Class of the logged exception, e.g. `RuntimeError`. |
347
+ | `%{exception_message}` | Message of the logged exception. |
348
+ | `%{exception_details}` | Detail Ruby adds to the exception's message, such as did_you_mean suggestions. See [Exception details](#exception-details). |
349
+ | `%{backtrace}` | Backtrace of the logged exception. |
350
+ | `%{duration}` | Human readable duration, e.g. `1.2ms`. |
351
+ | `%{duration_ms}` | Duration in milliseconds (numeric). |
352
+ | `%{thread_name}` | Name of the thread that logged the message. |
353
+ | `%{pid}` | Process id. |
354
+ | `%{file_name}` | Ruby file name that logged the message, e.g. `app.rb`. |
355
+ | `%{line}` | Line number within the Ruby file, e.g. `42`. |
356
+ | `%{tags}` | Tags, comma separated. |
357
+ | `%{named_tags}` | All named tags. One tag with `%{named_tags:request_id}`. |
358
+ | `%{host}` | Host name. |
359
+ | `%{application}` | Application name. |
360
+ | `%{environment}` | Environment name. |
361
+
362
+ When the pattern is omitted it defaults to a layout similar to the default text formatter:
363
+ `%{time} %{level} [%{pid}:%{thread_name}] %{name} -- %{message}`.
364
+
365
+ #### Example: a custom timestamp format
366
+
367
+ The `%{time}` directive accepts a [strftime](https://ruby-doc.org/core/Time.html#method-i-strftime)
368
+ format string, applied directly to the log time:
369
+
370
+ ~~~ruby
371
+ SemanticLogger.add_appender(
372
+ io: $stdout,
373
+ formatter: {pattern: {pattern: "%{time:%Y-%m-%dT%H:%M:%S.%6N%z} %{level} -- %{message}"}}
374
+ )
375
+ # => 2017-04-05T01:05:52.868286+0000 info -- Hello World
376
+ ~~~
377
+
378
+ #### Example: include a request id from the named tags
379
+
380
+ Named tags (set with `SemanticLogger.tagged(request_id: "...")`) can be pulled out individually with
381
+ `%{named_tags:key}`:
382
+
383
+ ~~~ruby
384
+ SemanticLogger.add_appender(
385
+ io: $stdout,
386
+ formatter: {pattern: {pattern: "%{time} %{level} [%{named_tags:request_id}] %{name} -- %{message}"}}
387
+ )
388
+ ~~~
389
+
390
+ ### Formatter as a Proc
391
+
392
+ For full control with minimal ceremony, supply a block. It is called with two arguments, the log
393
+ event and the appender; a block may accept just the log event and ignore the appender. For the
394
+ structure of the log event, see [Log Event](log.html).
395
+
396
+ ~~~ruby
397
+ formatter = proc do |log|
398
+ # This formatter just returns the log event as a string
399
+ log.inspect
400
+ end
401
+ SemanticLogger.add_appender(io: $stdout, formatter: formatter)
402
+ ~~~
403
+
404
+ ### Formatter as a class
405
+
406
+ When the formatting logic is substantial or reused, subclass one of the built-in formatters and
407
+ override just the methods you want to change.
408
+
409
+ Override the default text formatter to upper-case the level name:
410
+
411
+ ~~~ruby
412
+ class MyFormatter < SemanticLogger::Formatters::Default
413
+ # Return the complete log level name in uppercase
414
+ def level
415
+ log.level.upcase
416
+ end
417
+ end
418
+
419
+ SemanticLogger.add_appender(file_name: "development.log", formatter: MyFormatter.new)
420
+ ~~~
421
+
422
+ The colorized formatter can be customized the same way, keeping its color codes:
423
+
424
+ ~~~ruby
425
+ class MyFormatter < SemanticLogger::Formatters::Color
426
+ def level
427
+ "#{color}#{log.level.upcase}#{color_map.clear}"
428
+ end
429
+ end
430
+
431
+ SemanticLogger.add_appender(file_name: "development.log", formatter: MyFormatter.new)
432
+ ~~~
433
+
434
+ A common request is to leave out the process id, for example when running a single process per
435
+ container (where the pid is always 1):
436
+
437
+ ~~~ruby
438
+ class NoPidFormatter < SemanticLogger::Formatters::Default
439
+ # Leave out the pid
440
+ def pid
441
+ end
442
+ end
443
+
444
+ SemanticLogger.add_appender(file_name: "development.log", formatter: NoPidFormatter.new)
445
+ ~~~
446
+
447
+ See
448
+ [SemanticLogger::Formatters::Default](https://github.com/reidmorrison/semantic_logger/blob/main/lib/semantic_logger/formatters/default.rb)
449
+ and
450
+ [SemanticLogger::Formatters::Color](https://github.com/reidmorrison/semantic_logger/blob/main/lib/semantic_logger/formatters/color.rb)
451
+ for all the methods that can be overridden.
452
+
453
+ To replace the formatter on an appender that is already installed, for example in a Rails app:
454
+
455
+ ~~~ruby
456
+ # Find the file appender and replace its formatter:
457
+ appender = SemanticLogger.appenders.find { |a| a.is_a?(SemanticLogger::Appender::File) }
458
+ appender.formatter = MyFormatter.new
459
+ ~~~
460
+
461
+ ### Escaping control characters
462
+
463
+ By design, the human readable text formatters (`:default` and `:color`) write log messages exactly
464
+ as supplied, including newlines and ANSI color codes. This is intentional and useful: multi-line
465
+ messages and colorized output make local logs easier to read.
466
+
467
+ When log messages can contain untrusted, attacker-controlled data (for example a user name, request
468
+ parameter, or `User-Agent` header), those same characters can be abused. A newline can forge an
469
+ additional, fake log entry ("log forging"), and an ANSI escape sequence can spoof or hide terminal
470
+ output when the log is viewed in a terminal.
471
+
472
+ Structured formatters such as `:json` are not affected, because JSON encoding always escapes control
473
+ characters. They are the recommended choice when forwarding logs that may contain untrusted data to
474
+ a centralized logging system.
475
+
476
+ For the text formatters, enable the `escape_control_chars` option to replace control characters in
477
+ untrusted log data (the logger name, thread name, message, payload, tags, named tags, the message,
478
+ details, and backtrace lines of the logged exception, and the message and backtrace lines of each
479
+ exception that caused it) with a printable, escaped form. For example a newline is written as `\n`
480
+ and the ANSI escape as `\e`. The option is **disabled by default** to preserve the existing human
481
+ readable output:
482
+
483
+ ```ruby
484
+ # Text appender that escapes control characters in untrusted data:
485
+ SemanticLogger.add_appender(file_name: "production.log", formatter: {default: {escape_control_chars: true}})
486
+
487
+ # Colorized appender, still escaping control characters in the logged data
488
+ # (the formatter's own color codes are preserved):
489
+ SemanticLogger.add_appender(io: $stdout, formatter: {color: {escape_control_chars: true}})
490
+ ```
491
+
492
+ The option only escapes the control characters in the logged data; it does not touch the formatter's
493
+ own decoration, so the `:color` formatter keeps emitting its color codes. It does render the payload
494
+ without Amazing Print's colors, and on a single line, since the colors and line breaks Amazing Print
495
+ adds cannot be told apart from those in the data. The text formatters still lay out an exception and
496
+ its backtrace over several lines, since that layout is generated by Semantic Logger rather than
497
+ supplied as log data. The `:syslog` formatter, which escapes control characters by
498
+ default, is the exception: it also escapes the newlines between an exception and its backtrace lines,
499
+ so that each entry is a single line. The pattern formatter supports the same option (`{pattern:
500
+ {pattern: "...", escape_control_chars: true}}`), and the syslog, TCP, and UDP appenders enable it
501
+ where appropriate; see [Appenders](appenders.html).
502
+
503
+ ### Exception details
504
+
505
+ Since Ruby 3.2, Ruby keeps some of the most useful information about an exception out of its
506
+ `message`, and adds it in `Exception#detailed_message` instead: did_you_mean suggestions, the
507
+ error_highlight snippet that points at the failing call, and any detail an exception class adds by
508
+ overriding `#detailed_message`. Semantic Logger logs that detail, without repeating the class name
509
+ that Ruby also adds, on the line after the exception message:
510
+
511
+ ~~~
512
+ 2026-10-25 12:00:00.000000 E [12345:1234 orders.rb:12] Orders -- Failed to ship -- Exception: NoMethodError: undefined method 'upcas' for an instance of String
513
+ Did you mean? upcase
514
+ upcase!
515
+ orders.rb:12:in 'Orders#ship'
516
+ ...
517
+ ~~~
518
+
519
+ The `log_exception_details` formatter option controls it:
520
+
521
+ | Value | Logs |
522
+ |-------|------|
523
+ | `true` | The detail, without the error_highlight source snippet. |
524
+ | `:with_source` | The detail, including the error_highlight source snippet. It is about 100 times slower, because error_highlight re-parses the source file of the failing line, and it copies source code into the log. |
525
+ | `false` | No detail. The exception message is unchanged either way. |
526
+
527
+ It defaults to `true` for the `:default`, `:color`, and `:pattern` formatters, and to `false` for
528
+ every other formatter, including `:one_line` and the syslog formatters, so their output does not
529
+ change. The syslog appender also turns it off for the formatter it uses with a local syslog daemon.
530
+ When enabled, `:one_line` joins the detail onto the same line, after ` -- `. With the `:json`
531
+ formatter and the others built on `:raw`, it adds an `exception.details` field (and `details`
532
+ within each `cause`), which `:logfmt` writes as `exception_details`:
533
+
534
+ ```ruby
535
+ # Include the error_highlight source snippet in development:
536
+ SemanticLogger.add_appender(io: $stdout, formatter: {color: {log_exception_details: :with_source}})
537
+
538
+ # Add exception.details to JSON output:
539
+ SemanticLogger.add_appender(file_name: "production.json", formatter: {json: {log_exception_details: true}})
540
+ ```
541
+
542
+ The pattern formatter's `%{exception_details}` directive interpolates the detail.
543
+
544
+ Notes:
545
+
546
+ * On Ruby 3.2, a `NameError` or `NoMethodError` calls `inspect` on its receiver every time its
547
+ message is built, and logging the detail builds it twice more. If the receivers of logged
548
+ exceptions are slow to inspect, set `log_exception_details: false`. Ruby 3.3 and later do not
549
+ inspect the receiver.
550
+ * An exception class that overrides `#detailed_message` without accepting extra keyword arguments
551
+ (`def detailed_message(highlight: false)`) cannot be asked to leave out error_highlight's source
552
+ snippet. When error_highlight decorates that exception (a `NameError`, `TypeError`, or
553
+ `ArgumentError`), its detail is only logged with `:with_source`.
554
+
555
+ ---
556
+
557
+ ## Custom appenders
558
+
559
+ To write your own log appender it should meet the following requirements:
560
+
561
+ * Inherit from `SemanticLogger::Subscriber`.
562
+ * In the initializer, connect to the resource being logged to.
563
+ * Implement `#log(log)`, which writes to the relevant resource.
564
+ * Implement `#flush` if the resource can be flushed.
565
+ * Write a test for the new appender.
566
+
567
+ The `#log` method receives the log event as its parameter. For its structure, see
568
+ [Log Event](log.html).
569
+
570
+ Basic outline for an appender:
571
+
572
+ ~~~ruby
573
+ require "semantic_logger"
574
+
575
+ class SimpleAppender < SemanticLogger::Subscriber
576
+ attr_reader :host
577
+
578
+ # Add additional arguments to the initializer while supporting all existing ones.
579
+ def initialize(host: host, **args, &block)
580
+ @host = host
581
+ super(**args, &block)
582
+ end
583
+
584
+ # Display the log struct and the text formatted output
585
+ def log(log)
586
+ # Display the raw log structure
587
+ p log
588
+
589
+ # Display the formatted output
590
+ puts format_log(log)
591
+ end
592
+
593
+ # Optional
594
+ def flush
595
+ puts "Flush :)"
596
+ end
597
+
598
+ # Optional
599
+ def close
600
+ puts "Closing :)"
601
+ end
602
+ end
603
+ ~~~
604
+
605
+ When the appender runs asynchronously (`async: true` or batching), an exception raised by `#log`
606
+ is retried after a back-off, on the basis that the destination may be briefly unavailable. An
607
+ exception caused by the content of one message cannot succeed on retry, so that message is discarded
608
+ instead. `#message_error?(exception)` decides which is which. Format each message with
609
+ `format_log(log)` (or a batch with `format_batch(logs)`) rather than `formatter.call`: any error the
610
+ formatter raises is then raised as a `SemanticLogger::Subscriber::FormatError`, which
611
+ `#message_error?` recognizes, since a formatter can only fail because of the content of the message.
612
+ By default it also recognizes the errors raised when serializing a value that cannot be written
613
+ (`JSON::GeneratorError`, `JSON::NestingError`, `EncodingError`, and `FloatDomainError`). Override it
614
+ to recognize errors your appender's client raises for content it cannot write:
615
+
616
+ ~~~ruby
617
+ def message_error?(exception)
618
+ exception.is_a?(MyClient::InvalidDocument) || super
619
+ end
620
+ ~~~
621
+
622
+ A `SystemStackError` is handled separately: the message is discarded, and since a thread must not
623
+ overflow its stack twice (on macOS that crashes Ruby), the worker carries on in a new thread. Do not
624
+ rescue it in `#log` or `#batch`.
625
+
626
+ An appender that supports batching implements `#batch(logs)`. When `#batch` raises a message error,
627
+ the whole batch is discarded by default, since it may already have written some of the messages.
628
+ If `#batch` writes nothing unless every message can be written, for example because it formats
629
+ them all into one request before sending it, return `true` from `#batch_all_or_nothing?`. The batch
630
+ is then split up and retried, so that only the offending message is discarded:
631
+
632
+ ~~~ruby
633
+ def batch(logs)
634
+ client.send_all(logs.map { |log| format_log(log) })
635
+ end
636
+
637
+ def batch_all_or_nothing?
638
+ true
639
+ end
640
+ ~~~
641
+
642
+ Register the appender by passing an instance to `add_appender`:
643
+
644
+ ~~~ruby
645
+ SemanticLogger.add_appender(appender: SimpleAppender.new)
646
+ ~~~
647
+
648
+ Look at the
649
+ [existing appenders](https://github.com/reidmorrison/semantic_logger/tree/main/lib/semantic_logger/appender)
650
+ for good examples. To have a custom appender included in the standard list, submit it with complete
651
+ working tests; see the
652
+ [Graylog Appender Test](https://github.com/reidmorrison/semantic_logger/blob/main/test/appender/graylog_test.rb)
653
+ for an example.
654
+
655
+ ---
656
+
657
+ ## Managing appenders and lifecycle
658
+
659
+ ### Adding and removing appenders
660
+
661
+ `SemanticLogger.add_appender` returns the appender it created, which can be used to remove that
662
+ appender later:
663
+
664
+ ~~~ruby
665
+ appender = SemanticLogger.add_appender(file_name: "development.log")
666
+
667
+ # ... later
668
+ SemanticLogger.remove_appender(appender)
669
+ ~~~
670
+
671
+ Other appender management methods:
672
+
673
+ ~~~ruby
674
+ # The list of currently active appenders
675
+ SemanticLogger.appenders
676
+
677
+ # Remove and close every appender
678
+ SemanticLogger.clear_appenders!
679
+
680
+ # Flush all appenders, then close them ( called automatically at process exit )
681
+ SemanticLogger.close
682
+ ~~~
683
+
684
+ ### Flushing
685
+
686
+ Semantic Logger automatically flushes all appenders (log files, etc.) when a process exits. The
687
+ `flush` method is not defined on individual logger instances, since there may be many of them. To
688
+ perform a global flush of all appenders and wait for any queued messages to be written:
689
+
690
+ ~~~ruby
691
+ SemanticLogger.flush
692
+ ~~~
693
+
694
+ ### Capturing context with `on_log`
695
+
696
+ Register a block to be called for every log entry, just before it is placed on the queue. The block
697
+ runs inline on the thread that created the entry, so it can capture request-scoped or thread-local
698
+ context that would otherwise be lost once the entry is handed off to the background thread:
699
+
700
+ ~~~ruby
701
+ SemanticLogger.on_log do |log|
702
+ log.set_context(:request_id, Thread.current[:request_id])
703
+ end
704
+ ~~~
705
+
706
+ Because these callbacks run on the application's own thread, keep them fast. The captured context is
707
+ available to appenders and formatters as `log.context`.