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/config.md CHANGED
@@ -1,9 +1,12 @@
1
1
  ---
2
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.
3
8
  ---
4
9
 
5
- ## Configuration
6
- {:.no_toc}
7
10
 
8
11
  **Contents**
9
12
 
@@ -270,8 +273,11 @@ filter: ->(log) { log.message !~ /\Aheartbeat/ }
270
273
  ### Example: rewriting a message inside a filter
271
274
 
272
275
  A filter can also **modify** the log event before it is written, as long as it still returns `true`
273
- so the (now edited) entry is kept. This is handy for redacting sensitive data. Resque, for example,
274
- logs the entire job payload, which may contain private information:
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:
275
281
 
276
282
  ```ruby
277
283
  Resque.logger.filter = ->(log) do
@@ -339,6 +345,7 @@ Available directives:
339
345
  | `%{payload}` | Payload rendered as a string. |
340
346
  | `%{exception_class}` | Class of the logged exception, e.g. `RuntimeError`. |
341
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). |
342
349
  | `%{backtrace}` | Backtrace of the logged exception. |
343
350
  | `%{duration}` | Human readable duration, e.g. `1.2ms`. |
344
351
  | `%{duration_ms}` | Duration in milliseconds (numeric). |
@@ -467,9 +474,11 @@ characters. They are the recommended choice when forwarding logs that may contai
467
474
  a centralized logging system.
468
475
 
469
476
  For the text formatters, enable the `escape_control_chars` option to replace control characters in
470
- untrusted log data (the message, tags, named tags, and exception message) with a printable, escaped
471
- form. For example a newline is written as `\n` and the ANSI escape as `\e`. The option is **disabled
472
- by default** to preserve the existing human readable output:
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:
473
482
 
474
483
  ```ruby
475
484
  # Text appender that escapes control characters in untrusted data:
