lumberjack 1.4.2 → 2.0.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +524 -176
  3. data/CHANGELOG.md +89 -0
  4. data/README.md +604 -211
  5. data/UPGRADE_GUIDE.md +80 -0
  6. data/VERSION +1 -1
  7. data/lib/lumberjack/attribute_formatter.rb +451 -0
  8. data/lib/lumberjack/attributes_helper.rb +100 -0
  9. data/lib/lumberjack/context.rb +120 -23
  10. data/lib/lumberjack/context_logger.rb +620 -0
  11. data/lib/lumberjack/device/buffer.rb +209 -0
  12. data/lib/lumberjack/device/date_rolling_log_file.rb +10 -62
  13. data/lib/lumberjack/device/log_file.rb +76 -29
  14. data/lib/lumberjack/device/logger_wrapper.rb +137 -0
  15. data/lib/lumberjack/device/multi.rb +92 -30
  16. data/lib/lumberjack/device/null.rb +26 -8
  17. data/lib/lumberjack/device/size_rolling_log_file.rb +13 -54
  18. data/lib/lumberjack/device/test.rb +337 -0
  19. data/lib/lumberjack/device/writer.rb +184 -176
  20. data/lib/lumberjack/device.rb +134 -15
  21. data/lib/lumberjack/device_registry.rb +90 -0
  22. data/lib/lumberjack/entry_formatter.rb +357 -0
  23. data/lib/lumberjack/fiber_locals.rb +55 -0
  24. data/lib/lumberjack/forked_logger.rb +143 -0
  25. data/lib/lumberjack/formatter/date_time_formatter.rb +14 -3
  26. data/lib/lumberjack/formatter/exception_formatter.rb +12 -2
  27. data/lib/lumberjack/formatter/id_formatter.rb +13 -1
  28. data/lib/lumberjack/formatter/inspect_formatter.rb +14 -1
  29. data/lib/lumberjack/formatter/multiply_formatter.rb +10 -0
  30. data/lib/lumberjack/formatter/object_formatter.rb +13 -1
  31. data/lib/lumberjack/formatter/pretty_print_formatter.rb +15 -2
  32. data/lib/lumberjack/formatter/redact_formatter.rb +18 -3
  33. data/lib/lumberjack/formatter/round_formatter.rb +12 -0
  34. data/lib/lumberjack/formatter/string_formatter.rb +9 -1
  35. data/lib/lumberjack/formatter/strip_formatter.rb +13 -1
  36. data/lib/lumberjack/formatter/structured_formatter.rb +18 -2
  37. data/lib/lumberjack/formatter/tagged_message.rb +10 -32
  38. data/lib/lumberjack/formatter/tags_formatter.rb +32 -0
  39. data/lib/lumberjack/formatter/truncate_formatter.rb +8 -1
  40. data/lib/lumberjack/formatter.rb +271 -141
  41. data/lib/lumberjack/formatter_registry.rb +84 -0
  42. data/lib/lumberjack/io_compatibility.rb +133 -0
  43. data/lib/lumberjack/local_log_template.rb +209 -0
  44. data/lib/lumberjack/log_entry.rb +154 -79
  45. data/lib/lumberjack/log_entry_matcher/score.rb +276 -0
  46. data/lib/lumberjack/log_entry_matcher.rb +126 -0
  47. data/lib/lumberjack/logger.rb +328 -556
  48. data/lib/lumberjack/message_attributes.rb +38 -0
  49. data/lib/lumberjack/rack/context.rb +66 -15
  50. data/lib/lumberjack/rack.rb +0 -2
  51. data/lib/lumberjack/remap_attribute.rb +24 -0
  52. data/lib/lumberjack/severity.rb +52 -15
  53. data/lib/lumberjack/tag_context.rb +8 -71
  54. data/lib/lumberjack/tag_formatter.rb +22 -188
  55. data/lib/lumberjack/tags.rb +15 -21
  56. data/lib/lumberjack/template.rb +252 -62
  57. data/lib/lumberjack/template_registry.rb +60 -0
  58. data/lib/lumberjack/utils.rb +198 -48
  59. data/lib/lumberjack.rb +167 -59
  60. data/lumberjack.gemspec +4 -2
  61. metadata +41 -15
  62. data/lib/lumberjack/device/rolling_log_file.rb +0 -145
  63. data/lib/lumberjack/rack/request_id.rb +0 -31
  64. data/lib/lumberjack/rack/unit_of_work.rb +0 -21
  65. data/lib/lumberjack/tagged_logger_support.rb +0 -81
  66. data/lib/lumberjack/tagged_logging.rb +0 -29
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # A buffered logging device that wraps another logging device. Entries are buffered in memory
5
+ # until the buffer size is reached or the device is flushed.
6
+ #
7
+ # @example Create a buffered device that flushes every 5 entries
8
+ # device = Lumberjack::Device::Buffer.new(Lumberjack::Device::LogFile.new("logfile.log"), buffer_size: 5)
9
+ #
10
+ # @example Create a buffered device that automatically flushes every 10 seconds
11
+ # device = Lumberjack::Device::Buffer.new("/var/log/app.log", buffer_size: 10, flush_seconds: 10)
12
+ #
13
+ # @example Create a buffered device with a before_flush callback
14
+ # before_flush = -> { puts "Flushing log buffer" }
15
+ # device = Lumberjack::Device::Buffer.new(device, buffer_size: 10, before_flush: before_flush)
16
+ class Device::Buffer < Device
17
+ # Internal class that manages the entry buffer and flushing logic.
18
+ class EntryBuffer
19
+ attr_accessor :size
20
+
21
+ attr_reader :device, :last_flushed_at
22
+
23
+ def initialize(device, size, before_flush)
24
+ @device = device
25
+ @size = size
26
+ @before_flush = before_flush if before_flush.respond_to?(:call)
27
+ @lock = Mutex.new
28
+ @entries = []
29
+ @last_flushed_at = Time.now
30
+ @closed = false
31
+ end
32
+
33
+ def <<(entry)
34
+ return if closed?
35
+
36
+ @lock.synchronize do
37
+ @entries << entry
38
+ end
39
+
40
+ flush if @entries.size >= @size
41
+ end
42
+
43
+ def flush
44
+ entries = nil
45
+
46
+ if closed?
47
+ @before_flush&.call
48
+ entries = @entries
49
+ @entries = []
50
+ else
51
+ @lock.synchronize do
52
+ @before_flush&.call
53
+ entries = @entries
54
+ @entries = []
55
+ end
56
+ end
57
+
58
+ @last_flushed_at = Time.now
59
+
60
+ return if entries.nil?
61
+
62
+ entries.each do |entry|
63
+ @device.write(entry)
64
+ rescue => e
65
+ warn("Error writing log entry from buffer: #{e.inspect}")
66
+ end
67
+ end
68
+
69
+ def close
70
+ @closed = true
71
+ flush
72
+ end
73
+
74
+ def closed?
75
+ @closed
76
+ end
77
+
78
+ def reopen
79
+ @closed = false
80
+ end
81
+
82
+ def empty?
83
+ @entries.empty?
84
+ end
85
+ end
86
+
87
+ class << self
88
+ private
89
+
90
+ def create_finalizer(buffer) # :nodoc:
91
+ lambda { |object_id| buffer.close }
92
+ end
93
+
94
+ def create_flusher_thread(flush_seconds, buffer) # :nodoc:
95
+ Thread.new do
96
+ until buffer.closed?
97
+ sleep(flush_seconds)
98
+ buffer.flush if Time.now - buffer.last_flushed_at >= flush_seconds
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ # Initialize a new buffered logging device that wraps another device.
105
+ #
106
+ # @param wrapped_device [Lumberjack::Device, String, Symbol, IO] The underlying device to wrap.
107
+ # This can be any valid device specification that +Lumberjack::Device.open_device+ accepts.
108
+ # Options not related to buffering will be passed to the underlying device constructor.
109
+ # @param options [Hash] Options for the buffer and the underlying device.
110
+ # @option options [Integer] :buffer_size The number of entries to buffer before flushing. Default is 0 (no buffering).
111
+ # @option options [Integer] :flush_seconds If specified, a background thread will flush the buffer every N seconds.
112
+ # @option options [Proc] :before_flush A callback that will be called before each flush. The callback should
113
+ # respond to +call+ and take no arguments.
114
+ def initialize(wrapped_device, options = {})
115
+ buffer_options = [:buffer_size, :flush_seconds, :before_flush]
116
+ device_options = options.reject { |k, _| buffer_options.include?(k) }
117
+ device = Device.open_device(wrapped_device, device_options)
118
+
119
+ @buffer = EntryBuffer.new(device, options[:buffer_size] || 0, options[:before_flush])
120
+
121
+ flush_seconds = options[:flush_seconds]
122
+ self.class.send(:create_flusher_thread, flush_seconds, @buffer) if flush_seconds.is_a?(Numeric) && flush_seconds > 0
123
+
124
+ # Add a finalizer to ensure flush is called before the object is destroyed
125
+ ObjectSpace.define_finalizer(self, self.class.send(:create_finalizer, @buffer))
126
+ end
127
+
128
+ def buffer_size
129
+ @buffer.size
130
+ end
131
+
132
+ # Set the buffer size. The underlying device will only be written to when the buffer size
133
+ # is exceeded.
134
+ #
135
+ # @param [Integer] value The size of the buffer in bytes.
136
+ # @return [void]
137
+ def buffer_size=(value)
138
+ @buffer.size = value
139
+ @buffer.flush
140
+ end
141
+
142
+ # Write an entry to the underlying device.
143
+ #
144
+ # @param [LogEntry, String] entry The entry to write.
145
+ # @return [void]
146
+ def write(entry)
147
+ @buffer << entry
148
+ end
149
+
150
+ # Close the device.
151
+ #
152
+ # @return [void]
153
+ def close
154
+ @buffer.close
155
+ @buffer.device.close
156
+
157
+ # Remove the finalizer since we've already flushed
158
+ ObjectSpace.undefine_finalizer(self)
159
+ end
160
+
161
+ # Return true if the buffer has been closed.
162
+ def closed?
163
+ @buffer.closed?
164
+ end
165
+
166
+ # Flush the buffer to the underlying device.
167
+ #
168
+ # @return [void]
169
+ def flush
170
+ @buffer.flush
171
+ end
172
+
173
+ # Reopen the underlying device, optionally with a new log destination.
174
+ def reopen(logdev = nil)
175
+ flush
176
+ @buffer.device.reopen(logdev)
177
+ @buffer.reopen
178
+ ObjectSpace.define_finalizer(self, self.class.send(:create_finalizer, @buffer))
179
+ end
180
+
181
+ # Return the underlying stream. Provided for API compatibility with Logger devices.
182
+ #
183
+ # @return [IO] The underlying stream.
184
+ def dev
185
+ @buffer.device.dev
186
+ end
187
+
188
+ # @api private
189
+ def last_flushed_at
190
+ @buffer.last_flushed_at
191
+ end
192
+
193
+ # @api private
194
+ def empty?
195
+ @buffer.empty?
196
+ end
197
+
198
+ private
199
+
200
+ def create_flusher_thread(flush_seconds, buffer) # :nodoc:
201
+ Thread.new do
202
+ until buffer.closed?
203
+ sleep(flush_seconds)
204
+ buffer.flush if Time.now - buffer.last_flushed_at >= flush_seconds
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -3,72 +3,20 @@
3
3
  require "date"
