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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +524 -176
- data/CHANGELOG.md +89 -0
- data/README.md +604 -211
- data/UPGRADE_GUIDE.md +80 -0
- data/VERSION +1 -1
- data/lib/lumberjack/attribute_formatter.rb +451 -0
- data/lib/lumberjack/attributes_helper.rb +100 -0
- data/lib/lumberjack/context.rb +120 -23
- data/lib/lumberjack/context_logger.rb +620 -0
- data/lib/lumberjack/device/buffer.rb +209 -0
- data/lib/lumberjack/device/date_rolling_log_file.rb +10 -62
- data/lib/lumberjack/device/log_file.rb +76 -29
- data/lib/lumberjack/device/logger_wrapper.rb +137 -0
- data/lib/lumberjack/device/multi.rb +92 -30
- data/lib/lumberjack/device/null.rb +26 -8
- data/lib/lumberjack/device/size_rolling_log_file.rb +13 -54
- data/lib/lumberjack/device/test.rb +337 -0
- data/lib/lumberjack/device/writer.rb +184 -176
- data/lib/lumberjack/device.rb +134 -15
- data/lib/lumberjack/device_registry.rb +90 -0
- data/lib/lumberjack/entry_formatter.rb +357 -0
- data/lib/lumberjack/fiber_locals.rb +55 -0
- data/lib/lumberjack/forked_logger.rb +143 -0
- data/lib/lumberjack/formatter/date_time_formatter.rb +14 -3
- data/lib/lumberjack/formatter/exception_formatter.rb +12 -2
- data/lib/lumberjack/formatter/id_formatter.rb +13 -1
- data/lib/lumberjack/formatter/inspect_formatter.rb +14 -1
- data/lib/lumberjack/formatter/multiply_formatter.rb +10 -0
- data/lib/lumberjack/formatter/object_formatter.rb +13 -1
- data/lib/lumberjack/formatter/pretty_print_formatter.rb +15 -2
- data/lib/lumberjack/formatter/redact_formatter.rb +18 -3
- data/lib/lumberjack/formatter/round_formatter.rb +12 -0
- data/lib/lumberjack/formatter/string_formatter.rb +9 -1
- data/lib/lumberjack/formatter/strip_formatter.rb +13 -1
- data/lib/lumberjack/formatter/structured_formatter.rb +18 -2
- data/lib/lumberjack/formatter/tagged_message.rb +10 -32
- data/lib/lumberjack/formatter/tags_formatter.rb +32 -0
- data/lib/lumberjack/formatter/truncate_formatter.rb +8 -1
- data/lib/lumberjack/formatter.rb +271 -141
- data/lib/lumberjack/formatter_registry.rb +84 -0
- data/lib/lumberjack/io_compatibility.rb +133 -0
- data/lib/lumberjack/local_log_template.rb +209 -0
- data/lib/lumberjack/log_entry.rb +154 -79
- data/lib/lumberjack/log_entry_matcher/score.rb +276 -0
- data/lib/lumberjack/log_entry_matcher.rb +126 -0
- data/lib/lumberjack/logger.rb +328 -556
- data/lib/lumberjack/message_attributes.rb +38 -0
- data/lib/lumberjack/rack/context.rb +66 -15
- data/lib/lumberjack/rack.rb +0 -2
- data/lib/lumberjack/remap_attribute.rb +24 -0
- data/lib/lumberjack/severity.rb +52 -15
- data/lib/lumberjack/tag_context.rb +8 -71
- data/lib/lumberjack/tag_formatter.rb +22 -188
- data/lib/lumberjack/tags.rb +15 -21
- data/lib/lumberjack/template.rb +252 -62
- data/lib/lumberjack/template_registry.rb +60 -0
- data/lib/lumberjack/utils.rb +198 -48
- data/lib/lumberjack.rb +167 -59
- data/lumberjack.gemspec +4 -2
- metadata +41 -15
- data/lib/lumberjack/device/rolling_log_file.rb +0 -145
- data/lib/lumberjack/rack/request_id.rb +0 -31
- data/lib/lumberjack/rack/unit_of_work.rb +0 -21
- data/lib/lumberjack/tagged_logger_support.rb +0 -81
- 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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
17
|
+
new_options = options.reject { |k, _| k == :roll }.merge(shift_age: options[:roll].to_s.downcase)
|
68
18
|
|
69
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
12
|
-
attr_reader :path
|
59
|
+
filtered_options = options.slice(*supported_kwargs)
|
13
60
|
|
14
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|