@@ -481,11 +490,67 @@ SemanticLogger.add_appender(io: $stdout, formatter: {color: {escape_control_char
481
490
  ```
482
491
 
483
492
  The option only escapes the control characters in the logged data; it does not touch the formatter's
484
- own decoration, so the `:color` formatter keeps emitting its color codes. Multi-line exception
485
- backtraces are also preserved, since they are generated by Semantic Logger rather than supplied as
486
- log data. The pattern formatter supports the same option (`{pattern: {pattern: "...",
487
- escape_control_chars: true}}`), and the syslog, TCP, and UDP appenders enable it where appropriate;
488
- see [Appenders](appenders.html).
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`.
489
554
 
490
555
  ---
491
556
 
@@ -522,7 +587,7 @@ class SimpleAppender < SemanticLogger::Subscriber
522
587
  p log
523
588
 
524
589
  # Display the formatted output
525
- puts formatter.call(log, self)
590
+ puts format_log(log)
526
591
  end
527
592
 
528
593
  # Optional
@@ -537,6 +602,43 @@ class SimpleAppender < SemanticLogger::Subscriber
537
602
  end
538
603
  ~~~
539
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
+
540
642
  Register the appender by passing an instance to `add_appender`:
541
643
 
542
644
  ~~~ruby
data/docs/index.md CHANGED
@@ -1,11 +1,9 @@
1
1
  ---
2
2
  layout: default
3
+ heading: What is Semantic Logger?
4
+ mermaid: true
3
5
  ---
4
6
 
5
- ![Semantic Logger](images/hero.jpg){:.hero-image}
6
-
7
- ## What is Semantic Logger?
8
- {:.no_toc}
9
7
 
10
8
  **Contents**
11
9
 
@@ -177,7 +175,35 @@ Every logger forwards its log events to a single, shared background thread throu
177
175
  queue. That thread writes each event to every registered appender (destination) in turn. Because
178
176
  the writing happens off to the side, the thread that called `logger.info` returns immediately.
179
177
 
180
- ![Log message flow diagram](images/log_event_flow.png "Flow Diagram")
178
+ ~~~mermaid
179
+ flowchart LR
180
+ subgraph callers["Application threads"]
181
+ direction TB
182
+ A["OrderProcessor<br/>logger.info"]
183
+ B["PaymentGateway<br/>logger.measure_info"]
184
+ C["Rails<br/>logger.error"]
185
+ end
186
+
187
+ Q["In-memory queue<br/>max_queue_size: 10,000"]
188
+ P(["Processor<br/>one background thread"])
189
+
190
+ subgraph appenders["Appenders"]
191
+ direction TB
192
+ D["Screen"]
193
+ E["File"]
194
+ F["Elasticsearch"]
195
+ G["Sentry"]
196
+ end
197
+
198
+ A -->|"enqueue, return"| Q
199
+ B --> Q
200
+ C --> Q
201
+ Q --> P
202
+ P --> D
203
+ P --> E
204
+ P --> F
205
+ P --> G
206
+ ~~~
181
207
 
182
208
  This design is why Semantic Logger is both fast and thread safe: log calls from hundreds of
183
209
  concurrent threads simply enqueue events, and each appender writes them out sequentially in the
data/docs/log.md CHANGED
@@ -1,9 +1,12 @@
1
1
  ---
2
2
  layout: default
3
+ title: Log Event
4
+ description: >-
5
+ The Log event object that carries every entry through the pipeline: which
6
+ fields are always present, its attributes and helper methods, and how to
7
+ inspect one.
3
8
  ---
4
9
 
5
- ## Log Event
6
- {:.no_toc}
7
10
 
8
11
  **Contents**
9
12
 
@@ -22,23 +25,27 @@ You meet the Log event whenever you customize Semantic Logger:
22
25
  * A **formatter** receives it and turns it into the final output (text, JSON, ...). See [Custom formatters](config.html#custom-formatters).
23
26
  * A custom **appender**'s `#log(log)` method receives it and writes it to a destination.
24
27
 
25
- In every one of those places you are reading (and may modify) the same object documented here.
28
+ In every one of those places you are reading the same object documented here.
26
29
 
27
30
  ### The event is mutable
28
31
 
29
32
  The Log event is a plain mutable Ruby object: every attribute below has both a reader and a
30
- writer. Filters and formatters are allowed to change it before it is written, for example to
31
- redact a message or enrich the payload:
33
+ writer. All appenders share one event instance per log call, so change it only before it is handed
34
+ to them: in an [`on_log`](config.html#capturing-context-with-on_log) subscriber, which runs for
35
+ every entry, or in the `filter` of a logger instance, which runs for that logger's entries. For
36
+ example, to add context to every entry:
32
37
 
33
38
  ```ruby
34
- filter: ->(log) do
35
- log.message = log.message.gsub(/\d{16}/, "[REDACTED]")
36
- true # keep the (now edited) entry
39
+ SemanticLogger.on_log do |log|
40
+ log.set_context(:request_id, Thread.current[:request_id])
37
41
  end
38
42
  ```
39
43
 
40
- Because all appenders share one event instance per log call, keep any mutation deliberate: a
41
- change made in a filter is visible to every appender that runs afterwards.
44
+ To redact sensitive data, see [Redacting sensitive data](security.html#redacting-sensitive-data).
45
+
46
+ Do not change the event in an appender's `filter` or in a formatter: the appenders that ran before
47
+ it have already written the event, and one that runs asynchronously may be reading it on its own
48
+ thread at the same time.
42
49
 
43
50
  ### Which fields are always present
44
51
 
@@ -87,7 +94,8 @@ These read-only helpers derive convenient values from the attributes above:
87
94
  |`level_to_s`|`String`|The level as a single upper-case character, for example `"I"` for `:info`.|
88
95
  |`cleansed_message`|`String`|The message with Rails/ANSI color escape codes stripped. Used to keep terminal codes out of structured output.|
89
96
  |`each_exception`|Enumerator|Iterates the exception and its nested `cause` chain, yielding `(exception, depth)`.|
90
- |`backtrace_to_s`|`String`|The exception backtrace as a string, including every exception in the `cause` chain.|
97
+ |`backtrace_to_s`|`String`|The exception backtrace as a string, including every exception in the `cause` chain. With a block, each cause's message and each backtrace line is replaced by the value the block returns, for example to escape it.|
98
+ |`exception_details(exception, source: false)`|`String` or `nil`|The detail Ruby's `Exception#detailed_message` adds to the message of `exception` (this event's exception or one of its causes), such as did_you_mean suggestions, without the class name. `source: true` also includes error_highlight's source snippet. `nil` when there is none. Cached on the event.|
91
99
  |`file_name_and_line(short_name = false)`|`[String, Integer]` or `nil`|The file name and line number from the event's backtrace or exception. Pass `true` for just the base file name.|
92
100
  |`to_h`|`Hash`|The event as a plain Hash (via the Raw formatter), including `host`, `application`, and `environment`. Handy for inspecting an event or building a custom formatter.|
93
101
  |`set_context(key, value)`|`Hash`|Lazily initializes `context` and stores a key/value pair on it.|
data/docs/metrics.md CHANGED
@@ -1,9 +1,12 @@
1
1
  ---
2
2
  layout: default
3
+ title: Metrics
4
+ description: >-
5
+ Emit named numeric metrics alongside log entries: counters, durations,
6
+ breaking a metric down with dimensions, and sending them on to a metrics
7
+ backend.
3
8
  ---
4
9
 
5
- ## Metrics
6
- {:.no_toc}
7
10
 
8
11
  **Contents**
9
12
 
data/docs/operations.md CHANGED
@@ -1,9 +1,12 @@
1
1
  ---
2
2
  layout: default
3
+ title: Operations
4
+ description: >-
5
+ Running Semantic Logger in production: process forking, log rotation, tuning
6
+ the background pipeline, Linux signals, and shipping to a centralized
7
+ system.
3
8
  ---
4
9
 
5
- ## Operations
6
- {:.no_toc}
7
10
 
8
11
  **Contents**
9
12
 
@@ -111,19 +114,13 @@ By default the queue is capped (`max_queue_size`, default `10,000`). When it fil
111
114
  because an appender cannot keep up), `logger.info` **blocks** until there is room, guaranteeing no
112
115
  message is lost at the cost of briefly slowing the application.
113
116
 
114
- When availability matters more than complete logs, set `non_blocking: true` so that messages are
115
- **dropped** instead of blocking once the queue is full:
117
+ There are two places a slow appender can hold up the application, and a `non_blocking` setting for
118
+ each.
116
119
 
117
- ~~~ruby
118
- SemanticLogger.add_appender(
119
- file_name: "production.log",
120
- async: true,
121
- non_blocking: true
122
- )
123
- ~~~
124
-
125
- Dropped messages are counted and reported at most once every `dropped_message_report_seconds`
126
- (default `30`) so they do not go unnoticed:
120
+ **A slow appender that runs on its own thread.** An appender added with `async: true` (or a batching
121
+ appender) has a second queue of its own, between the main queue and that appender. Set
122
+ `non_blocking: true` on it so that, once its own queue is full, messages for that appender are
123
+ **dropped** instead of holding up the main thread:
127
124
 
128
125
  ~~~ruby
129
126
  SemanticLogger.add_appender(
@@ -134,6 +131,30 @@ SemanticLogger.add_appender(
134
131
  )
135
132
  ~~~
136
133
 
134
+ The main thread never waits on that appender, so the main queue keeps draining and every other
135
+ appender still receives every message. Prefer this when you know which appender is slow, such as one
136
+ that writes to a remote service. Dropped messages are counted and reported to the internal logger at
137
+ most once every `dropped_message_report_seconds` (default `30`), and appear as `dropped` for that
138
+ appender in `SemanticLogger.stats[:appenders]`.
139
+
140
+ **Everything else.** The main queue still fills if an appender that is not async slows down (for
141
+ example `io: $stdout` writing to a pipe that is not being read), or if the main thread cannot keep up
142
+ with the volume of messages. Set `SemanticLogger.non_blocking = true` so that messages are
143
+ **dropped** instead of blocking `logger.info` once the main queue is full:
144
+
145
+ ~~~ruby
146
+ SemanticLogger.non_blocking = true
147
+ ~~~
148
+
149
+ This can be set at any time, before or after appenders are added. Dropped messages are reported to
150
+ the internal logger at most once every 30 seconds, and the count is available from
151
+ `SemanticLogger.stats[:dropped]`. It is not supported in synchronous mode (`SemanticLogger.sync!`),
152
+ which has no queue.
153
+
154
+ Either setting keeps logging calls from blocking; neither makes `SemanticLogger.flush` return sooner.
155
+ A flush, including the one that runs automatically when the process exits, still waits for every
156
+ message already on the queue to be written.
157
+
137
158
  `non_blocking` applies only to a capped queue. An uncapped queue (`max_queue_size: -1`) never blocks
138
159
  and never drops, but can grow without bound.
139
160
 
@@ -144,8 +165,26 @@ a transient failure (such as a brief network blip to a remote appender) does not
144
165
  logging. Each restart sleeps with an increasing back-off (1 second, then 2, ...), reset as soon as a
145
166
  message is processed successfully.
146
167
 
168
+ A message that fails because of its own content, rather than the destination, is not retried: for
169
+ example a value the formatter cannot serialize (a `JSON::GeneratorError`, `JSON::NestingError`,
170
+ encoding error, or `FloatDomainError`). That one message is discarded, and the thread carries on
171
+ without backing off. When batching with an appender that sends the whole batch in a single request,
172
+ such as the HTTP, Loki, Splunk HTTP, Elasticsearch, and OpenSearch appenders, only that message is
173
+ discarded and the rest of the batch is still written. With an appender that may have written part
174
+ of the batch before failing, the whole batch is discarded, so that no message is written twice.
175
+
176
+ The first discarded message is reported to the internal logger straight away, with its error. After
177
+ that, the number discarded is reported, with the latest error, at most once every
178
+ `dropped_message_report_seconds` (default `30`). The total appears as `failed` for that appender in
179
+ `SemanticLogger.stats[:appenders]`.
180
+
181
+ A `JSON::ParserError`, such as from a destination that answered with an HTML error page, is a
182
+ problem with the destination, so it is retried like any other error. The appender decides which
183
+ errors are caused by message content; see [Custom appenders](config.html#custom-appenders).
184
+
147
185
  After `async_max_retries` (default `100`) consecutive failed restarts the worker thread gives up,
148
- rather than spinning forever on a persistent failure:
186
+ rather than spinning forever on a persistent failure. From then on new log messages for that
187
+ appender are dropped once its queue is full, instead of blocking the application:
149
188
 
150
189
  ~~~ruby
151
190
  SemanticLogger.add_appender(
@@ -159,6 +198,12 @@ SemanticLogger.add_appender(
159
198
  Set `async_max_retries: -1` to retry indefinitely. The back-off still applies and still resets after
160
199
  a successful message.
161
200
 
201
+ A flush or close is never left waiting on a worker thread that cannot answer it. When a batch cannot
202
+ be written, a flush or close taken off the queue with it still runs before the thread backs off; a
203
+ close then stops the thread instead of restarting it. If the worker thread ends without answering,
204
+ for example once it gives up, `SemanticLogger.flush` (including the flush at exit) stops waiting on
205
+ that appender.
206
+
162
207
  ### Give a slow appender its own thread
163
208
 
164
209
  If one destination is slow, such as a remote HTTP service, run just that appender on its own thread
@@ -200,12 +245,13 @@ SemanticLogger.stats
200
245
  # capped: true,
201
246
  # max_queue_size: 10_000,
202
247
  # processed: 1_529,
203
- # dropped: 0 }
248
+ # dropped: 0,
249
+ # failed: 0 } # discarded because their content could not be written
204
250
  # ]
205
251
  # }
206
252
  ~~~
207
253
 
208
- The `processed` and `dropped` counters are cumulative since process startup. Reading `stats` is
254
+ The `processed`, `dropped`, and `failed` counters are cumulative since process startup. Reading `stats` is
209
255
  thread-safe and adds no locking to the logging hot path.
210
256
 
211
257
  Semantic Logger also warns when an entry has waited on the queue too long. Tune the threshold and how
data/docs/rails.md CHANGED
@@ -1,9 +1,12 @@
1
1
  ---
2
2
  layout: default
3
+ title: Rails
4
+ description: >-
5
+ Using Semantic Logger in Rails through the rails_semantic_logger gem:
6
+ configuring appenders, tuning what Rails logs, common recipes, and
7
+ upgrading.
3
8
  ---
4
9
 
5
- ## Rails
6
- {:.no_toc}
7
10
 
8
11
  **Contents**
9
12
 
@@ -275,7 +278,7 @@ you can write color to the screen and JSON to a file at the same time.
275
278
  | `:color` | Plain text with color (uses Amazing Print for the payload when installed). |
276
279
  | `:json` | One JSON object per entry. |
277
280
  | `:logfmt` | `key=value` logfmt output. |
278
- | `:one_line` | Each entry reduced to a single line. |
281
+ | `:one_line` | Each entry reduced to a single line: line breaks are removed from every field (or escaped with `escape_control_chars: true`) and an exception shows only its class and message, without the backtrace. |
279
282
  | A class instance | Any instance of a class derived from `SemanticLogger::Formatters::Base`. |
280
283
  | A `Proc` | Called with the `log` entry; returns the formatted output. |
281
284
 
@@ -677,6 +680,21 @@ If you use a forking server (Puma, Unicorn) or fork worker processes, see
677
680
  [Process Forking](operations.html#process-forking). With Semantic Logger v5 appenders are reopened automatically after
678
681
  a fork, so the manual `after_fork { SemanticLogger.reopen }` hook is usually no longer needed.
679
682
 
683
+ ### Drop messages instead of blocking
684
+
685
+ By default, if log messages arrive faster than the appenders can write them, calls to `logger.info`
686
+ wait once the in-memory queue is full, so requests, jobs, and shutdown hooks that log can stall behind
687
+ a slow destination. When staying responsive matters more than keeping every message, have them dropped
688
+ instead, in `config/application.rb` or `config/environments/production.rb`:
689
+
690
+ ~~~ruby
691
+ config.semantic_logger.non_blocking = true
692
+ ~~~
693
+
694
+ This needs Semantic Logger v5.2 or later. See
695
+ [Drop messages instead of blocking](operations.html#drop-messages-instead-of-blocking) for how it
696
+ relates to the per-appender `non_blocking:` option and how dropped messages are reported.
697
+
680
698
  ### Log rotation
681
699
 
682
700
  Because the log file is held open between writes, rotate it with a **copy-truncate** strategy rather
@@ -712,6 +730,39 @@ After they initialize, Rails Semantic Logger replaces the loggers of these libra
712
730
 
713
731
  ---
714
732
 
733
+ ## Upgrading to v5.1
734
+
735
+ v5.1 requires Semantic Logger v5.1 and needs no configuration changes. Two behaviors differ.
736
+
737
+ ### A log file that cannot be opened now fails at boot
738
+
739
+ Semantic Logger v5.1 opens the file appender's log file when the appender is created, rather than on
740
+ the first write. A missing directory, a bad path, or insufficient permissions therefore raises
741
+ during boot, where Rails Semantic Logger catches it and falls back to logging to standard error at
742
+ the `:warn` level, with a message explaining the problem.
743
+
744
+ Previously that failure surfaced asynchronously on the appender thread after the application had
745
+ already booted, so the fallback never ran and the app kept running against an appender that silently
746
+ dropped every message. If you have been running with an unwritable log path without noticing, this
747
+ release is when you will find out.
748
+
749
+ As a side effect, the log file is created as soon as the appender is added, even if nothing has been
750
+ logged yet. This matches the standard Ruby and Rails loggers.
751
+
752
+ ### Sidekiq v4, v5, and v6 are no longer supported
753
+
754
+ Sidekiq v7 and v8 are supported and tested. The patches for older versions have been removed, along
755
+ with the pre-7.1.6 error handler branches. Sidekiq v7 and v8 both predate this gem's Rails 7.2 and
756
+ Ruby 3.2 minimums, so upgrading Sidekiq is the path forward.
757
+
758
+ On Sidekiq 8 two of its own settings are now honored: `logged_job_attributes` adds job attributes to
759
+ the logging context, and `skip_default_job_logging` suppresses the `Start #perform` /
760
+ `Completed #perform` messages (an alternative to
761
+ `RailsSemanticLogger::Sidekiq::JobLogger.perform_messages = false`). See
762
+ [Sidekiq](#sidekiq) for the logging configuration.
763
+
764
+ ---
765
+
715
766
  ## Migrating from v4 to v5
716
767
 
717
768
  ### Ruby and Rails minimums
@@ -814,3 +865,26 @@ and will be **removed in v6**. Each is replaced by the [appenders block](#config
814
865
  | `config.rails_semantic_logger.filter` | `filter:` on the appender |
815
866
  | `config.rails_semantic_logger.console_logger` | Declare (or omit) an `add_console` appender |
816
867
  | `config.rails_semantic_logger.add_file_appender` | Declare appenders in the block (doing so already replaces the default file appender) |
868
+
869
+ ### Silencing the warnings until you migrate
870
+
871
+ The gem registers its deprecator with the application as
872
+ `Rails.application.deprecators[:rails_semantic_logger]`, so the standard Rails deprecation settings
873
+ govern these warnings:
874
+
875
+ ~~~ruby
876
+ # Hide them until you have migrated
877
+ config.active_support.report_deprecations = false
878
+
879
+ # Or silence only this gem's warnings, leaving the rest of the application's in place
880
+ Rails.application.deprecators[:rails_semantic_logger].silenced = true
881
+
882
+ # Or make them fatal, e.g. in config/environments/test.rb, so CI catches them
883
+ config.active_support.deprecation = :raise
884
+ ~~~
885
+
886
+ The warnings are recorded when the option is set and emitted once Rails has applied the settings
887
+ above. Rails reads `config/environments/*.rb` well before it applies deprecation configuration, so
888
+ warning from the option setter itself would land where none of these settings could reach it. Each
889
+ option is reported once per boot, attributed to the place it was set. Options set later, from
890
+ `config/initializers/*` or at runtime, warn immediately instead.