philiprehberger-structured_logger 0.1.0 → 0.3.1

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: 6ef7c59f106a991921ebecf414e00246f465120af2cc8d1abf17ebc9dafeb227
4
- data.tar.gz: 6b803b7b323343ba481445416dabffeed056d6a79788ada1c797b752c88dcb87
3
+ metadata.gz: 03457e3b8dbeb12b1eb34a344672baa4e2cd98fd601ea2b71e8e6ce0c2bcedfb
4
+ data.tar.gz: 4067a29a06130c4923473f5daa9ccd5dccea65f6fedf04aca35fd88c77f70f26
5
5
  SHA512:
6
- metadata.gz: 7c14cb179e55002dc57fdaf65fb59dc8d570da2da2fb62205aed82bd49e916704409ab46f0350f690e57d02478591cb256ce49f7fb057655e9894ae8c842e257
7
- data.tar.gz: caef71a9ca0b7d278d77eb2f0a31b7c0349af95905f143535d008a7f3e95f37315c393ca183f383844a05819c7be7c487d98b4c8cf44357b6e237e6ad00820cd
6
+ metadata.gz: f50731219d14fb4d57f473bbb24a1cb919469991e5968315cf0a768fda7af6d9db62f190003a8bd4114474113a0f0db8990fe064f0c4c6e44b80dacf92ec4c95
7
+ data.tar.gz: 0e50cd5166c237bb68c2ecbb526d02adc08cbbd8dc2fe46b543e694b3f7198b20a2390ae8813f3aa4cb2b0dae8864294e9ad9f645ba129efdb167600b04eaa02
data/CHANGELOG.md CHANGED
@@ -1,6 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+ n## [0.3.1] - 2026-03-22
10
+
11
+ ### Changed
12
+ - Update rubocop configuration for Windows compatibility
13
+
14
+ ## [0.3.0] - 2026-03-17
15
+
16
+ ### Added
17
+ - Add multiple outputs (appenders) with per-output level filtering and formatters
18
+ - Add `add_output` for adding output destinations at runtime
19
+ - Add custom formatters: `:json` (default), `:text`, and any callable (proc/lambda)
20
+ - Add `TextFormatter` for human-readable `[TIMESTAMP] LEVEL: message key=value` output
21
+ - Add log sampling with configurable rates per level
22
+ - Add `with_correlation_id` for injecting correlation/request IDs via Thread-local storage
23
+ - Add buffered async output via background thread with backpressure support
24
+ - Add `flush` and `close` methods for async writer lifecycle management
25
+
26
+ ## [0.2.1] - 2026-03-16
27
+
28
+ ### Changed
29
+ - Add License badge to README
30
+ - Add bug_tracker_uri to gemspec
31
+ - Add Development section to README
32
+ - Add Requirements section to README
33
+
34
+ ## [0.2.0] - 2026-03-13
35
+
36
+ ### Added
37
+ - Add `level` getter to read the current log level
38
+ - Add `with_context` for temporarily merging context during a block
39
+ - Add `silence` for temporarily raising the log level during a block
40
+ - Add `log_exception` for logging exception class, message, and backtrace
41
+
3
42
  ## [0.1.0] - 2026-03-10
43
+
44
+ ### Added
4
45
  - Initial release
5
46
  - Structured JSON log output
6
47
  - Log levels: debug, info, warn, error, fatal
data/README.md CHANGED
@@ -1,6 +1,14 @@
1
- # Philiprehberger::StructuredLogger
1
+ # philiprehberger-structured_logger
2
2
 
