log_shout 0.1.0 → 0.1.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: cc4cc267c8eeb16ef1f1615f57a50189549a896c2d583eb74ab0540857812146
4
- data.tar.gz: 487bb3371dbf862cb701e1e5278959d722765f424c3f70832cffdef30eb577dc
3
+ metadata.gz: 1ea94d24d14b119d2a45952bd943d3c4fc0f19bbe3297c3f77041e9e2ad95805
4
+ data.tar.gz: 39f4263faad4e590ac3ff444e2edf2a61d7ca55aeb2e6460b123758249ccbc5c
5
5
  SHA512:
6
- metadata.gz: e85575c4a8cb47cabd352d1104326f07fb3c15f7ec486ae64de5b61cb75182235a3889a671b598a510aa5ed9f4c7a44df5cd062a743e86c0b5cefdbc46b71ae8
7
- data.tar.gz: b45d29c88b91d0bacae4f55ce5864dd630847a544e680b6319bbf6b37c4a5c71c06a3ab412f3c4baefe61cf80c4e60486baad3455fd72319769158accb64abca
6
+ metadata.gz: aa7024d62547b37076b0f38eb355631fbcfc7c74d9e201b4392944dc7947277df192634f94c8d56938a129b9b68cd3116ce3fd3d3a33c9fc8990b5450c49130d
7
+ data.tar.gz: 0fcd3d043f3f33d978a9bf7cf9746a65a087954c4598f0341e6a4f464fdb6bdb211611e0342cb2eed78b156f4b64faceabf8277a550e8a089de4993a94836ac7
data/CHANGELOG.md CHANGED
@@ -1,8 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.0 (unreleased)
3
+ ## 0.1.1 (2026-10-24)
4
4
 
5
- Initial release candidate:
5
+ - Continue delivering messages to healthy destinations when another destination fails.
6
+ - Attempt every destination during flush and shutdown; report multiple failures through
7
+ `LogShout::HandlerErrors#errors`, preserving the original exception for a single failure.
8
+ - Recover from failed workers when installing a replacement configuration, reporting
9
+ every old worker failure to stderr.
10
+ - Initialize configuration when starting a logging job after `reset!`.
11
+ - Accept symbol names when starting logging Ractors.
12
+ - Report invalid configuration handler and logger names as `LogShoutError`.
13
+
14
+ ## 0.1.0 (2026-10-24)
15
+
16
+ Initial release:
6
17
 
7
18
  - Fan out Ruby Logger messages to stdout, stderr, files, and memory through Ractors.
8
19
  - Configure named loggers and shared destinations with YAML.
data/README.md CHANGED
@@ -151,6 +151,12 @@ currently no backpressure, rotation, retry, or delivery guarantee after failure.
151
151
  Destinations are opened during configuration. If opening or writing fails,
152
152
  `Ractor::RemoteError` is raised by initialization or the next synchronous
153
153
  `flush`, `read`, or `close`; its `cause` contains the underlying IO exception.
154
+ When several destinations fail during a write, flush, or shutdown,
155
+ `LogShout::HandlerErrors` contains every original exception in its `errors`
156
+ array. Its message includes every failure and its underlying cause. A single
157
+ failure still raises the original exception directly. Ruby Logger may catch
158
+ write exceptions and print their messages; call `LogShout.flush` explicitly
159
+ to handle worker failures in application code.
154
160
  A failed worker wakes waiting callers. A live worker blocked in operating-system
155
161
  IO can still block a flush or shutdown; no IO timeout is imposed. Ruby Logger
156
162
  may report immediate write errors to stderr instead of propagating them, so use
@@ -165,9 +171,13 @@ application exception. For reliable delivery, explicitly stop producers and call
165
171
  `LogShout.configure(path: ...)` validates and opens the replacement destinations,
166
172
  drains and closes the previous handlers, and clears the logger cache. Invalid
167
173
  configuration or an opening failure preserves the current configuration.