4
4
 
5
5
  module Lumberjack
6
- class Device
7
- # This log device will append entries to a file and roll the file periodically by date. Files
8
- # are rolled at midnight and can be rolled daily, weekly, or monthly. Archive file names will
9
- # have the date appended to them in the format ".YYYY-MM-DD" for daily, ".week-of-YYYY-MM-DD" for weekly
10
- # and ".YYYY-MM" for monthly. It is not guaranteed that log messages will break exactly on the
11
- # roll period as buffered entries will always be written to the same file.
12
- class DateRollingLogFile < RollingLogFile
13
- # Create a new logging device to the specified file. The period to roll the file is specified
14
- # with the :roll option which may contain a value of :daily, :weekly,
15
- # or :monthly.
16
- #
17
- # @param [String, Pathname] path The path to the log file.
18
- # @param [Hash] options The options for the device.
19
- def initialize(path, options = {})
20
- @manual = options[:manual]
21
- @file_date = Date.today
22
- if options[:roll]&.to_s&.match(/(daily)|(weekly)|(monthly)/i)
23
- @roll_period = $~[0].downcase.to_sym
24
- options.delete(:roll)
25
- else
26
- raise ArgumentError.new("illegal value for :roll (#{options[:roll].inspect})")
27
- end
28
- super
29
- end
30
-
31
- # The date based suffix for file.
32
- #
33
- # @return [String]
34
- def archive_file_suffix
35
- case @roll_period
36
- when :weekly
37
- @file_date.strftime("week-of-%Y-%m-%d").to_s
38
- when :monthly
39
- @file_date.strftime("%Y-%m").to_s
40
- else
41
- @file_date.strftime("%Y-%m-%d").to_s
42
- end
43
- end
6
+ # Deprecated device. Use LogFile instead.
7
+ #
8
+ # @deprecated Use Lumberjack::Device::LogFile
9
+ class Device::DateRollingLogFile < Device::LogFile
10
+ def initialize(path, options = {})
11
+ Utils.deprecated("Lumberjack::Device::DateRollingLogFile", "Lumberjack::Device::DateRollingLogFile is deprecated and will be removed in version 2.1; use Lumberjack::Device::LogFile instead.")
44
12
 