3
- A zero-dependency Ruby gem for structured JSON logging with context merging, child loggers, level filtering, and pluggable outputs.
3
+ [![Tests](https://github.com/philiprehberger/rb-structured-logger/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-structured-logger/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-structured_logger.svg)](https://rubygems.org/gems/philiprehberger-structured_logger)
5
+ [![License](https://img.shields.io/github/license/philiprehberger/rb-structured-logger)](LICENSE)
6
+
7
+ Structured JSON logger with context merging, child loggers, filtering, and async output for Ruby
8
+
9
+ ## Requirements
10
+
11
+ - Ruby >= 3.1
4
12
 
5
13
  ## Installation
6
14
 
@@ -12,7 +20,7 @@ gem "philiprehberger-structured_logger"
12
20
 
13
21
  Or install directly:
14
22
 
15
- ```sh
23
+ ```bash
16
24
  gem install philiprehberger-structured_logger
17
25
  ```
18
26
 
@@ -61,35 +69,198 @@ logger.warn("visible") # written
61
69
  logger.error("visible") # written
62
70
  ```
63
71
 
72
+ Read the current level:
73
+
74
+ ```ruby
75
+ logger.level # => :debug
76
+ ```
77
+
64
78
  Change the level at runtime:
65
79
 
66
80
  ```ruby
67
81
  logger.level = :error
68
82
  ```
69
83
 
70
- ### Custom Output
84
+ ### Temporary Context
85
+
86
+ Use `with_context` to add context for the duration of a block:
87
+
88
+ ```ruby
89
+ logger.with_context(request_id: "abc-123") do
90
+ logger.info("Processing request")
91
+ # => {"timestamp":"...","level":"info","message":"Processing request","request_id":"abc-123"}
92
+ end
93
+ # Context is restored after the block
94
+ ```
95
+
96
+ ### Silence
97
+
98
+ Temporarily suppress log output by raising the minimum level:
99
+
100
+ ```ruby
101
+ logger.silence(:fatal) do
102
+ logger.info("suppressed") # not written
103
+ logger.error("suppressed") # not written
104
+ end
105
+ # Level is restored after the block
106
+ ```
107
+
108
+ ### Exception Logging
109
+
110
+ Log exceptions with class, message, and backtrace:
111
+
112
+ ```ruby
113
+ begin
114
+ risky_operation
115
+ rescue => e
116
+ logger.log_exception(e)
117
+ # => {"timestamp":"...","level":"error","message":"something broke","error_class":"RuntimeError","backtrace":[...]}
118
+ end
119
+
120
+ # Custom level and extra context:
121
+ logger.log_exception(e, level: :fatal, user_id: 42)
122
+ ```
123
+
124
+ ### Multiple Outputs
125
+
126
+ Log to multiple destinations simultaneously. Each output can have its own level filter and formatter:
127
+
128
+ ```ruby
129
+ logger = Philiprehberger::StructuredLogger::Logger.new(
130
+ outputs: [$stdout, File.open("app.log", "a")]
131
+ )
132
+
133
+ # With per-output configuration:
134
+ logger = Philiprehberger::StructuredLogger::Logger.new(outputs: [
135
+ { io: $stdout, formatter: :text },
136
+ { io: File.open("app.log", "a"), formatter: :json },
137
+ { io: $stderr, level: :error }
138
+ ])
139
+ ```
140
+
141
+ Add outputs at runtime:
142
+
143
+ ```ruby
144
+ logger.add_output($stderr, level: :error)
145
+ logger.add_output(File.open("debug.log", "a"), formatter: :text)
146
+ ```
147
+
148
+ The singular `output:` parameter still works for backwards compatibility:
149
+
150
+ ```ruby
151
+ logger = Philiprehberger::StructuredLogger::Logger.new(output: $stdout)
152
+ ```
153
+
154
+ ### Custom Formatters
71
155
 
72
- Pass any IO-like object that responds to `puts`:
156
+ Choose from built-in formatters or provide your own:
73
157
 
74
158
  ```ruby
75
- file = File.open("app.log", "a")
76
- logger = Philiprehberger::StructuredLogger::Logger.new(output: file)
159
+ # JSON formatter (default)
160
+ logger = Philiprehberger::StructuredLogger::Logger.new(formatter: :json)
161
+
162
+ # Text formatter — human-readable output
163
+ logger = Philiprehberger::StructuredLogger::Logger.new(formatter: :text)
164
+ logger.info("hello", user: "alice")
165
+ # => [2026-03-10T12:00:00.000Z] INFO: hello user=alice
166
+
167
+ # Custom proc formatter
168
+ logger = Philiprehberger::StructuredLogger::Logger.new(
169
+ formatter: ->(level, message, context) { "#{level.upcase} #{message}" }
170
+ )
171
+
172
+ # Any callable object
173
+ class MyFormatter
174
+ def call(level, message, context)
175
+ "#{level}|#{message}|#{context.to_json}"
176
+ end
177
+ end
178
+
179
+ logger = Philiprehberger::StructuredLogger::Logger.new(formatter: MyFormatter.new)
77
180
  ```
78
181
 
182
+ ### Log Sampling
183
+
184
+ Sample a percentage of logs per level to reduce volume:
185
+
186
+ ```ruby
187
+ logger = Philiprehberger::StructuredLogger::Logger.new(
188
+ sampling: { debug: 0.1, info: 0.5 }
189
+ )
190
+ ```
191
+
192
+ - `1.0` means log everything (default for unspecified levels)
193
+ - `0.5` means log approximately 50%
194
+ - `0.0` means log nothing
195
+
196
+ ### Correlation ID
197
+
198
+ Inject a correlation/request ID into all log entries within a block:
199
+
200
+ ```ruby
201
+ logger.with_correlation_id("req-abc-123") do
202
+ logger.info("processing")
203
+ # => {"timestamp":"...","level":"info","message":"processing","correlation_id":"req-abc-123"}
204
+ end
205
+
206
+ # Auto-generate a UUID:
207
+ logger.with_correlation_id do
208
+ logger.info("processing")
209
+ # => {"timestamp":"...","level":"info","message":"processing","correlation_id":"550e8400-e29b-41d4-a716-446655440000"}
210
+ end
211
+ ```
212
+
213
+ Correlation IDs nest correctly and are stored in Thread-local storage:
214
+
215
+ ```ruby
216
+ logger.with_correlation_id("outer") do
217
+ logger.with_correlation_id("inner") do
218
+ logger.info("nested") # correlation_id: "inner"
219
+ end
220
+ logger.info("back") # correlation_id: "outer"
221
+ end
222
+ ```
223
+
224
+ ### Async Output
225
+
226
+ Enable non-blocking log writes via a background thread:
227
+
228
+ ```ruby
229
+ logger = Philiprehberger::StructuredLogger::Logger.new(async: true, buffer_size: 100)
230
+
231
+ logger.info("non-blocking")
232
+
233
+ # Force immediate write of buffered entries:
234
+ logger.flush
235
+
236
+ # Flush and stop the background thread:
237
+ logger.close
238
+ ```
239
+
240
+ When the buffer is full, writes fall back to synchronous mode (backpressure) to avoid dropping log entries.
241
+
79
242
  ## API
80
243
 
81
244
  ### `Philiprehberger::StructuredLogger::Logger`
82
245
 
83
246
  | Method | Description |
84
247
  |---|---|
85
- | `new(output: $stdout, level: :debug, context: {})` | Create a logger |
248
+ | `new(output: $stdout, outputs: nil, level: :debug, context: {}, formatter: nil, sampling: {}, async: false, buffer_size: 1000)` | Create a logger |
86
249
  | `debug(message, **extra)` | Log at debug level |
87
250
  | `info(message, **extra)` | Log at info level |
88
251
  | `warn(message, **extra)` | Log at warn level |
89
252
  | `error(message, **extra)` | Log at error level |
90
253
  | `fatal(message, **extra)` | Log at fatal level |
91
254
  | `child(**context)` | Create a child logger with merged context |
255
+ | `level` | Get the current log level |
92
256
  | `level=(new_level)` | Set the minimum log level |
257
+ | `with_context(**extra, &block)` | Temporarily merge context for a block |
258
+ | `silence(level = :fatal, &block)` | Temporarily raise log level for a block |
259
+ | `log_exception(exception, level: :error, **extra)` | Log exception details |
260
+ | `add_output(io, level: nil, formatter: nil)` | Add an output destination at runtime |
261
+ | `with_correlation_id(id = nil, &block)` | Set a correlation ID for the block |
262
+ | `flush` | Force write of all buffered log entries |
263
+ | `close` | Flush and stop async background threads |
93
264
 
94
265
  ### `Philiprehberger::StructuredLogger::Formatter`
95
266
 
@@ -97,6 +268,29 @@ logger = Philiprehberger::StructuredLogger::Logger.new(output: file)
97
268
  |---|---|
98
269
  | `call(level, message, context)` | Build a JSON log string |
99
270
 
271
+ ### `Philiprehberger::StructuredLogger::TextFormatter`
272
+
273
+ | Method | Description |
274
+ |---|---|
275
+ | `call(level, message, context)` | Build a human-readable text log string |
276
+
277
+ ### `Philiprehberger::StructuredLogger::AsyncWriter`
278
+
279
+ | Method | Description |
280
+ |---|---|
281
+ | `new(output, buffer_size: 1000)` | Create an async writer wrapping an IO |
282
+ | `puts(line)` | Enqueue a line for async writing |
283
+ | `flush` | Force write of buffered entries |
284
+ | `close` | Flush and stop the background thread |
285
+
286
+ ## Development
287
+
288
+ ```bash
289
+ bundle install
290
+ bundle exec rspec
291
+ bundle exec rubocop
292
+ ```
293
+
100
294
  ## License
101
295
 
102
296
  MIT
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module StructuredLogger
5
+ # Non-blocking log writer that enqueues log lines to a background thread.
6
+ # Falls back to synchronous writes when the buffer is full (backpressure).
7
+ class AsyncWriter
8
+ # @param output [IO] writable output destination
9
+ # @param buffer_size [Integer] maximum number of queued log lines
10
+ def initialize(output, buffer_size: 1000)
11
+ @output = output
12
+ @buffer_size = buffer_size
13
+ @queue = SizedQueue.new(buffer_size)
14
+ @stopped = false
15
+ @closed = false
16
+ @mutex = Mutex.new
17
+ @thread = Thread.new { drain }
18
+ end
19
+
20
+ # Enqueue a log line for asynchronous writing.
21
+ # Falls back to synchronous write if the buffer is full.
22
+ #
23
+ # @param line [String] the formatted log line
24
+ def write(line)
25
+ @mutex.synchronize do
26
+ return sync_write(line) if @stopped
27
+ end
28
+
29
+ begin
30
+ @queue.push(line, true)
31
+ rescue ThreadError
32
+ sync_write(line)
33
+ end
34
+ end
35
+
36
+ # Write a log line using puts.
37
+ #
38
+ # @param line [String] the formatted log line
39
+ def puts(line)
40
+ write(line)
41
+ end
42
+
43
+ # Force all buffered log lines to be written immediately.
44
+ def flush
45
+ Thread.pass until @queue.empty?
46
+ @output.flush if @output.respond_to?(:flush)
47
+ end
48
+
49
+ # Flush remaining log lines and stop the background thread.
50
+ # Safe to call multiple times.
51
+ def close
52
+ @mutex.synchronize do
53
+ return if @closed
54
+
55
+ @stopped = true
56
+ @closed = true
57
+ end
58
+ @queue.push(:stop)
59
+ @thread.join
60
+ @output.flush if @output.respond_to?(:flush)
61
+ end
62
+
63
+ private
64
+
65
+ def drain
66
+ loop do
67
+ line = @queue.pop
68
+ break if line == :stop
69
+
70
+ @output.puts(line)
71
+ end
72
+
73
+ # Drain remaining items after stop signal
74
+ drain_remaining
75
+ end
76
+
77
+ def drain_remaining
78
+ loop do
79
+ line = @queue.pop(true)
80
+ @output.puts(line) unless line == :stop
81
+ rescue ThreadError
82
+ break
83
+ end
84
+ end
85
+
86
+ def sync_write(line)
87
+ @output.puts(line)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -29,5 +29,35 @@ module Philiprehberger
29
29
  }
30
30
  end
31
31
  end
32
+
33
+ # Builds plain-text structured log entries.
34
+ class TextFormatter
35
+ # Format a log entry as a human-readable text string.
36
+ #
37
+ # @param level [Symbol] the log level
38
+ # @param message [String] the log message
39
+ # @param context [Hash] merged context data
40
+ # @return [String] formatted text log line
41
+ def call(level, message, context)
42
+ timestamp = Time.now.utc.iso8601(3)
43
+ parts = ["[#{timestamp}] #{level.to_s.upcase}: #{message}"]
44
+ context.each do |key, value|
45
+ parts << "#{key}=#{value}"
46
+ end
47
+ parts.join(" ")
48
+ end
49
+ end
50
+
51
+ def self.resolve_formatter(formatter)
52
+ case formatter
53
+ when nil, :json then Formatter.new
54
+ when :text then TextFormatter.new
55
+ when Proc then formatter
56
+ else
57
+ raise ArgumentError, "Invalid formatter" unless formatter.respond_to?(:call)
58
+
59
+ formatter
60
+ end
61
+ end
32
62
  end
33
63
  end
@@ -1,40 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "monitor"
4
+ require "securerandom"
4
5
 
5
6
  module Philiprehberger
6
7
  module StructuredLogger
7
- # Thread-safe structured JSON logger with context and child loggers.
8
8
  class Logger
9
9
  LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }.freeze
10
10
 
11
- attr_reader :context
11
+ CORRELATION_ID_KEY = :philiprehberger_structured_logger_correlation_id
12
12
 
13
- # @param output [IO] writable output (default: $stdout)
14
- # @param level [Symbol] minimum log level
15
- # @param context [Hash] base context merged into every entry
16
- def initialize(output: $stdout, level: :debug, context: {})
17
- @output = output
18
- @level = level
19
- @context = context.freeze
20
- @formatter = Formatter.new
13
+ attr_reader :context, :level
14
+
15
+ def initialize(**opts)
16
+ @level = opts.fetch(:level, :debug)
17
+ @context = opts.fetch(:context, {}).freeze
18
+ @sampling = opts.fetch(:sampling, {})
19
+ @async = opts.fetch(:async, false)
20
+ @buffer_size = opts.fetch(:buffer_size, 1000)
21
21
  @monitor = Monitor.new
22
+
23
+ @outputs = OutputBuilder.call(opts, @async, @buffer_size)
22
24
  end
23
25
 
24
- # Set the minimum log level.
25
- #
26
- # @param new_level [Symbol]
27
26
  def level=(new_level)
28
27
  validate_level!(new_level)
29
28
  @level = new_level
30
29
  end
31
30
 
32
- # Create a child logger with additional context.
33
- #
34
- # @param extra [Hash] context to merge
35
- # @return [Logger]
31
+ def add_output(io, level: nil, formatter: nil)
32
+ resolved = StructuredLogger.resolve_formatter(formatter)
33
+ wrapped = @async ? AsyncWriter.new(io, buffer_size: @buffer_size) : io
34
+ @monitor.synchronize { @outputs << { io: wrapped, level: level, formatter: resolved } }
35
+ end
36
+
36
37
  def child(**extra)
37
- self.class.new(output: @output, level: @level, context: @context.merge(extra))
38
+ clone = self.class.allocate
39
+ clone.send(:initialize_child, @outputs, @level, @context.merge(extra), @sampling, @monitor)
40
+ clone
41
+ end
42
+
43
+ def with_context(**extra, &block)
44
+ @monitor.synchronize do
45
+ original = @context
46
+ @context = @context.merge(extra).freeze
47
+ block.call
48
+ ensure
49
+ @context = original
50
+ end
51
+ end
52
+
53
+ def silence(temp_level = :fatal, &block)
54
+ @monitor.synchronize do
55
+ original = @level
56
+ @level = temp_level
57
+ block.call
58
+ ensure
59
+ @level = original
60
+ end
61
+ end
62
+
63
+ def with_correlation_id(id = nil, &block)
64
+ id ||= SecureRandom.uuid
65
+ previous = Thread.current[CORRELATION_ID_KEY]
66
+ Thread.current[CORRELATION_ID_KEY] = id
67
+ block.call
68
+ ensure
69
+ Thread.current[CORRELATION_ID_KEY] = previous
70
+ end
71
+
72
+ def log_exception(exception, level: :error, **extra)
73
+ log(level, exception.message,
74
+ error_class: exception.class.name,
75
+ backtrace: exception.backtrace || [],
76
+ **extra)
77
+ end
78
+
79
+ def flush
80
+ @monitor.synchronize { @outputs.each { |out| out[:io].flush if out[:io].respond_to?(:flush) } }
81
+ end
82
+
83
+ def close
84
+ @monitor.synchronize { @outputs.each { |out| out[:io].close if out[:io].is_a?(AsyncWriter) } }
38
85
  end
39
86
 
40
87
  LEVELS.each_key do |lvl|
@@ -45,22 +92,76 @@ module Philiprehberger
45
92
 
46
93
  private
47
94
 
95
+ def initialize_child(outputs, level, context, sampling, monitor)
96
+ @outputs = outputs
97
+ @level = level
98
+ @context = context.freeze
99
+ @sampling = sampling
100
+ @monitor = monitor
101
+ end
102
+
48
103
  def log(level, message, **extra)
49
- return unless should_log?(level)
104
+ return unless LEVELS.fetch(level) >= LEVELS.fetch(@level)
105
+ return unless sample?(level)
50
106
 
51
107
  merged = @context.merge(extra)
52
- line = @formatter.call(level, message, merged)
53
- @monitor.synchronize { @output.puts(line) }
108
+ cid = Thread.current[CORRELATION_ID_KEY]
109
+ merged = merged.merge(correlation_id: cid) if cid
110
+ @monitor.synchronize { write_to_outputs(level, message, merged) }
111
+ end
112
+
113
+ def write_to_outputs(level, message, merged)
114
+ @outputs.each do |out|
115
+ next if out[:level] && LEVELS.fetch(level) < LEVELS.fetch(out[:level])
116
+
117
+ out[:io].puts(out[:formatter].call(level, message, merged))
118
+ end
54
119
  end
55
120
 
56
- def should_log?(level)
57
- LEVELS.fetch(level) >= LEVELS.fetch(@level)
121
+ def sample?(level)
122
+ rate = @sampling.fetch(level, 1.0)
123
+ rate >= 1.0 || (rate > 0.0 && rand < rate)
58
124
  end
59
125
 
60
126
  def validate_level!(level)
61
- return if LEVELS.key?(level)
127
+ raise ArgumentError, "Invalid level: #{level}" unless LEVELS.key?(level)
128
+ end
129
+ end
130
+
131
+ # Builds output configuration from constructor options.
132
+ module OutputBuilder
133
+ module_function
134
+
135
+ def call(opts, async, buffer_size)
136
+ outputs = opts[:outputs]
137
+ if outputs
138
+ build_multi(outputs, opts[:formatter], async, buffer_size)
139
+ else
140
+ build_single(opts[:output], opts[:formatter], async, buffer_size)
141
+ end
142
+ end
143
+
144
+ def build_multi(outputs, default_formatter, async, buffer_size)
145
+ outputs.map do |out|
146
+ if out.is_a?(Hash)
147
+ build_hash_output(out, async, buffer_size)
148
+ else
149
+ io = async ? AsyncWriter.new(out, buffer_size: buffer_size) : out
150
+ { io: io, level: nil, formatter: StructuredLogger.resolve_formatter(default_formatter) }
151
+ end
152
+ end
153
+ end
154
+
155
+ def build_hash_output(out, async, buffer_size)
156
+ io = async ? AsyncWriter.new(out[:io], buffer_size: buffer_size) : out[:io]
157
+ fmt = StructuredLogger.resolve_formatter(out[:formatter])
158
+ { io: io, level: out[:level], formatter: fmt }
159
+ end
62
160
 
63
- raise ArgumentError, "Invalid level: #{level}. Valid: #{LEVELS.keys.join(', ')}"
161
+ def build_single(output, formatter, async, buffer_size)
162
+ io = output || $stdout
163
+ io = AsyncWriter.new(io, buffer_size: buffer_size) if async
164
+ [{ io: io, level: nil, formatter: StructuredLogger.resolve_formatter(formatter) }]
64
165
  end
65
166
  end
66
167
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module StructuredLogger
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "structured_logger/version"
4
4
  require_relative "structured_logger/formatter"
5
+ require_relative "structured_logger/async_writer"
5
6
  require_relative "structured_logger/logger"
6
7
 
7
8
  module Philiprehberger
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.1.0
4
+ version: 0.3.1
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-03-11 00:00:00.000000000 Z
11
+ date: 2026-03-23 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.
@@ -22,6 +22,7 @@ files:
22
22
  - LICENSE
23
23
  - README.md
24
24
  - lib/philiprehberger/structured_logger.rb
25
+ - lib/philiprehberger/structured_logger/async_writer.rb
25
26
  - lib/philiprehberger/structured_logger/formatter.rb
26
27
  - lib/philiprehberger/structured_logger/logger.rb
27
28
  - lib/philiprehberger/structured_logger/version.rb
@@ -33,6 +34,7 @@ metadata:
33
34
  source_code_uri: https://github.com/philiprehberger/rb-structured-logger
34
35
  changelog_uri: https://github.com/philiprehberger/rb-structured-logger/blob/main/CHANGELOG.md
35
36
  rubygems_mfa_required: 'true'
37
+ bug_tracker_uri: https://github.com/philiprehberger/rb-structured-logger/issues
36
38
  post_install_message:
37
39
  rdoc_options: []
38
40
  require_paths:
@@ -41,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
41
43
  requirements:
42
44
  - - ">="
43
45
  - !ruby/object:Gem::Version
44
- version: '3.1'
46
+ version: 3.1.0
45
47
  required_rubygems_version: !ruby/object:Gem::Requirement
46
48
  requirements:
47
49
  - - ">="