174
+ If an old worker has failed, reconfiguration reports its error to stderr and
175
+ installs the healthy replacement configuration. Writes and flushes attempt every
176
+ destination before reporting all failures, so a failed destination does
177
+ not prevent delivery to the remaining destinations.
168
178
  `LogShout.reset!` closes handlers and clears configuration; the next `get_logger`
169
- loads configuration again. Stop and join background jobs before reconfiguring,
170
- resetting, or closing. Discard old logger references and fetch fresh ones after
179
+ or `LoggingRactor.start` loads configuration again. Stop and join background jobs
180
+ before reconfiguring, resetting, or closing. Discard old logger references and fetch fresh ones after
171
181
  reconfiguration. Configuration changes are intended for the main Ractor while
172
182
  producers are stopped; concurrent reconfiguration is unsupported.
173
183
 
@@ -8,7 +8,7 @@ module LogShout
8
8
  handlers = LogShout.get_handlers_for(name).freeze
9
9
  level = ::Logger.const_get(LogShout.get_level_for_name(name).upcase)
10
10
  logdev = LogShout::MultiIO.new(handlers)
11
- r = Ractor.new(level, logdev, args, name: name) do |level, logdev, a|
11
+ r = Ractor.new(level, logdev, args, name: name.to_s) do |level, logdev, a|
12
12
  logger = ::Logger.new(logdev,
13
13
  level: level,
14
14
  progname: Ractor.current.name,
@@ -7,7 +7,7 @@ module LogShout
7
7
  end
8
8
 
9
9
  def write(message)
10
- @targets.each { |t| t.write(message) }
10
+ each_target { |target| target.write(message) }
11
11
  nil
12
12
  end
13
13
 
@@ -18,8 +18,8 @@ module LogShout
18
18
  end
19
19
 
20
20
  def flush(wait_for_flush = true)
21
- @targets.each do |t|
22
- t.flush(wait_for_flush)
21
+ each_target do |target|
22
+ target.flush(wait_for_flush)
23
23
  end
24
24
  nil
25
25
  end
@@ -32,5 +32,18 @@ module LogShout
32
32
  end
33
33
  nil
34
34
  end
35
+
36
+ private
37
+
38
+ # Report failures after giving every destination a chance to process the call.
39
+ def each_target
40
+ errors = []
41
+ @targets.each do |target|
42
+ yield target
43
+ rescue => e
44
+ errors << e
45
+ end
46
+ HandlerErrors.raise_if_any(errors)
47
+ end
35
48
  end
36
49
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogShout
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/lib/log_shout.rb CHANGED
@@ -15,6 +15,25 @@ require "log_shout/formatters/default"
15
15
  module LogShout
16
16
  class LogShoutError < StandardError; end
17
17
 
18
+ class HandlerErrors < LogShoutError
19
+ attr_reader :errors
20
+
21
+ def initialize(errors)
22
+ @errors = errors.flat_map { |error| error.is_a?(HandlerErrors) ? error.errors : [error] }.freeze
23
+ details = @errors.map do |error|
24
+ detail = "#{error.class}: #{error.message}"
25
+ detail += " (#{error.cause.class}: #{error.cause.message})" if error.cause
26
+ detail
27
+ end
28
+ super("#{@errors.length} handler failures: #{details.join("; ")}")
29
+ end
30
+
31
+ def self.raise_if_any(errors)
32
+ raise errors.first if errors.length == 1
33
+ raise new(errors) unless errors.empty?
34
+ end
35
+ end
36
+
18
37
  # Replace the Sorbet enum with a plain Ruby value-object enum
19
38
  class LogDestination
20
39
  attr_reader :name
@@ -144,9 +163,17 @@ module LogShout
144
163
  handlers = build_handlers(validated_config)
145
164
  begin
146
165
  close_handlers
147
- rescue
148
- handlers.each_value(&:close)
149
- raise
166
+ rescue => e
167
+ errors = e.is_a?(HandlerErrors) ? e.errors : [e]
168
+ if errors.all? { |error| error.is_a?(Ractor::RemoteError) }
169
+ # Every old destination has been closed; report each failed worker.
170
+ errors.each do |error|
171
+ warn "LogShout replaced failed handlers: #{error.cause.class}: #{error.cause.message}"
172
+ end
173
+ else
174
+ handlers.each_value(&:close)
175
+ raise
176
+ end
150
177
  end
151
178
  @config_file_path = config_file_path
152
179
  @validated_config = validated_config
@@ -183,6 +210,7 @@ module LogShout
183
210
  default_handler_names = config.fetch(:defaultHandlers, ["stdout"])
184
211
  raise LogShoutError, "defaultHandlers must be an array" unless default_handler_names.is_a?(Array)
185
212
  extra_handlers = config.fetch(:extraHandlers, {}).each_with_object({}) do |(name, config), obj|
213
+ name = configuration_name(name, "handler")
186
214
  if name === :stdout || name === :stderr
187
215
  raise LogShoutError, "Cannot override built-in handler: #{name}"
188
216
  end
@@ -205,17 +233,20 @@ module LogShout
205
233
  obj[name.to_sym] = handler
206
234
  end
207
235
  default_handlers = default_handler_names.map do |name|
208
- BUILTIN_HANDLERS[name.to_sym] || extra_handlers[name.to_sym] || (raise LogShoutError, "Unknown handler: #{name}")
236
+ name = configuration_name(name, "handler")
237
+ BUILTIN_HANDLERS[name] || extra_handlers[name] || (raise LogShoutError, "Unknown handler: #{name}")
209
238
  end
210
239
 
211
240
  loggers = config.fetch(:loggers, {}).each_with_object({}) do |(name, config), obj|
241
+ name = configuration_name(name, "logger")
212
242
  raise LogShoutError, "Logger #{name} must be a mapping" unless config.is_a?(Hash)
213
243
  level = config.fetch(:level, default_level)
214
244
  validate_level(level)
215
245
  handler_names = config.fetch(:handlers, default_handler_names)
216
246
  raise LogShoutError, "Logger #{name} handlers must be an array" unless handler_names.is_a?(Array)
217
247
  handlers = handler_names.map do |name|
218
- BUILTIN_HANDLERS[name.to_sym] || extra_handlers[name.to_sym] || (raise LogShoutError, "Unknown handler: #{name}")
248
+ name = configuration_name(name, "handler")
249
+ BUILTIN_HANDLERS[name] || extra_handlers[name] || (raise LogShoutError, "Unknown handler: #{name}")
219
250
  end
220
251
  obj[name.to_sym] = LogConfig.new(level: level, handlers: handlers)
221
252
  end
@@ -235,11 +266,20 @@ module LogShout
235
266
  end
236
267
  private_class_method :validate_level
237
268
 
269
+ def self.configuration_name(name, kind)
270
+ unless name.is_a?(String) || name.is_a?(Symbol)
271
+ raise LogShoutError, "Invalid #{kind} name: #{name.inspect}; expected a string or symbol"
272
+ end
273
+ name.to_sym
274
+ end
275
+ private_class_method :configuration_name
276
+
238
277
  def self.get_handler(name)
239
278
  @handlers[name]
240
279
  end
241
280
 
242
281
  def self.get_handlers_for(logger_name)
282
+ configure unless @handlers
243
283
  config = configuration[:loggers].fetch(
244
284
  logger_name.to_sym,
245
285
  {
@@ -254,19 +294,17 @@ module LogShout
254
294
 
255
295
  def self.flush
256
296
  return unless @handlers
257
- @handlers.each_value do |handler|
258
- handler.flush
259
- end
297
+ MultiIO.new(@handlers.values).flush
260
298
  end
261
299
 
262
300
  def self.close_handlers
263
- error = nil
301
+ errors = []
264
302
  (@handlers || {}).each_value do |handler|
265
303
  handler.close
266
304
  rescue => e
267
- error ||= e
305
+ errors << e
268
306
  end
269
- raise error if error
307
+ HandlerErrors.raise_if_any(errors)
270
308
  nil
271
309
  end
272
310
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: log_shout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Christen