45
- # Check if the file should be rolled.
46
- #
47
- # @return [Boolean]
48
- def roll_file?
49
- if @manual
50
- true
51
- else
52
- date = Date.today
53
- if date.year > @file_date.year
54
- true
55
- elsif @roll_period == :daily && date.yday > @file_date.yday
56
- true
57
- elsif @roll_period == :weekly && date.cweek != @file_date.cweek
58
- true
59
- elsif @roll_period == :monthly && date.month > @file_date.month
60
- true
61
- else
62
- false
63
- end
64
- end
13
+ unless options[:roll]&.to_s&.match(/(daily)|(weekly)|(monthly)/i)
14
+ raise ArgumentError.new("illegal value for :roll (#{options[:roll].inspect})")
65
15
  end
66
16
 
67
- protected
17
+ new_options = options.reject { |k, _| k == :roll }.merge(shift_age: options[:roll].to_s.downcase)
68
18
 
69
- def after_roll
70
- @file_date = Date.today
71
- end
19
+ super(path, new_options)
72
20
  end
73
21
  end
74
22
  end
@@ -1,40 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
4
-
5
3
  module Lumberjack
6
- class Device
7
- # This is a logging device that appends log entries to a file.
8
- class LogFile < Writer
9
- EXTERNAL_ENCODING = "ascii-8bit"
4
+ # A file-based logging device that extends the Writer device with automatic
5
+ # log rotation capabilities. This device wraps Ruby's standard Logger::LogDevice
6
+ # to provide file size-based and time-based log rotation while maintaining
7
+ # compatibility with the Lumberjack device interface.
8
+ #
9
+ # The device supports all the rotation features available in Ruby's Logger,
10
+ # including maximum file size limits, automatic rotation based on age, and
11
+ # automatic cleanup of old log files. This makes it suitable for production
12
+ # environments where log management is crucial.
13
+ #
14
+ # @example Basic file logging
15
+ # device = Lumberjack::Device::LogFile.new("/var/log/app.log")
16
+ #
17
+ # @example With size-based rotation (10MB files, keep 5 old files)
18
+ # device = Lumberjack::Device::LogFile.new(
19
+ # "/var/log/app.log",
20
+ # shift_size: 10 * 1024 * 1024, # 10MB
21
+ # shift_age: 5 # Keep 5 old files
22
+ # )
23
+ #
24
+ # @example With daily rotation
25
+ # device = Lumberjack::Device::LogFile.new(
26
+ # "/var/log/app.log",
27
+ # shift_age: "daily"
28
+ # )
29
+ #
30
+ # @example With weekly rotation
31
+ # device = Lumberjack::Device::LogFile.new(
32
+ # "/var/log/app.log",
33
+ # shift_age: "weekly"
34
+ # )
35
+ #
36
+ # @see Device::Writer
37
+ # @see Logger::LogDevice
38
+ class Device::LogFile < Device::Writer
39
+ # Initialize a new LogFile device with automatic log rotation capabilities.
40
+ # This constructor wraps Ruby's Logger::LogDevice while filtering options to
41
+ # only pass supported parameters, ensuring compatibility across Ruby versions.
42
+ #
43
+ # @param stream [String, IO] The log destination. Can be a file path string
44
+ # or an IO object. When a string path is provided, the file will be created
45
+ # if it doesn't exist, and parent directories will be created as needed.
46
+ # @param options [Hash] Configuration options for the log device. All options
47
+ # supported by Logger::LogDevice are accepted, including:
48
+ # - +:shift_age+ - Number of old files to keep, or rotation frequency
49
+ # ("daily", "weekly", "monthly")
50
+ # - +:shift_size+ - Maximum file size in bytes before rotation
51
+ # - +:shift_period_suffix+ - Suffix to add to rotated log files
52
+ # - +:binmode+ - Whether to open the log file in binary mode
53
+ def initialize(stream, options = {})
54
+ # Filter options to only include keyword arguments supported by Logger::LogDevice#initialize
55
+ supported_kwargs = ::Logger::LogDevice.instance_method(:initialize).parameters
56
+ .select { |type, _| type == :key || type == :keyreq }
57
+ .map { |_, name| name }
10
58
 
