1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
|
# frozen_string_literal: true
require_relative "device_registry"
module Lumberjack
# Abstract base class defining the interface for logging output devices.
# Devices are responsible for the final output of log entries to various
# destinations such as files, streams, databases, or external services.
#
# This class establishes the contract that all concrete device implementations
# must follow, with the +write+ method being the only required implementation.
# Additional lifecycle methods (+close+, +flush+, +reopen+) and configuration
# methods (+datetime_format+) are optional but provide standardized interfaces
# for device management.
#
# The device architecture allows for flexible log output handling while
# maintaining consistent behavior across different output destinations.
# Devices receive formatted LogEntry objects and are responsible for their
# final serialization and delivery.
#
# @abstract Subclass and implement {#write} to create a concrete device
# @see Lumberjack::Device::Writer File-based output device
# @see Lumberjack::Device::LoggerWrapper Ruby Logger compatibility device
# @see Lumberjack::Device::Multi Multiple device routing
# @see Lumberjack::Device::Null Silent device for testing
# @see Lumberjack::Device::Test In-memory device for testing
class Device
require_relative "device/writer"
require_relative "device/log_file"
require_relative "device/logger_wrapper"
require_relative "device/multi"
require_relative "device/null"
require_relative "device/test"
require_relative "device/buffer"
require_relative "device/size_rolling_log_file"
require_relative "device/date_rolling_log_file"
class << self
# Open a logging device with the given options.
#
# @param device [nil, Symbol, String, File, IO, Array, Lumberjack::Device, ContextLogger] The device to open.
# The device can be:
# - +nil+: returns a +Device::Null+ instance that discards all log entries.
# - +Symbol+: looks up the device in the +DeviceRegistry+ and creates a new instance with the provided options.
# - +String+ or +Pathname+: treated as a file path and opens a +Device::LogFile+.
# - +File+: opens a +Device::LogFile+ for the given file stream.
# - +IO+: opens a +Device::Writer+ wrapping the given IO stream.
# - +Lumberjack::Device+: returns the device instance as-is.
# - +ContextLogger+: wraps the logger in a +Device::LoggerWrapper+.
# - +Array+: each element is treated as a device specification and opened recursively,
# returning a +Device::Multi+ that routes log entries to all specified devices. Each
# device can have its own options hash if passed as a two-element array +[device, options]+.
# @param options [Hash] Options to pass to the device constructor.
# @return [Lumberjack::Device] The opened device instance.
#
# @example Open a file-based device
# device = Lumberjack::Device.open_device("/var/log/myapp.log", shift_age: "daily")
#
# @example Open a stream-based device
# device = Lumberjack::Device.open_device($stdout)
#
# @example Open a device from the registry
# device = Lumberjack::Device.open_device(:syslog)
#
# @example Open multiple devices
# device = Lumberjack::Device.open_device([["/var/log/app.log", {shift_age: "daily"}], $stdout])
#
# @example Wrap another logger
# device = Lumberjack::Device.open_device(Lumberjack::Logger.new($stdout))
def open_device(device, options = {})
device = device.to_s if device.is_a?(Pathname)
if device.nil?
Device::Null.new
elsif device.is_a?(Device)
device
elsif device.is_a?(Symbol)
DeviceRegistry.new_device(device, options)
elsif device.is_a?(ContextLogger) || device.is_a?(::Logger)
Device::LoggerWrapper.new(device)
elsif device.is_a?(Array)
devices = device.collect do |dev, dev_options|
dev_options = dev_options.is_a?(Hash) ? options.merge(dev_options) : options
open_device(dev, dev_options)
end
Device::Multi.new(devices)
elsif io_but_not_file_stream?(device)
Device::Writer.new(device, options)
else
Device::LogFile.new(device, options)
end
end
private
def io_but_not_file_stream?(object)
return false if object.is_a?(File)
return false unless object.respond_to?(:write)
return true if object.respond_to?(:tty?) && object.tty?
return false if object.respond_to?(:path) && object.path
true
end
end
# Write a log entry to the device. This is the core method that all device
# implementations must provide. The method receives a fully formatted
# LogEntry object and is responsible for outputting it to the target
# destination.
#
# @param entry [Lumberjack::LogEntry] The log entry to write to the device
# @return [void]
# @abstract Subclasses must implement this method
# @raise [NotImplementedError] If called on the abstract base class
def write(entry)
raise NotImplementedError
end
# Close the device and release any resources. The default implementation
# calls flush to ensure any buffered data is written before closing.
# Subclasses should override this method if they need to perform specific
# cleanup operations such as closing file handles or network connections.
#
# @return [void]
def close
flush
end
# Reopen the device, optionally with a new log destination. The default
# implementation calls flush to ensure data consistency. This method is
# typically used for log rotation scenarios or when changing output
# destinations dynamically.
#
# @param logdev [Object, nil] Optional new log device or destination
# @return [void]
def reopen(logdev = nil)
flush
end
# Flush any buffered data to the output destination. The default
# implementation is a no-op since not all devices use buffering.
# Subclasses that implement buffering should override this method
# to ensure data is written to the final destination.
#
# @return [void]
def flush
end
# Get the current datetime format string used for timestamp formatting.
# The default implementation returns nil, indicating no specific format
# is set. Subclasses may override this to provide device-specific
# timestamp formatting.
#
# @return [String, nil] The datetime format string, or nil if not set
def datetime_format
end
# Set the datetime format string for timestamp formatting. The default
# implementation is a no-op. Subclasses that support configurable
# timestamp formatting should override this method to store and apply
# the specified format.
#
# @param format [String, nil] The datetime format string to use for timestamps
# @return [void]
def datetime_format=(format)
end
# Expose the underlying stream if any.
#
# @return [IO, Lumberjacke::Device, nil]
# @api private
def dev
self
end
end
end
|