philiprehberger-structured_logger 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +69 -1
- data/lib/philiprehberger/structured_logger/logger.rb +101 -2
- data/lib/philiprehberger/structured_logger/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc219c6050b216013a637d5318a9709a870c024ab1edb390868062659343ba7f
|
|
4
|
+
data.tar.gz: 38c8d7a6d7938d2f7494c805d7f77460d5f644957110221bf5621c5e39db0516
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f29e6f9e660c14de6266aa7897ea2abbd6805eea66c5c027b32e6076175cdf86cfc592849b85c0ed09397b14b57bf90bba52c3f441edcff93f23d27f436dfed6
|
|
7
|
+
data.tar.gz: f09c837882298ea589e3b18730c523ec6343254945c9e4c2cf8c1a0928af51d5fa0753a15abde31674df1d792801d3ffc0dd84bc8c89f31ec4fe5eca98744fc3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-05-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Logger#log_exception(structured_backtrace: true)` option to emit the backtrace as an array of `{file:, line:, method:}` hashes for friendlier log-aggregation indexing
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- README now includes the standard package card image after the badges
|
|
17
|
+
- Bug report issue template now includes a required `gem-version` field
|
|
18
|
+
|
|
19
|
+
## [0.5.0] - 2026-04-25
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `Logger#with_tags(*tags)` for tagged context blocks with automatic restoration
|
|
23
|
+
- `Logger#measure_value(message)` variant of `#measure` that returns the block result while still emitting the timing entry
|
|
24
|
+
|
|
10
25
|
## [0.4.0] - 2026-04-17
|
|
11
26
|
|
|
12
27
|
### Added
|
data/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-structured_logger)
|
|
5
5
|
[](https://github.com/philiprehberger/rb-structured-logger/commits/main)
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
Structured JSON logger with context and child loggers
|
|
8
10
|
|
|
9
11
|
## Requirements
|
|
@@ -121,6 +123,36 @@ end
|
|
|
121
123
|
logger.log_exception(e, level: :fatal, user_id: 42)
|
|
122
124
|
```
|
|
123
125
|
|
|
126
|
+
### Structured backtraces
|
|
127
|
+
|
|
128
|
+
By default `log_exception` emits the backtrace as an array of raw
|
|
129
|
+
strings (matching `exception.backtrace`). Pass `structured_backtrace: true`
|
|
130
|
+
to parse each line into a hash with `:file`, `:line`, and `:method`
|
|
131
|
+
keys — easier to index in log-aggregation systems like Elasticsearch,
|
|
132
|
+
Datadog, or Loki:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
begin
|
|
136
|
+
risky_operation
|
|
137
|
+
rescue => e
|
|
138
|
+
logger.log_exception(e, structured_backtrace: true)
|
|
139
|
+
# => {
|
|
140
|
+
# "timestamp":"...",
|
|
141
|
+
# "level":"error",
|
|
142
|
+
# "message":"something broke",
|
|
143
|
+
# "error_class":"RuntimeError",
|
|
144
|
+
# "backtrace":[
|
|
145
|
+
# {"file":"app/foo.rb","line":42,"method":"bar"},
|
|
146
|
+
# {"file":"lib/baz.rb","line":7,"method":"qux"}
|
|
147
|
+
# ]
|
|
148
|
+
# }
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Lines without a method segment omit the `:method` key. Lines that
|
|
153
|
+
don't match the standard Ruby backtrace format fall back to
|
|
154
|
+
`{ raw: "<original line>" }`.
|
|
155
|
+
|
|
124
156
|
### Timing a block
|
|
125
157
|
|
|
126
158
|
Use `measure` to time a block and emit a single structured event with `duration_ms`:
|
|
@@ -256,6 +288,40 @@ logger.close
|
|
|
256
288
|
|
|
257
289
|
When the buffer is full, writes fall back to synchronous mode (backpressure) to avoid dropping log entries.
|
|
258
290
|
|
|
291
|
+
### Tagged Context
|
|
292
|
+
|
|
293
|
+
Use `with_tags` to add tags to the logging context for the duration of a block. Tags merge with any existing tags (de-duplicated, preserving order) and the original context is restored on exit:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
require "philiprehberger/structured_logger"
|
|
297
|
+
|
|
298
|
+
logger = Philiprehberger::StructuredLogger::Logger.new
|
|
299
|
+
|
|
300
|
+
logger.with_tags("auth", "request") do
|
|
301
|
+
logger.info("Login attempt")
|
|
302
|
+
# => {"timestamp":"...","level":"info","message":"Login attempt","tags":["auth","request"]}
|
|
303
|
+
end
|
|
304
|
+
# Tags are restored after the block
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Measuring with Return Value
|
|
308
|
+
|
|
309
|
+
Use `measure_value` when you need the block's result while still emitting a timing entry:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
require "philiprehberger/structured_logger"
|
|
313
|
+
|
|
314
|
+
logger = Philiprehberger::StructuredLogger::Logger.new
|
|
315
|
+
|
|
316
|
+
result = logger.measure_value("db.query") do
|
|
317
|
+
# ...query the database...
|
|
318
|
+
[{ id: 1, name: "Alice" }]
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# result == [{ id: 1, name: "Alice" }]
|
|
322
|
+
# => {"timestamp":"...","level":"info","message":"db.query","event":"db.query","duration_ms":12.345}
|
|
323
|
+
```
|
|
324
|
+
|
|
259
325
|
## API
|
|
260
326
|
|
|
261
327
|
### `Philiprehberger::StructuredLogger::Logger`
|
|
@@ -273,8 +339,10 @@ When the buffer is full, writes fall back to synchronous mode (backpressure) to
|
|
|
273
339
|
| `level=(new_level)` | Set the minimum log level |
|
|
274
340
|
| `with_context(**extra, &block)` | Temporarily merge context for a block |
|
|
275
341
|
| `silence(level = :fatal, &block)` | Temporarily raise log level for a block |
|
|
276
|
-
| `log_exception(exception, level: :error, **extra)` | Log exception details |
|
|
342
|
+
| `log_exception(exception, level: :error, structured_backtrace: false, **extra)` | Log exception details. Pass `structured_backtrace: true` to emit the backtrace as `{file:, line:, method:}` hashes |
|
|
277
343
|
| `measure(event_name, **context) { block }` | Time a block, emit an info event with `duration_ms`, and re-raise on failure |
|
|
344
|
+
| `#with_tags(*tags) { block }` | Add tags to context for the block |
|
|
345
|
+
| `#measure_value(message) { block }` | Like #measure but returns the block's value |
|
|
278
346
|
| `add_output(io, level: nil, formatter: nil)` | Add an output destination at runtime |
|
|
279
347
|
| `with_correlation_id(id = nil, &block)` | Set a correlation ID for the block |
|
|
280
348
|
| `flush` | Force write of all buffered log entries |
|
|
@@ -10,6 +10,12 @@ module Philiprehberger
|
|
|
10
10
|
|
|
11
11
|
CORRELATION_ID_KEY = :philiprehberger_structured_logger_correlation_id
|
|
12
12
|
|
|
13
|
+
# Regex matching a single Ruby backtrace line. Captures the file
|
|
14
|
+
# path, the line number, and (optionally) the method name. Handles
|
|
15
|
+
# both Ruby 3.4+ single-quote (`'method'`) and Ruby 3.3-and-earlier
|
|
16
|
+
# backtick-apostrophe (`` `method' ``) quoting.
|
|
17
|
+
BACKTRACE_LINE = /\A(?<file>.+?):(?<line>\d+)(?::in ['`](?<method>[^']+)')?\z/
|
|
18
|
+
|
|
13
19
|
attr_reader :context, :level
|
|
14
20
|
|
|
15
21
|
def initialize(**opts)
|
|
@@ -50,6 +56,41 @@ module Philiprehberger
|
|
|
50
56
|
end
|
|
51
57
|
end
|
|
52
58
|
|
|
59
|
+
# Adds the given tags to the logger's context under the `:tags`
|
|
60
|
+
# key, merging with any existing tags (de-duplicated, preserving
|
|
61
|
+
# insertion order). When a block is given, the previous context is
|
|
62
|
+
# restored when the block exits (even on exception). Without a
|
|
63
|
+
# block, the change persists like {#with_context}.
|
|
64
|
+
#
|
|
65
|
+
# @param tags [Array<String, Symbol>] one or more tags to add.
|
|
66
|
+
# @yield (optional) executes within the tagged context; the
|
|
67
|
+
# original context is restored on exit.
|
|
68
|
+
# @return [Object, Hash] the block's return value when a block is
|
|
69
|
+
# given, otherwise the new merged context hash.
|
|
70
|
+
#
|
|
71
|
+
# @example Block form
|
|
72
|
+
# logger.with_tags('auth', 'request') do
|
|
73
|
+
# logger.info('Login attempt')
|
|
74
|
+
# # entry includes tags: ['auth', 'request']
|
|
75
|
+
# end
|
|
76
|
+
def with_tags(*tags)
|
|
77
|
+
existing = @context[:tags] || []
|
|
78
|
+
merged_tags = (existing + tags).uniq
|
|
79
|
+
if block_given?
|
|
80
|
+
@monitor.synchronize do
|
|
81
|
+
original = @context
|
|
82
|
+
@context = @context.merge(tags: merged_tags).freeze
|
|
83
|
+
yield
|
|
84
|
+
ensure
|
|
85
|
+
@context = original
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
@monitor.synchronize do
|
|
89
|
+
@context = @context.merge(tags: merged_tags).freeze
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
53
94
|
def silence(temp_level = :fatal, &block)
|
|
54
95
|
@monitor.synchronize do
|
|
55
96
|
original = @level
|
|
@@ -69,10 +110,41 @@ module Philiprehberger
|
|
|
69
110
|
Thread.current[CORRELATION_ID_KEY] = previous
|
|
70
111
|
end
|
|
71
112
|
|
|
72
|
-
|
|
113
|
+
# Logs an exception's message, class, and backtrace as a single
|
|
114
|
+
# structured entry.
|
|
115
|
+
#
|
|
116
|
+
# @param exception [Exception] the exception to log.
|
|
117
|
+
# @param level [Symbol] the log level for the entry (default
|
|
118
|
+
# `:error`).
|
|
119
|
+
# @param structured_backtrace [Boolean] when `false` (default),
|
|
120
|
+
# the backtrace is emitted as an array of raw strings (the same
|
|
121
|
+
# shape as `exception.backtrace`). When `true`, each backtrace
|
|
122
|
+
# line is parsed into a hash with `:file`, `:line` (Integer),
|
|
123
|
+
# and (when present) `:method` keys. Lines that don't match the
|
|
124
|
+
# standard Ruby backtrace format are passed through as
|
|
125
|
+
# `{ raw: "<original line>" }`. The parsed form is generally
|
|
126
|
+
# easier to index in log-aggregation systems like Elasticsearch,
|
|
127
|
+
# Datadog, or Loki.
|
|
128
|
+
# @param extra [Hash] additional context merged into the log
|
|
129
|
+
# entry.
|
|
130
|
+
# @return [void]
|
|
131
|
+
#
|
|
132
|
+
# @example Default (raw string backtrace)
|
|
133
|
+
# logger.log_exception(e)
|
|
134
|
+
# # backtrace: ["app/foo.rb:42:in 'bar'", ...]
|
|
135
|
+
#
|
|
136
|
+
# @example Structured backtrace
|
|
137
|
+
# logger.log_exception(e, structured_backtrace: true)
|
|
138
|
+
# # backtrace: [
|
|
139
|
+
# # { file: "app/foo.rb", line: 42, method: "bar" },
|
|
140
|
+
# # ...
|
|
141
|
+
# # ]
|
|
142
|
+
def log_exception(exception, level: :error, structured_backtrace: false, **extra)
|
|
143
|
+
bt = exception.backtrace || []
|
|
144
|
+
bt = parse_backtrace(bt) if structured_backtrace
|
|
73
145
|
log(level, exception.message,
|
|
74
146
|
error_class: exception.class.name,
|
|
75
|
-
backtrace:
|
|
147
|
+
backtrace: bt,
|
|
76
148
|
**extra)
|
|
77
149
|
end
|
|
78
150
|
|
|
@@ -111,6 +183,23 @@ module Philiprehberger
|
|
|
111
183
|
end
|
|
112
184
|
end
|
|
113
185
|
|
|
186
|
+
# Variant of {#measure} that emits the same timing log entry but
|
|
187
|
+
# also returns the block's return value. Captures and re-raises
|
|
188
|
+
# exceptions like {#measure}.
|
|
189
|
+
#
|
|
190
|
+
# @param event_name [String, Symbol] the event name to record as
|
|
191
|
+
# the `event` field in the log entry.
|
|
192
|
+
# @param context [Hash] extra context merged into the log entry.
|
|
193
|
+
# @yield executes the measured block.
|
|
194
|
+
# @return [Object] the block's return value on success.
|
|
195
|
+
# @raise re-raises any exception raised by the block.
|
|
196
|
+
#
|
|
197
|
+
# @example Capturing a query result
|
|
198
|
+
# result = logger.measure_value('db.query') { query_database }
|
|
199
|
+
def measure_value(event_name, **context, &)
|
|
200
|
+
measure(event_name, **context, &)
|
|
201
|
+
end
|
|
202
|
+
|
|
114
203
|
def flush
|
|
115
204
|
@monitor.synchronize { @outputs.each { |out| out[:io].flush if out[:io].respond_to?(:flush) } }
|
|
116
205
|
end
|
|
@@ -161,6 +250,16 @@ module Philiprehberger
|
|
|
161
250
|
def validate_level!(level)
|
|
162
251
|
raise ArgumentError, "Invalid level: #{level}" unless LEVELS.key?(level)
|
|
163
252
|
end
|
|
253
|
+
|
|
254
|
+
def parse_backtrace(backtrace)
|
|
255
|
+
backtrace.map do |line|
|
|
256
|
+
if (m = line.match(BACKTRACE_LINE))
|
|
257
|
+
{ file: m[:file], line: m[:line].to_i, method: m[:method] }.compact
|
|
258
|
+
else
|
|
259
|
+
{ raw: line }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
164
263
|
end
|
|
165
264
|
|
|
166
265
|
# Builds output configuration from constructor options.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-structured_logger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A zero-dependency Ruby gem for structured JSON logging with context merging,
|
|
14
14
|
child loggers, level filtering, and pluggable outputs.
|