11
- # The absolute path of the file being logged to.
12
- attr_reader :path
59
+ filtered_options = options.slice(*supported_kwargs)
13
60
 
14
- # Create a logger to the file at +path+. Options are passed through to the Writer constructor.
15
- #
16
- # @param [String, Pathname] path The path to the log file.
17
- # @param [Hash] options The options for the device.
18
- def initialize(path, options = {})
19
- @path = File.expand_path(path)
20
- FileUtils.mkdir_p(File.dirname(@path))
21
- super(file_stream, options)
22
- end
61
+ logdev = ::Logger::LogDevice.new(stream, **filtered_options)
23
62
 
24
- # Reopen the log file.
25
- #
26
- # @param [Object] logdev not used
27
- # @return [void]
28
- def reopen(logdev = nil)
29
- close
30
- @stream = file_stream
31
- end
63
+ super(logdev, options)
64
+ end
32
65
 
33
- private
66
+ # Get the file system path of the current log file. This method provides
67
+ # access to the actual file path being written to, which is useful for
68
+ # monitoring, log analysis tools, or other file-based operations.
69
+ #
70
+ # @return [String] The absolute file system path of the current log file
71
+ def path
72
+ stream.filename
73
+ end
74
+
75
+ # Expose the underlying stream.
76
+ #
77
+ # @return [IO]
78
+ # @api private
79
+ def dev
80
+ stream.dev
81
+ end
34
82
 
