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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc7adb741998f54fcb27d66e78322d0abcb77c6537298e8b8d9be58b999bf571
4
- data.tar.gz: bbeea6cf1479ffa7548be5ab32cdaca87dc42c8d5657a778e1e71574056ac4f1
3
+ metadata.gz: fc219c6050b216013a637d5318a9709a870c024ab1edb390868062659343ba7f
4
+ data.tar.gz: 38c8d7a6d7938d2f7494c805d7f77460d5f644957110221bf5621c5e39db0516
5
5
  SHA512:
6
- metadata.gz: 67ac2a22e4a7b1b3397256fc39e9e04d11fe219b21096fd1b3c8e9996bb98f8120859baa378448d33cfe55425759c406aac72f17520582fcda4ad23e4171c4a2
7
- data.tar.gz: db854b806432b15b8b891592011f02ca39741975657ef1948d6fcfad6f5408e69101b7100b46007b03588216fd85cb997c7d91fbfa299d99dcff82c88fdb5ace
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
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-structured_logger.svg)](https://rubygems.org/gems/philiprehberger-structured_logger)
5
5
  [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-structured-logger)](https://github.com/philiprehberger/rb-structured-logger/commits/main)
6
6
 
7
+ ![philiprehberger-structured_logger](https://raw.githubusercontent.com/philiprehberger/rb-structured-logger/main/package-card.webp)
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
- def log_exception(exception, level: :error, **extra)
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: exception.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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module StructuredLogger
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
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.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-04-18 00:00:00.000000000 Z
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.