semantic_logger 5.0.0 → 5.1.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -0
  3. data/Rakefile +29 -0
  4. data/docs/api.md +480 -0
  5. data/docs/appenders.md +990 -0
  6. data/docs/config.md +605 -0
  7. data/docs/index.md +197 -0
  8. data/docs/log.md +108 -0
  9. data/docs/metrics.md +168 -0
  10. data/docs/operations.md +400 -0
  11. data/docs/rails.md +816 -0
  12. data/docs/security.md +119 -0
  13. data/docs/testing.md +285 -0
  14. data/docs/upgrading.md +409 -0
  15. data/lib/semantic_logger/appender/cloudwatch_logs.rb +4 -2
  16. data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
  17. data/lib/semantic_logger/appender/elasticsearch_base.rb +17 -17
  18. data/lib/semantic_logger/appender/elasticsearch_http.rb +14 -7
  19. data/lib/semantic_logger/appender/file.rb +5 -0
  20. data/lib/semantic_logger/appender/new_relic.rb +6 -0
  21. data/lib/semantic_logger/appender/new_relic_logs.rb +2 -1
  22. data/lib/semantic_logger/appender/open_telemetry.rb +2 -1
  23. data/lib/semantic_logger/appender/opensearch.rb +2 -1
  24. data/lib/semantic_logger/appender/rabbitmq.rb +4 -3
  25. data/lib/semantic_logger/appender/sentry.rb +5 -0
  26. data/lib/semantic_logger/appender/syslog.rb +6 -3
  27. data/lib/semantic_logger/appender/tcp.rb +4 -3
  28. data/lib/semantic_logger/appender/wrapper.rb +2 -1
  29. data/lib/semantic_logger/appender.rb +6 -3
  30. data/lib/semantic_logger/appenders.rb +2 -1
  31. data/lib/semantic_logger/base.rb +0 -2
  32. data/lib/semantic_logger/formatters/color.rb +5 -2
  33. data/lib/semantic_logger/formatters/default.rb +7 -2
  34. data/lib/semantic_logger/formatters/logfmt.rb +4 -2
  35. data/lib/semantic_logger/formatters/pattern.rb +3 -1
  36. data/lib/semantic_logger/formatters/syslog.rb +2 -1
  37. data/lib/semantic_logger/formatters/syslog_cee.rb +2 -1
  38. data/lib/semantic_logger/metric/signalfx.rb +4 -2
  39. data/lib/semantic_logger/queue_processor.rb +5 -2
  40. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  41. data/lib/semantic_logger/test/capture_log_events.rb +1 -1
  42. data/lib/semantic_logger/version.rb +1 -1
  43. metadata +14 -3