35
- def file_stream
36
- File.new(@path, "a", encoding: EXTERNAL_ENCODING)
37
- end
83
+ def reopen(logdev = nil)
84
+ stream.reopen(logdev)
38
85
  end
39
86
  end
40
87
  end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # A logging device that forwards log entries to another logger instance.
5
+ # This device enables hierarchical logging architectures and broadcasting scenarios
6
+ # where log entries need to be distributed to multiple loggers or processed through
7
+ # different logging pipelines.
8
+ #
9
+ # The device is particularly useful when combined with Device::Multi to create
10
+ # master loggers that can simultaneously write to multiple destinations (files,
11
+ # databases, external services) while maintaining consistent formatting and
12
+ # attribute handling across all targets.
13
+ #
14
+ # Unlike other devices that write directly to output streams, this device delegates
15
+ # to another logger's processing pipeline, allowing for complex logging topologies
16
+ # and reuse of existing logger configurations.
17
+ #
18
+ # @example Basic logger forwarding
19
+ # file_logger = Lumberjack::Logger.new("/var/log/app.log")
20
+ # logger_device = Lumberjack::Device::LoggerWrapper.new(file_logger)
21
+ #
22
+ # @example Broadcasting with Multi device
23
+ # main_logger = Lumberjack::Logger.new("/var/log/main.log")
24
+ # error_logger = Lumberjack::Logger.new("/var/log/errors.log")
25
+ #
26
+ # broadcast_device = Lumberjack::Device::Multi.new([
27
+ # Lumberjack::Device::LoggerWrapper.new(main_logger),
28
+ # Lumberjack::Device::LoggerWrapper.new(error_logger)
29
+ # ])
30
+ #
31
+ # master_logger = Lumberjack::Logger.new(broadcast_device)
32
+ #
33
+ # @example Hierarchical logging with filtering
34
+ # # Main application logger
35
+ # app_logger = Lumberjack::Logger.new("/var/log/app.log")
36
+ #
37
+ # # Focused error logs
38
+ # error_logger = Lumberjack::Logger.new("/var/log/error.log")
39
+ # error_logger.level = Logger::WARN
40
+ #
41
+ # # Route all logs to app, error logs to error logger
42
+ # multi_device = Lumberjack::Device::Multi.new([
43
+ # Lumberjack::Device::LoggerWrapper.new(app_logger),
44
+ # Lumberjack::Device::LoggerWrapper.new(error_logger)
45
+ # ])
46
+ #
47
+ # @see Device::Multi
48
+ # @see Lumberjack::Logger
49
+ # @see Lumberjack::ContextLogger
50
+ class Device::LoggerWrapper < Device
51
+ # @!attribute [r] logger
52
+ # @return [Lumberjack::ContextLogger] The target logger that will receive forwarded log entries
53
+ attr_reader :logger
54
+
55
+ # Initialize a new Logger device that forwards entries to the specified logger.
56
+ # The target logger must be a Lumberjack logger that supports the ContextLogger
57
+ # interface to ensure proper entry handling and attribute processing.
58
+ #
59
+ # @param logger [Lumberjack::ContextLogger, ::Logger] The target logger to receive forwarded entries.
60
+ # Must be a Lumberjack logger instance (Logger, ForkedLogger, etc.) that includes
61
+ # the ContextLogger mixin for proper entry processing.
62
+ #
63
+ # @raise [ArgumentError] If the provided logger is not a Lumberjack::ContextLogger
64
+ def initialize(logger)
65
+ unless logger.is_a?(Lumberjack::ContextLogger) || logger.is_a?(::Logger)
66
+ raise ArgumentError.new("Logger must be a Lumberjack logger")
67
+ end
68
+
69
+ @logger = logger
70
+ end
71
+
72
+ # Forward a log entry to the target logger for processing. This method extracts
73
+ # the entry components and delegates to the target logger's add_entry method,
74
+ # ensuring that all attributes, formatting, and processing logic of the target
75
+ # logger are properly applied.
76
+ #
77
+ # The forwarded entry maintains all original metadata including severity,
78
+ # timestamp, program name, and custom attributes, allowing the target logger
79
+ # to process it as if it were generated directly.
80
+ #
81
+ # @param entry [Lumberjack::LogEntry] The log entry to forward to the target logger
82
+ # @return [void]
83
+ def write(entry)
84
+ if @logger.is_a?(Lumberjack::ContextLogger)
85
+ @logger.add_entry(entry.severity, entry.message, entry.progname, entry.attributes)
86
+ else
87
+ message = entry.message
88
+ if entry.attributes && !entry.attributes.empty?
89
+ message_attributes = []
90
+ entry.attributes.each do |key, value|
91
+ next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
92
+
93
+ value = value.join(",") if value.is_a?(Enumerable)
94
+ message_attributes << "[#{key}=#{value}]"
95
+ end
96
+ message = "#{message} #{message_attributes.join(" ")}" unless message_attributes.empty?
97
+ end
98
+ @logger.add(entry.severity, message, entry.progname)
99
+ end
100
+ end
101
+
102
+ # Closes the target logger to release any resources or finalize log output.
103
+ # This method delegates to the target logger's +close+ method.
104
+ #
105
+ # @return [void]
106
+ def close
107
+ @logger.close
108
+ end
109
+
110
+ # Reopen the underlying logger device. This is typically used to reopen log files
111
+ # after log rotation or to refresh the logger's output stream.
112
+ #
113
+ # Delegates to the target logger's +reopen+ method.
114
+ #
115
+ # @return [void]
116
+ def reopen
117
+ @logger.reopen
118
+ end
119
+
120
+ # Flushes the target logger, ensuring that any buffered log entries are written out.
121
+ # This delegates to the target logger's flush method, which may flush buffers to disk,
122
+ # external services, or other destinations depending on the logger's configuration.
123
+ #
124
+ # @return [void]
125
+ def flush
126
+ @logger.flush
127
+ end
128
+
129
+ # Expose the underlying stream if any.
130
+ #
131
+ # @return [IO, Lumberjack::Device, nil]
132
+ # @api private
133
+ def dev
134
+ @logger.device&.dev
135
+ end
136
+ end
137
+ end
@@ -1,46 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lumberjack
4
- class Device
5
- # This is a logging device that forward log entries to multiple other devices.
6
- class Multi < Device
7
- # @param [Array<Lumberjack::Device>] devices The devices to write to.
8
- def initialize(*devices)
9
- @devices = devices.flatten
10
- end
4
+ # A multiplexing logging device that broadcasts log entries to multiple target
5
+ # devices simultaneously. This device enables sophisticated logging architectures
6
+ # where a single log entry needs to be processed by multiple output destinations,
7
+ # each potentially with different formatting, filtering, or storage mechanisms.
8
+ #
9
+ # The Multi device acts as a fan-out mechanism, ensuring that all configured
10
+ # devices receive every log entry while maintaining independent processing
11
+ # pipelines. This is particularly useful for creating redundant logging systems,
12
+ # separating log streams by concern, or implementing complex routing logic.
13
+ #
14
+ # All device lifecycle methods (flush, close, reopen) are propagated to all
15
+ # child devices, ensuring consistent state management across the entire
16
+ # logging topology.
17
+ #
18
+ # @example Basic multi-device setup
19
+ # file_device = Lumberjack::Device::Writer.new("/var/log/app.log")
20
+ # console_device = Lumberjack::Device::Writer.new(STDOUT, template: "{{message}}")
21
+ # multi_device = Lumberjack::Device::Multi.new(file_device, console_device)
22
+ class Device::Multi < Device
23
+ attr_reader :devices
11
24
 
