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 +4 -4
- data/CHANGELOG.md +13 -2
- data/README.md +12 -2
- data/lib/log_shout/logging_ractor.rb +1 -1
- data/lib/log_shout/multi_io.rb +16 -3
- data/lib/log_shout/version.rb +1 -1
- data/lib/log_shout.rb +49 -11
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ea94d24d14b119d2a45952bd943d3c4fc0f19bbe3297c3f77041e9e2ad95805
|
|
4
|
+
data.tar.gz: 39f4263faad4e590ac3ff444e2edf2a61d7ca55aeb2e6460b123758249ccbc5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
3
|
+
## 0.1.1 (2026-10-24)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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,
|
data/lib/log_shout/multi_io.rb
CHANGED
|
@@ -7,7 +7,7 @@ module LogShout
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def write(message)
|
|
10
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
data/lib/log_shout/version.rb
CHANGED
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
258
|
-
handler.flush
|
|
259
|
-
end
|
|
297
|
+
MultiIO.new(@handlers.values).flush
|
|
260
298
|
end
|
|
261
299
|
|
|
262
300
|
def self.close_handlers
|
|
263
|
-
|
|
301
|
+
errors = []
|
|
264
302
|
(@handlers || {}).each_value do |handler|
|
|
265
303
|
handler.close
|
|
266
304
|
rescue => e
|
|
267
|
-
|
|
305
|
+
errors << e
|
|
268
306
|
end
|
|
269
|
-
|
|
307
|
+
HandlerErrors.raise_if_any(errors)
|
|
270
308
|
nil
|
|
271
309
|
end
|
|
272
310
|
|