data/docs/index.md ADDED
@@ -0,0 +1,197 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ ![Semantic Logger](images/hero.jpg){:.hero-image}
6
+
7
+ ## What is Semantic Logger?
8
+ {:.no_toc}
9
+
10
+ **Contents**
11
+
12
+ * TOC
13
+ {:toc}
14
+
15
+ Semantic Logger is a high-performance, asynchronous structured logging framework
16
+ for Ruby and Rails.
17
+
18
+ It does two things that ordinary loggers do not:
19
+
20
+ 1. **It logs structured data, not just strings.** Along with the usual text message you can
21
+ attach a payload (any Hash), an exception, a duration, metrics, and tags. That data is
22
+ preserved all the way to the destination, so it stays searchable instead of being flattened
23
+ into a line of text.
24
+ 2. **It logs asynchronously.** Log events are placed on an in-memory queue and written to their
25
+ destinations by a separate background thread. Your application is not blocked while logs are
26
+ written to disk, a database, or a remote service.
27
+
28
+ If you are using Rails, use the companion gem
29
+ [rails_semantic_logger](https://github.com/reidmorrison/rails_semantic_logger), which replaces
30
+ the Rails logger automatically. See the [Rails guide](rails.html).
31
+
32
+ ## Why use it?
33
+
34
+ ### The problem with traditional logging
35
+
36
+ With the standard Ruby logger you eventually end up building messages by hand:
37
+
38
+ ~~~ruby
39
+ logger.info("Queried users table in #{duration} ms, with a result code of #{result}")
40
+ ~~~
41
+
42
+ That reads fine for a human, but to a machine it is just a string. To answer a question like
43
+ "show me every query that took longer than 100 ms" you have to write fragile regular expressions
44
+ against your log files, and every developer formats their messages slightly differently.
45
+
46
+ ### The Semantic Logger way
47
+
48
+ Log the message and the data separately:
49
+
50
+ ~~~ruby
51
+ logger.info("Queried users table",
52
+ duration: duration,
53
+ result: result,
54
+ table: "users",
55
+ action: "query")
56
+ ~~~
57
+
58
+ Now the same event is still readable by a human, but a machine can index `duration`, `result`,
59
+ `table`, and `action` as real fields. Send it to a JSON file, Elasticsearch, or Splunk and you can
60
+ search, filter, and build dashboards on those fields directly, no log parsing required.
61
+
62
+ ### Reasons developers choose Semantic Logger
63
+
64
+ * **Fast.** Logging happens on a background thread, so even logging thousands of lines per second
65
+ does not slow your application down. See [how it works](#how-it-works) below.
66
+ * **Structured.** Every log entry can carry a payload, exception, duration, metrics, and tags
67
+ without losing that structure.
68
+ * **Human and machine readable at the same time.** Colorized text for developers, JSON for your
69
+ log aggregator, written from the same log call.
70
+ * **Many destinations at once.** Write to a file, the screen, and a remote service simultaneously,
71
+ each with its own log level and format. See [Appenders](appenders.html).
72
+ * **A familiar API.** It supports the standard `debug`/`info`/`warn`/`error`/`fatal` API, so
73
+ existing code and other gems keep working. You mostly just change how the logger is created.
74
+ * **Built for production.** Per-class log levels, change the log level of a running process with a
75
+ [signal](operations.html#linux-signals), tagged logging for tracing requests across threads, automatic exception
76
+ capture, and [metrics](metrics.html) for dashboards.
77
+
78
+ ## Quick start
79
+
80
+ Install the gem:
81
+
82
+ ~~~bash
83
+ gem install semantic_logger
84
+ ~~~
85
+
86
+ Or add it to your `Gemfile`:
87
+
88
+ ~~~ruby
89
+ gem "semantic_logger"
90
+ ~~~
91
+
92
+ Configure it once when your application starts, then log:
93
+
94
+ ~~~ruby
95
+ require "semantic_logger"
96
+
97
+ # Log :info and above. Use :trace, :debug, :info, :warn, :error, or :fatal.
98
+ SemanticLogger.default_level = :info
99
+
100
+ # Send log output to the screen using the colorized formatter.
101
+ SemanticLogger.add_appender(io: $stdout, formatter: :color)
102
+
103
+ # Create a logger for the current class. The class name is added to every message.
104
+ logger = SemanticLogger["MyApp"]
105
+
106
+ logger.info("Application started")
107
+ ~~~
108
+
109
+ That is the whole setup. The [Programmer's Guide](api.html) covers the full logging API.
110
+
111
+ ## A tour of the features
112
+
113
+ The examples below assume you already have a `logger` from `SemanticLogger["MyApp"]`.
114
+
115
+ #### Log a message with structured data
116
+
117
+ ~~~ruby
118
+ logger.error("Outbound call failed", result: :failed, reason_code: -10)
119
+ ~~~
120
+
121
+ #### Log an exception
122
+
123
+ Pass the exception directly. The class, message, and backtrace are all captured, including any
124
+ nested "caused by" exceptions.
125
+
126
+ ~~~ruby
127
+ begin
128
+ # ... code that may raise
129
+ rescue => exception
130
+ logger.error("Import failed", exception)
131
+ end
132
+ ~~~
133
+
134
+ #### Only evaluate expensive messages when needed
135
+
136
+ Pass a block. It runs only when the log level is active, so it is skipped entirely in production
137
+ when the level is higher.
138
+
139
+ ~~~ruby
140
+ logger.debug { "Processed a total of #{records.sum(&:size)} bytes" }
141
+ ~~~
142
+
143
+ #### Measure how long something takes
144
+
145
+ The message is written when the block completes, together with its duration. If the block raises,
146
+ the exception is logged with the duration and then re-raised.
147
+
148
+ ~~~ruby
149
+ logger.measure_info("Called external interface") do
150
+ # Code to call the external service ...
151
+ end
152
+ ~~~
153
+
154
+ #### Tag a block of related log entries
155
+
156
+ Every log entry inside the block carries the tags, which is invaluable for tracing one request
157
+ across many classes and threads.
158
+
159
+ ~~~ruby
160
+ SemanticLogger.tagged(user: "jbloggs", request_id: "abc123") do
161
+ logger.info("Hello World")
162
+ logger.debug("More detail")
163
+ end
164
+ ~~~
165
+
166
+ #### Name your threads
167
+
168
+ So concurrent log entries are easy to tell apart.
169
+
170
+ ~~~ruby
171
+ Thread.current.name = "worker-1"
172
+ ~~~
173
+
174
+ ## How it works
175
+
176
+ Every logger forwards its log events to a single, shared background thread through an in-memory
177
+ queue. That thread writes each event to every registered appender (destination) in turn. Because
178
+ the writing happens off to the side, the thread that called `logger.info` returns immediately.
179
+
180
+ ![Log message flow diagram](images/log_event_flow.png "Flow Diagram")
181
+
182
+ This design is why Semantic Logger is both fast and thread safe: log calls from hundreds of
183
+ concurrent threads simply enqueue events, and each appender writes them out sequentially in the
184
+ correct order. For the rare cases where you want logging to happen inline on the calling thread,
185
+ see [Synchronous Operation](operations.html#synchronous-operation).
186
+
187
+ This design is also tunable for production workloads. The in-memory queue is capped by default
188
+ (blocking when full so that no log message is lost), but it can instead drop messages to protect
189
+ application availability, give a slow appender its own thread, retry transient appender failures,
190
+ and be monitored at runtime. See
191
+ [Performance and reliability tuning](operations.html#performance-and-reliability-tuning) in the
192
+ Configuration guide.
193
+
194
+ ### Ruby Support
195
+
196
+ For the complete list of supported Ruby versions, see
197
+ the [Testing file](https://github.com/reidmorrison/semantic_logger/blob/main/.github/workflows/ci.yml).
data/docs/log.md ADDED
@@ -0,0 +1,108 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ ## Log Event
6
+ {:.no_toc}
7
+
8
+ **Contents**
9
+
10
+ * TOC
11
+ {:toc}
12
+
13
+ Every call to a logger (`logger.info`, `logger.measure_error`, and so on) builds a single **Log
14
+ event** object. That object is the unit of work that flows through the whole pipeline: it is
15
+ handed to each [filter](config.html#filtering), then to each appender's [formatter](config.html#custom-formatters), and
16
+ finally written out by the [appender](appenders.html) itself, usually on a background thread so the
17
+ calling application is not blocked.
18
+
19
+ You meet the Log event whenever you customize Semantic Logger:
20
+
21
+ * A **filter** Proc receives it and returns `true` to keep the entry. See [Filtering](config.html#filtering).
22
+ * A **formatter** receives it and turns it into the final output (text, JSON, ...). See [Custom formatters](config.html#custom-formatters).
23
+ * A custom **appender**'s `#log(log)` method receives it and writes it to a destination.
24
+
25
+ In every one of those places you are reading (and may modify) the same object documented here.
26
+
27
+ ### The event is mutable
28
+
29
+ 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:
32
+
33
+ ```ruby
34
+ filter: ->(log) do
35
+ log.message = log.message.gsub(/\d{16}/, "[REDACTED]")
36
+ true # keep the (now edited) entry
37
+ end
38
+ ```
39
+
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.
42
+
43
+ ### Which fields are always present
44
+
45
+ A handful of attributes are populated for **every** event:
46
+
47
+ * `level`, `level_index`, `name`, `time`, `thread_name`, `tags`, `named_tags`.
48
+
49
+ The rest are **situational**: they are set only when relevant to that particular call. For example
50
+ `duration` is present only for `measure_*` calls, `exception` only when an exception was logged,
51
+ `metric`/`metric_amount`/`dimensions` only for metrics, and `backtrace` only when backtrace
52
+ capture is enabled (`SemanticLogger.backtrace_level`). Always guard against `nil` when reading a
53
+ situational field.
54
+
55
+ ### Attributes
56
+
57
+ |Attribute|Type|Description|
58
+ |---------|----|-----------|
59
+ |`level`|`Symbol`|Log level of the call: `:trace`, `:debug`, `:info`, `:warn`, `:error`, `:fatal`.|
60
+ |`level_index`|`Integer`|Numeric form of the level for fast comparisons: `:trace=>0`, `:debug=>1`, `:info=>2`, `:warn=>3`, `:error=>4`, `:fatal=>5`.|
61
+ |`name`|`String`|Class or component name supplied to the logger, for example the `"MyClass"` in `SemanticLogger["MyClass"]`.|
62
+ |`message`|`String`|The text message. May be `nil` (for example, a metric-only event).|
63
+ |`payload`|`Hash`|Structured data logged alongside the message. `nil` when none was supplied.|
64
+ |`time`|`Time`|The moment the event was created.|
65
+ |`thread_name`|`String`|Name (or id) of the thread that made the log call.|
66
+ |`tags`|`Array<String>`|Tags active on the thread when the call was made.|
67
+ |`named_tags`|`Hash<String, Object>`|Named tags active on the thread when the call was made.|
68
+ |`context`|`Hash`|Named contexts captured in-line at the point the event was created. `nil` when none.|
69
+ |`duration`|`Float`|Time in milliseconds taken by a `measure_*` call. `nil` for ordinary log calls.|
70
+ |`exception`|`Exception`|The Ruby exception that was logged. `nil` when none. Use `each_exception` to walk a `cause` chain.|
71
+ |`backtrace`|`Array<String>`|Backtrace captured at the call site, present only when the level is at or above `SemanticLogger.backtrace_level`.|
72
+ |`metric`|`String`|The metric name, present for metric and `measure_*` calls. See [Metrics](metrics.html).|
73
+ |`metric_amount`|`Float`|Numeric amount for a counter or gauge metric, for example the quantity purchased.|
74
+ |`dimensions`|`Hash`|Dimensions (key/value labels) supplied for a metric.|
75
+
76
+ ### Helper methods
77
+
78
+ These read-only helpers derive convenient values from the attributes above:
79
+
80
+ |Method|Returns|Description|
81
+ |------|-------|-----------|
82
+ |`payload?`|`true`/`false`|Whether the event carries a non-empty payload.|
83
+ |`payload_to_s`|`String` or `nil`|The payload rendered with `inspect`, or `nil` when absent.|
84
+ |`metric_only?`|`true`/`false`|`true` when the event has a metric but no message and no exception. Text appenders typically skip these; machine-readable (JSON) appenders usually keep them.|
85
+ |`duration_to_s`|`String` or `nil`|The duration in milliseconds as a short string, for example `"12ms"`. `nil` when there is no duration.|
86
+ |`duration_human`|`String` or `nil`|The duration in human readable form, scaling up to seconds, minutes, hours, or days as needed.|
87
+ |`level_to_s`|`String`|The level as a single upper-case character, for example `"I"` for `:info`.|
88
+ |`cleansed_message`|`String`|The message with Rails/ANSI color escape codes stripped. Used to keep terminal codes out of structured output.|
89
+ |`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.|
91
+ |`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
+ |`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
+ |`set_context(key, value)`|`Hash`|Lazily initializes `context` and stores a key/value pair on it.|
94
+
95
+ ### Inspecting an event
96
+
97
+ When writing a filter or formatter, the quickest way to see what a real event contains is to dump
98
+ it with `to_h`:
99
+
100
+ ```ruby
101
+ SemanticLogger.add_appender(
102
+ io: $stdout,
103
+ filter: ->(log) do
104
+ pp log.to_h # one-off: inspect the event during development
105
+ true
106
+ end
107
+ )
108
+ ```
data/docs/metrics.md ADDED
@@ -0,0 +1,168 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ ## Metrics
6
+ {:.no_toc}
7
+
8
+ **Contents**
9
+
10
+ * TOC
11
+ {:toc}
12
+
13
+ A **metric** is a named number that Semantic Logger emits alongside a log entry, so the same call
14
+ that records *what happened* can also feed your dashboards and alerts.
15
+
16
+ Use metrics to track things like:
17
+
18
+ * How often an event happens (an error, a sign-up, a cache miss).
19
+ * How long something takes (an external API call, a database query).
20
+ * Running totals, such as the amount purchased per department over time.
21
+
22
+ There are two parts to using metrics:
23
+
24
+ 1. **Emit** a metric by adding the `:metric` option to any log or `measure_` call. This is covered in
25
+ Steps 1 to 3 below.
26
+ 2. **Subscribe** a destination (Statsd, New Relic, SignalFx, and so on) that receives those metrics.
27
+ Until a subscriber is registered, the `:metric` option is harmless and the number goes nowhere.
28
+ See [Send metrics to a backend](#send-metrics-to-a-backend).
29
+
30
+ Metric subscribers are notified asynchronously on the background log thread, so emitting a metric
31
+ never slows the calling thread. A metric rides along with its log entry, so it obeys the same level
32
+ and [filtering](config.html#filtering) rules: a metric on a `:trace` call is not emitted while the
33
+ level is `:info`.
34
+
35
+ > **Not to be confused with operational stats.** The metrics on this page are *application* metrics
36
+ > that your code emits about what it is doing. To monitor Semantic Logger's *own* health instead,
37
+ > such as queue sizes and the number of log entries processed or dropped, use
38
+ > [`SemanticLogger.stats`](operations.html#monitoring-the-background-thread).
39
+
40
+ ## Step 1: Count something
41
+
42
+ The simplest metric counts how often something happens. Add `:metric` with a name to any log call,
43
+ and the metric is incremented by 1:
44
+
45
+ ~~~ruby
46
+ logger.info(message: "Signed up", metric: "users/signup")
47
+ ~~~
48
+
49
+ Supply `:metric_amount` to change the amount. To decrement:
50
+
51
+ ~~~ruby
52
+ logger.info(message: "Item removed", metric: "cart/items", metric_amount: -1)
53
+ ~~~
54
+
55
+ To add to a running total, such as the dollar amount of purchases per department:
56
+
57
+ ~~~ruby
58
+ logger.info(message: "Purchase complete", metric: "departments/clothing", metric_amount: 189.42)
59
+ ~~~
60
+
61
+ Note: Statsd counters are integers, so float amounts are rounded to the nearest integer.
62
+
63
+ ## Step 2: Measure how long something takes
64
+
65
+ A duration metric records elapsed time. The easiest way is to add `:metric` to a `measure_` block
66
+ (see [Measure how long something takes](api.html#step-6-measure-how-long-something-takes) in the
67
+ Guide). The measured duration becomes the metric:
68
+
69
+ ~~~ruby
70
+ logger.measure_info("Called supplier", metric: "supplier/add_user") do
71
+ # Code to call the external service ...
72
+ end
73
+ ~~~
74
+
75
+ When you already have the duration, emit it on a plain log call by supplying both `:metric` and
76
+ `:duration` (in milliseconds):
77
+
78
+ ~~~ruby
79
+ logger.info(message: "Called supplier", metric: "supplier/add_user", duration: 100.23)
80
+ ~~~
81
+
82
+ ## Step 3: Break a metric down with dimensions
83
+
84
+ Dimensions are key/value labels attached to a metric, so a backend that supports them (such as
85
+ SignalFx) can slice it, for example by user, action, or state. Add them with the `:dimensions`
86
+ option:
87
+
88
+ ~~~ruby
89
+ # A counter, broken down by user:
90
+ logger.info(metric: "filters.count", dimensions: {user: "jbloggs"})
91
+
92
+ # A gauge with an amount and dimensions:
93
+ logger.info(metric: "filters.average", metric_amount: 1.2, dimensions: {user: "jbloggs"})
94
+ ~~~
95
+
96
+ Not every backend supports dimensions. **Statsd** and **New Relic** ignore any metric that carries
97
+ dimensions; **SignalFx** is built around them. See each subscriber below.
98
+
99
+ ## Send metrics to a backend
100
+
101
+ A metric goes nowhere until a **metric subscriber** is registered. Add one with
102
+ `SemanticLogger.add_appender(metric: ...)`, usually when your application starts. A subscriber
103
+ receives every logged entry that has a `:metric`, asynchronously on the background thread.
104
+
105
+ ### Statsd
106
+
107
+ Send metrics to [Statsd](https://github.com/statsd/statsd) over UDP, which can roll them up and
108
+ forward them to [Graphite](https://graphiteapp.org), MongoDB, and others (requires the
109
+ `statsd-ruby` gem):
110
+
111
+ ~~~ruby
112
+ SemanticLogger.add_appender(metric: :statsd, url: "udp://localhost:8125")
113
+ ~~~
114
+
115
+ Counters are integers (float amounts are rounded). Does not support dimensions.
116
+
117
+ ### New Relic
118
+
119
+ Forward metrics to New Relic so they can be displayed on custom dashboards (requires the
120
+ `newrelic_rpm` gem):
121
+
122
+ ~~~ruby
123
+ SemanticLogger.add_appender(metric: :new_relic)
124
+ ~~~
125
+
126
+ Does not support dimensions.
127
+
128
+ ### SignalFx
129
+
130
+ Forward metrics to [SignalFx](https://www.splunk.com/en_us/products/infrastructure-monitoring.html),
131
+ which is built around dimensions:
132
+
133
+ ~~~ruby
134
+ SemanticLogger.add_appender(metric: :signalfx, token: "SIGNALFX_ORG_ACCESS_TOKEN")
135
+ ~~~
136
+
137
+ `application` and `host` are always sent as dimensions. To also forward specific named tags as
138
+ dimensions whenever they are present on a log entry, list them:
139
+
140
+ ~~~ruby
141
+ SemanticLogger.add_appender(
142
+ metric: :signalfx,
143
+ token: "SIGNALFX_ORG_ACCESS_TOKEN",
144
+ dimensions: [:user_id, :state]
145
+ )
146
+ ~~~
147
+
148
+ When a duration metric has no dimensions, SignalFx receives both a gauge and a counter, so you can
149
+ chart both the timing and the number of occurrences. When dimensions are present, the metric is sent
150
+ as-is.
151
+
152
+ ### Elasticsearch and Splunk
153
+
154
+ These are ordinary log [appenders](appenders.html), not metric subscribers, but a metric is part of
155
+ the log entry, so `metric`, `metric_amount`, and any dimensions are written into the document
156
+ automatically. You can build dashboards on those fields directly, without registering a separate
157
+ metric subscriber.
158
+
159
+ ## How metrics behave
160
+
161
+ **Asynchronous.** Subscribers run on the background logging thread, so emitting a metric never slows
162
+ down the thread that logged it.
163
+
164
+ **Follows the log level.** A metric is emitted only when its log entry is actually logged, so it
165
+ obeys the same log level and filtering as any other entry. Use this to your advantage: leave detailed
166
+ `:trace` level metrics in the code where they stay dormant under an `:info` level, then turn them on
167
+ when needed, for example by sending the `SIGUSR2` [signal](operations.html#linux-signals) to lower
168
+ the log level on a running process.