12
- def write(entry)
13
- @devices.each do |device|
14
- device.write(entry)
15
- end
16
- end
25
+ # Initialize a new Multi device with the specified target devices. The device
26
+ # accepts multiple devices either as individual arguments or as arrays,
27
+ # automatically flattening nested arrays for convenient configuration.
28
+ #
29
+ # @param devices [Array<Lumberjack::Device>] The target devices to receive
30
+ # log entries. Can be passed as individual arguments or arrays. All devices
31
+ # must implement the standard Lumberjack::Device interface.
32
+ #
33
+ # @example Individual device arguments
34
+ # multi = Multi.new(file_device, console_device, database_device)
35
+ def initialize(*devices)
36
+ @devices = devices.flatten
37
+ end
17
38
 
18
- def flush
19
- @devices.each do |device|
20
- device.flush
21
- end
39
+ # Broadcast a log entry to all configured devices. Each device receives the
40
+ # same LogEntry object and processes it according to its own configuration,
41
+ # formatting, and output logic. Devices are processed sequentially in the
42
+ # order they were configured.
43
+ #
44
+ # @param entry [Lumberjack::LogEntry] The log entry to broadcast to all devices
45
+ # @return [void]
46
+ def write(entry)
47
+ devices.each do |device|
48
+ device.write(entry)
22
49
  end
50
+ end
23
51
 
24
- def close
25
- @devices.each do |device|
26
- device.close
27
- end
52
+ # Flush all configured devices to ensure buffered data is written to their
53
+ # respective destinations. This method calls flush on each device in sequence,
54
+ # ensuring consistent state across all output destinations.
55
+ #
56
+ # @return [void]
57
+ def flush
58
+ devices.each do |device|
59
+ device.flush
28
60
  end
61
+ end
29
62
 
30
- def reopen(logdev = nil)
31
- @devices.each do |device|
32
- device.reopen(logdev = nil)
33
- end
63
+ # Close all configured devices and release their resources. This method calls
64
+ # close on each device in sequence, ensuring proper cleanup of file handles,
65
+ # network connections, and other resources across all output destinations.
66
+ #
67
+ # @return [void]
68
+ def close
69
+ devices.each do |device|
70
+ device.close
34
71
  end
72
+ end
35
73
 
36
- def datetime_format
37
- @devices.detect(&:datetime_format).datetime_format
74
+ # Reopen all configured devices, optionally with a new log destination.
75
+ # This method calls reopen on each device in sequence, which is typically
76
+ # used for log rotation scenarios or when changing output destinations.
77
+ #
78
+ # @param logdev [Object, nil] Optional new log device or destination to pass
79
+ # to each device's reopen method
80
+ # @return [void]
81
+ def reopen(logdev = nil)
82
+ devices.each do |device|
83
+ device.reopen(logdev = nil)
38
84
  end
85
+ end
86
+
87
+ # Get the datetime format from the first device that has one configured.
88
+ # This method searches through the configured devices and returns the
89
+ # datetime format from the first device that provides one.
90
+ #
91
+ # @return [String, nil] The datetime format string from the first device
92
+ # that has one configured, or nil if no devices have a format set
93
+ def datetime_format
94
+ devices.detect(&:datetime_format).datetime_format
95
+ end
39
96
 
40
- def datetime_format=(format)
41
- @devices.each do |device|
42
- device.datetime_format = format
43
- end
97
+ # Set the datetime format on all configured devices that support it.
98
+ # This method propagates the format setting to each device, allowing
99
+ # coordinated timestamp formatting across all output destinations.
100
+ #
101
+ # @param format [String] The datetime format string to apply to all devices
102
+ # @return [void]
103
+ def datetime_format=(format)
104
+ devices.each do |device|
105
+ device.datetime_format = format
44
106
  end
45
107
  end
46
108
  end