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
@@ -1,206 +1,214 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lumberjack
4
- class Device
5
- # This logging device writes log entries as strings to an IO stream. By default, messages will be buffered
6
- # and written to the stream in a batch when the buffer is full or when +flush+ is called.
7
- #
8
- # Subclasses can implement a +before_flush+ method if they have logic to execute before flushing the log.
9
- # If it is implemented, it will be called before every flush inside a mutex lock.
10
- class Writer < Device
11
- DEFAULT_FIRST_LINE_TEMPLATE = "[:time :severity :progname(:pid) #:unit_of_work_id] :message :tags"
12
- DEFAULT_ADDITIONAL_LINES_TEMPLATE = "#{Lumberjack::LINE_SEPARATOR}> [#:unit_of_work_id] :message"
13
-
14
- # The size of the internal buffer. Defaults to 32K.
15
- attr_reader :buffer_size
16
-
17
- # Internal buffer to batch writes to the stream.
18
- class Buffer # :nodoc:
19
- attr_reader :size
20
-
21
- def initialize
22
- @values = []
23
- @size = 0
24
- end
4
+ # A versatile logging device that writes formatted log entries to IO streams.
5
+ # This device serves as the foundation for most output-based logging, converting
6
+ # LogEntry objects into formatted strings using configurable templates and
7
+ # writing them to any IO-compatible stream.
8
+ #
9
+ # The Writer device supports extensive customization through templates, encoding
10
+ # options, stream management, and error handling. It can write to files, console
11
+ # output, network streams, or any object that implements the IO interface.
12
+ #
13
+ # Templates can be either string-based (compiled into Template objects) or
14
+ # callable objects (Procs, lambdas) for maximum flexibility. The device handles
15
+ # character encoding, whitespace normalization, and provides robust error
16
+ # recovery when stream operations fail.
17
+ #
18
+ # @see Template
19
+ class Device::Writer < Device
20
+ EDGE_WHITESPACE_PATTERN = /\A\s|[ \t\f\v][\r\n]*\z/
21
+
22
+ # Initialize a new Writer device with configurable formatting and stream options.
23
+ # The device supports multiple template types, encoding control, and stream
24
+ # behavior configuration for flexible output handling.
25
+ #
26
+ # @param stream [IO, #write] The target stream for log output. Can be any object
27
+ # that responds to write(), including File objects, STDOUT/STDERR, StringIO,
28
+ # network streams, or custom IO-like objects
29
+ # @param options [Hash] Configuration options for the writer device
30
+ #
31
+ # @option options [String, Proc, nil] :template The formatting template for log entries.
32
+ # - String: Compiled into a Template object (default: "[:time :severity :progname(:pid)] :message")
33
+ # - Proc: Called with LogEntry, should return formatted string
34
+ # - nil: Uses default template
35
+ #
36
+ # @option options [Logger::Formatter] :standard_logger_formatter Use a Ruby Logger
37
+ # formatter for compatibility with existing logging code
38
+ #
39
+ # @option options [String, nil] :additional_lines Template for formatting additional
40
+ # lines in multi-line messages (default: "\n :message")
41
+ #
42
+ # @option options [String, Symbol] :time_format Format for timestamps in templates.
43
+ # Accepts strftime patterns or :milliseconds/:microseconds shortcuts
44
+ #
45
+ # @option options [String] :attribute_format Printf-style format for attributes
46
+ # with exactly two %s placeholders for name and value (default: "[%s:%s]")
47
+ #
48
+ # @option options [Boolean] :autoflush (true) Whether to automatically flush
49
+ # the stream after each write for immediate output
50
+ #
51
+ # @option options [Boolean] :binmode (false) Whether to treat the stream as
52
+ # binary, skipping UTF-8 encoding conversion
53
+ #
54
+ # @option options [Boolean] :colorize (false) Whether to colorize log output
55
+ def initialize(stream, options = {})
56
+ @stream = stream
57
+ @stream.sync = true if @stream.respond_to?(:sync=) && options[:autoflush] != false
25
58
 
26
- def <<(string)
27
- @values << string
28
- @size += string.size
29
- end
59
+ @binmode = options[:binmode]
30
60
 
31
- def empty?
32
- @values.empty?
33
- end
61
+ if options[:standard_logger_formatter]
62
+ @template = Template::StandardFormatterTemplate.new(options[:standard_logger_formatter])
63
+ else
64
+ template = options[:template]
34
65
 
35
- def pop!
36
- return nil if @values.empty?
37
- popped = @values
38
- clear
39
- popped
40
- end
66
+ template = TemplateRegistry.template(template, options) if template.is_a?(Symbol)
41
67
 
42
- def clear
43
- @values = []
44
- @size = 0
45
- end
46
- end
47
-
48
- # Create a new device to write log entries to a stream. Entries are converted to strings
49
- # using a Template. The template can be specified using the :template option. This can
50
- # either be a Proc or a string that will compile into a Template object.
51
- #
52
- # If the template is a Proc, it should accept an LogEntry as its only argument and output a string.
53
- #
54
- # If the template is a template string, it will be used to create a Template. The
55
- # :additional_lines and :time_format options will be passed through to the
56
- # Template constuctor.
57
- #
58
- # The default template is "[:time :severity :progname(:pid)] :message"
59
- # with additional lines formatted as "\n :message".
60
- #
61
- # The size of the internal buffer in bytes can be set by providing :buffer_size (defaults to 32K).
62
- #
63
- # @param [IO] stream The stream to write log entries to.
64
- # @param [Hash] options The options for the device.
65
- def initialize(stream, options = {})
66
- @lock = Mutex.new
67
- @stream = stream
68
- @stream.sync = true if @stream.respond_to?(:sync=)
69
- @buffer = Buffer.new
70
- @buffer_size = options[:buffer_size] || 0
71
- template = options[:template] || DEFAULT_FIRST_LINE_TEMPLATE
72
- if template.respond_to?(:call)
73
- @template = template
68
+ @template = if template.respond_to?(:call)
69
+ template
74
70
  else
75
- additional_lines = options[:additional_lines] || DEFAULT_ADDITIONAL_LINES_TEMPLATE
76
- @template = Template.new(template, additional_lines: additional_lines, time_format: options[:time_format])
71
+ Template.new(
72
+ template,
73
+ additional_lines: options[:additional_lines],
74
+ time_format: options[:time_format],
75
+ attribute_format: options[:attribute_format],
76
+ colorize: options[:colorize]
77
+ )
77
78
  end
78
79
  end
80
+ end
79
81
 
80
- # Set the buffer size in bytes. The device will only be physically written to when the buffer size
81
- # is exceeded.
82
- #
83
- # @param [Integer] value The size of the buffer in bytes.
84
- # @return [void]
85
- def buffer_size=(value)
86
- @buffer_size = value
87
- flush
82
+ # Write a log entry to the stream with automatic formatting and error handling.
83
+ # The entry is converted to a string using the configured template, processed
84
+ # for encoding and whitespace, and written to the stream with robust error recovery.
85
+ #
86
+ # @param entry [LogEntry, String] The log entry to write. LogEntry objects are
87
+ # formatted using the template, while strings are written directly after
88
+ # encoding and whitespace processing
89
+ # @return [void]
90
+ def write(entry)
91
+ string = (entry.is_a?(LogEntry) ? @template.call(entry) : entry)
92
+ return if string.nil?
93
+
94
+ if !@binmode && string.encoding != Encoding::UTF_8
95
+ string = string.encode("UTF-8", invalid: :replace, undef: :replace)
88
96
  end
89
97
 
90
- # Write an entry to the stream. The entry will be converted into a string using the defined template.
91
- #
92
- # @param [LogEntry, String] entry The entry to write to the stream.
93
- # @return [void]
94
- def write(entry)
95
- string = (entry.is_a?(LogEntry) ? @template.call(entry) : entry)
96
- return if string.nil?
97
- string = string.strip
98
- return if string.length == 0
99
-
100
- unless string.encoding == Encoding::UTF_8
101
- string = string.encode("UTF-8", invalid: :replace, undef: :replace)
102
- end
98
+ string = string.strip if string.match?(EDGE_WHITESPACE_PATTERN)
99
+ return if string.length == 0 || string == Lumberjack::LINE_SEPARATOR
103
100
 
104
- if buffer_size > 1
105
- @lock.synchronize do
106
- @buffer << string
107
- end
108
- flush if @buffer.size >= buffer_size
109
- else
110
- flush if respond_to?(:before_flush, true)
111
- write_to_stream(string)
112
- end
113
- end
114
-
115
- # Close the underlying stream.
116
- #
117
- # @return [void]
118
- def close
119
- flush
120
- stream.close
121
- end
101
+ write_to_stream(string)
102
+ end
122
103
 
123
- # Flush the underlying stream.
124
- #
125
- # @return [void]
126
- def flush
127
- lines = nil
128
- @lock.synchronize do
129
- before_flush if respond_to?(:before_flush, true)
130
- lines = @buffer.pop!
131
- end
132
- write_to_stream(lines) if lines
133
- end
104
+ # Close the underlying stream and release any associated resources. This method
105
+ # ensures all buffered data is flushed before closing the stream, providing
106
+ # clean shutdown behavior for file handles and network connections.
107
+ #
108
+ # @return [void]
109
+ def close
110
+ flush
111
+ stream.close
112
+ end
134
113
 
135
- # Get the datetime format.
136
- #
137
- # @return [String] The datetime format.
138
- def datetime_format
139
- @template.datetime_format if @template.respond_to?(:datetime_format)
140
- end
114
+ # Flush the underlying stream to ensure all buffered data is written to the
115
+ # destination. This method is safe to call on streams that don't support
116
+ # flushing, making it suitable for various IO types.
117
+ #
118
+ # @return [void]
119
+ def flush
120
+ stream.flush if stream.respond_to?(:flush)
121
+ end
141
122
 
142
- # Set the datetime format.
143
- #
144
- # @param [String] format The datetime format.
145
- # @return [void]
146
- def datetime_format=(format)
147
- if @template.respond_to?(:datetime_format=)
148
- @template.datetime_format = format
149
- end
150
- end
123
+ # Get the current datetime format from the template if supported. Returns the
124
+ # format string used for timestamp formatting in log entries.
125
+ #
126
+ # @return [String, nil] The datetime format string if the template supports it,
127
+ # or nil if the template doesn't provide datetime formatting
128
+ def datetime_format
129
+ @template.datetime_format if @template.respond_to?(:datetime_format)
130
+ end
151
131
 
152
- # Return the underlying stream. Provided for API compatibility with Logger devices.
153
- #
154
- # @return [IO] The underlying stream.
155
- def dev
156
- @stream
132
+ # Set the datetime format on the template if supported. This allows dynamic
133
+ # reconfiguration of timestamp formatting without recreating the device.
134
+ #
135
+ # @param format [String] The datetime format string (strftime pattern) to
136
+ # apply to the template for timestamp formatting
137
+ # @return [void]
138
+ def datetime_format=(format)
139
+ if @template.respond_to?(:datetime_format=)
140
+ @template.datetime_format = format
157
141
  end
142
+ end
158
143
 
159
- protected
160
-
161
- # Set the underlying stream.
162
- attr_writer :stream
144
+ # Access the underlying IO stream for direct manipulation or compatibility
145
+ # with code expecting Logger device interface. This method provides the
146
+ # raw stream object for advanced use cases.
147
+ #
148
+ # @return [IO] The underlying stream object used for output
149
+ # @api private
150
+ def dev
151
+ stream
152
+ end
163
153
 
164
- # Get the underlying stream.
165
- attr_reader :stream
154
+ # Get the file system path of the underlying stream if available. This method
155
+ # is useful for monitoring, log rotation, or any operations that need to
156
+ # work with the actual file path.
157
+ #
158
+ # @return [String, nil] The file system path if the stream is file-based,
159
+ # or nil for non-file streams (STDOUT, StringIO, network streams, etc.)
160
+ def path
161
+ stream.path if stream.respond_to?(:path)
162
+ end
166
163
 
167
- private
164
+ # The underlying stream object that is being written to.
165
+ #
166
+ # @return [IO] The current stream object
167
+ attr_accessor :stream
168
168
 
169
- def write_to_stream(lines)
170
- return if lines.empty?
171
- lines = lines.first if lines.is_a?(Array) && lines.size == 1
172
- out = if lines.is_a?(Array)
173
- "#{lines.join(Lumberjack::LINE_SEPARATOR)}#{Lumberjack::LINE_SEPARATOR}"
174
- else
175
- "#{lines}#{Lumberjack::LINE_SEPARATOR}"
176
- end
169
+ private
177
170
 
171
+ # Write a formatted line to the stream with robust error handling. This method
172
+ # ensures proper line termination, handles IO errors gracefully, and provides
173
+ # fallback error reporting to STDERR when the primary stream fails.
174
+ #
175
+ # @param line [String] The formatted log line to write
176
+ # @return [void]
177
+ def write_to_stream(line)
178
+ out = line.end_with?(Lumberjack::LINE_SEPARATOR) ? line : "#{line}#{Lumberjack::LINE_SEPARATOR}"
179
+ begin
178
180
  begin
179
- begin
180
- stream.write(out)
181
- rescue IOError => e
182
- # This condition can happen if another thread closed the stream in the `before_flush` call.
183
- # Synchronizing will handle the race condition, but since it's an exceptional case we don't
184
- # want to lock the thread on every stream write call.
185
- @lock.synchronize do
186
- if stream.closed?
187
- raise e
188
- else
189
- stream.write(out)
190
- end
191
- end
192
- end
193
- begin
194
- stream.flush
195
- rescue
196
- nil
197
- end
198
- rescue => e
199
- $stderr.write("#{e.class.name}: #{e.message}#{" at " + e.backtrace.first if e.backtrace}")
200
- $stderr.write(out)
201
- $stderr.flush
181
+ stream.write(out)
182
+ rescue IOError => e
183
+ raise e if stream.closed?
184
+
185
+ stream.write(out)
202
186
  end
187
+ rescue => e
188
+ $stderr.write(error_message(e))
189
+ $stderr.write(out)
203
190
  end
204
191
  end
192
+
193
+ # Generate a detailed error message for logging failures. This method creates
194
+ # informative error messages that include exception details and backtrace
195
+ # information for debugging stream write failures.
196
+ #
197
+ # @param e [Exception] The exception that occurred during stream operations
198
+ # @return [String] A formatted error message with exception details
199
+ def error_message(e)
200
+ "#{e.class.name}: #{e.message}#{" at " + e.backtrace.first if e.backtrace}#{Lumberjack::LINE_SEPARATOR}"
201
+ end
202
+
203
+ # Create a test log template.
204
+ def test_log_template(options)
205
+ kwargs = {
206
+ exclude_attributes: options[:exclude_attributes],
207
+ exclude_progname: options[:exclude_progname],
208
+ exclude_pid: options[:exclude_pid],
209
+ exclude_time: options[:exclude_time]
210
+ }
211
+ TestLogTemplate.new(**kwargs.compact)
212
+ end
205
213
  end
206
214
  end
@@ -1,57 +1,176 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "device_registry"
4
+
3
5
  module Lumberjack
4
- # This is an abstract class for logging devices. Subclasses must implement the +write+ method and
5
- # may implement the +close+ and +flush+ methods if applicable.
6
+ # Abstract base class defining the interface for logging output devices.
7
+ # Devices are responsible for the final output of log entries to various
8
+ # destinations such as files, streams, databases, or external services.
9
+ #
10
+ # This class establishes the contract that all concrete device implementations
11
+ # must follow, with the +write+ method being the only required implementation.
12
+ # Additional lifecycle methods (+close+, +flush+, +reopen+) and configuration
13
+ # methods (+datetime_format+) are optional but provide standardized interfaces
14
+ # for device management.
15
+ #
16
+ # The device architecture allows for flexible log output handling while
17
+ # maintaining consistent behavior across different output destinations.
18
+ # Devices receive formatted LogEntry objects and are responsible for their
19
+ # final serialization and delivery.
20
+ #
21
+ # @abstract Subclass and implement {#write} to create a concrete device
22
+ # @see Lumberjack::Device::Writer File-based output device
23
+ # @see Lumberjack::Device::LoggerWrapper Ruby Logger compatibility device
24
+ # @see Lumberjack::Device::Multi Multiple device routing
25
+ # @see Lumberjack::Device::Null Silent device for testing
26
+ # @see Lumberjack::Device::Test In-memory device for testing
6
27
  class Device
7
28
  require_relative "device/writer"
8
29
  require_relative "device/log_file"
9
- require_relative "device/rolling_log_file"
10
- require_relative "device/date_rolling_log_file"
11
- require_relative "device/size_rolling_log_file"
30
+ require_relative "device/logger_wrapper"
12
31
  require_relative "device/multi"
13
32
  require_relative "device/null"
33
+ require_relative "device/test"
34
+ require_relative "device/buffer"
35
+ require_relative "device/size_rolling_log_file"
36
+ require_relative "device/date_rolling_log_file"
37
+
38
+ class << self
39
+ # Open a logging device with the given options.
40
+ #
41
+ # @param device [nil, Symbol, String, File, IO, Array, Lumberjack::Device, ContextLogger] The device to open.
42
+ # The device can be:
43
+ # - +nil+: returns a +Device::Null+ instance that discards all log entries.
44
+ # - +Symbol+: looks up the device in the +DeviceRegistry+ and creates a new instance with the provided options.
45
+ # - +String+ or +Pathname+: treated as a file path and opens a +Device::LogFile+.
46
+ # - +File+: opens a +Device::LogFile+ for the given file stream.
47
+ # - +IO+: opens a +Device::Writer+ wrapping the given IO stream.
48
+ # - +Lumberjack::Device+: returns the device instance as-is.
49
+ # - +ContextLogger+: wraps the logger in a +Device::LoggerWrapper+.
50
+ # - +Array+: each element is treated as a device specification and opened recursively,
51
+ # returning a +Device::Multi+ that routes log entries to all specified devices. Each
52
+ # device can have its own options hash if passed as a two-element array +[device, options]+.
53
+ # @param options [Hash] Options to pass to the device constructor.
54
+ # @return [Lumberjack::Device] The opened device instance.
55
+ #
56
+ # @example Open a file-based device
57
+ # device = Lumberjack::Device.open_device("/var/log/myapp.log", shift_age: "daily")
58
+ #
59
+ # @example Open a stream-based device
60
+ # device = Lumberjack::Device.open_device($stdout)
61
+ #
62
+ # @example Open a device from the registry
63
+ # device = Lumberjack::Device.open_device(:syslog)
64
+ #
65
+ # @example Open multiple devices
66
+ # device = Lumberjack::Device.open_device([["/var/log/app.log", {shift_age: "daily"}], $stdout])
67
+ #
68
+ # @example Wrap another logger
69
+ # device = Lumberjack::Device.open_device(Lumberjack::Logger.new($stdout))
70
+ def open_device(device, options = {})
71
+ device = device.to_s if device.is_a?(Pathname)
72
+
73
+ if device.nil?
74
+ Device::Null.new
75
+ elsif device.is_a?(Device)
76
+ device
77
+ elsif device.is_a?(Symbol)
78
+ DeviceRegistry.new_device(device, options)
79
+ elsif device.is_a?(ContextLogger) || device.is_a?(::Logger)
80
+ Device::LoggerWrapper.new(device)
81
+ elsif device.is_a?(Array)
82
+ devices = device.collect do |dev, dev_options|
83
+ dev_options = dev_options.is_a?(Hash) ? options.merge(dev_options) : options
84
+ open_device(dev, dev_options)
85
+ end
86
+ Device::Multi.new(devices)
87
+ elsif io_but_not_file_stream?(device)
88
+ Device::Writer.new(device, options)
89
+ else
90
+ Device::LogFile.new(device, options)
91
+ end
92
+ end
14
93
 
15
- # Subclasses must implement this method to write a LogEntry.
94
+ private
95
+
96
+ def io_but_not_file_stream?(object)
97
+ return false if object.is_a?(File)
98
+ return false unless object.respond_to?(:write)
99
+ return true if object.respond_to?(:tty?) && object.tty?
100
+ return false if object.respond_to?(:path) && object.path
101
+
102
+ true
103
+ end
104
+ end
105
+
106
+ # Write a log entry to the device. This is the core method that all device
107
+ # implementations must provide. The method receives a fully formatted
108
+ # LogEntry object and is responsible for outputting it to the target
109
+ # destination.
16
110
  #
17
- # @param [Lumberjack::LogEntry] entry The entry to write.
111
+ # @param entry [Lumberjack::LogEntry] The log entry to write to the device
18
112
  # @return [void]
113
+ # @abstract Subclasses must implement this method
114
+ # @raise [NotImplementedError] If called on the abstract base class
19
115
  def write(entry)
20
116
  raise NotImplementedError
21
117
  end
22
118
 
23
- # Subclasses may implement this method to close the device.
119
+ # Close the device and release any resources. The default implementation
120
+ # calls flush to ensure any buffered data is written before closing.
121
+ # Subclasses should override this method if they need to perform specific
122
+ # cleanup operations such as closing file handles or network connections.
24
123
  #
25
124
  # @return [void]
26
125
  def close
27
126
  flush
28
127
  end
29
128
 
30
- # Subclasses may implement this method to reopen the device.
129
+ # Reopen the device, optionally with a new log destination. The default
130
+ # implementation calls flush to ensure data consistency. This method is
131
+ # typically used for log rotation scenarios or when changing output
132
+ # destinations dynamically.
31
133
  #
32
- # @param [Object] logdev The log device to use.
134
+ # @param logdev [Object, nil] Optional new log device or destination
33
135
  # @return [void]
34
136
  def reopen(logdev = nil)
35
137
  flush
36
138
  end
37
139
 
38
- # Subclasses may implement this method to flush any buffers used by the device.
140
+ # Flush any buffered data to the output destination. The default
141
+ # implementation is a no-op since not all devices use buffering.
142
+ # Subclasses that implement buffering should override this method
143
+ # to ensure data is written to the final destination.
39
144
  #
40
145
  # @return [void]
41
146
  def flush
42
147
  end
43
148
 
44
- # Subclasses may implement this method to get the format for log timestamps.
149
+ # Get the current datetime format string used for timestamp formatting.
150
+ # The default implementation returns nil, indicating no specific format
151
+ # is set. Subclasses may override this to provide device-specific
152
+ # timestamp formatting.
45
153
  #
46
- # @return [String] The format for log timestamps.
154
+ # @return [String, nil] The datetime format string, or nil if not set
47
155
  def datetime_format
48
156
  end
49
157
 
50
- # Subclasses may implement this method to set a format for log timestamps.
158
+ # Set the datetime format string for timestamp formatting. The default
159
+ # implementation is a no-op. Subclasses that support configurable
160
+ # timestamp formatting should override this method to store and apply
161
+ # the specified format.
51
162
  #
52
- # @param [String] format The format for log timestamps.
163
+ # @param format [String, nil] The datetime format string to use for timestamps
53
164
  # @return [void]
54
165
  def datetime_format=(format)
55
166
  end
167
+
168
+ # Expose the underlying stream if any.
169
+ #
170
+ # @return [IO, Lumberjacke::Device, nil]
171
+ # @api private
172
+ def dev
173
+ self
174
+ end
56
175
  end
57
176
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # The device registry is used for setting up names to represent Device classes. It is used
5
+ # in the constructor for Lumberjack::Logger and allows passing in a symbol to reference a
6
+ # device.
7
+ #
8
+ # Devices must have a constructor that accepts the options hash as its sole argument in order
9
+ # to use the device registry.
10
+ #
11
+ # The values :stdout and :stderr are registered by default and map to the standard output
12
+ # and standard error streams, respectively.
13
+ #
14
+ # @example
15
+ #
16
+ # Lumberjack::Device.register(:my_device, MyDevice)
17
+ # logger = Lumberjack::Logger.new(:my_device)
18
+ module DeviceRegistry
19
+ @registry = {stdout: :stdout, stderr: :stderr}
20
+
21
+ class << self
22
+ # Register a device name. Device names can be used to associate a symbol with a device
23
+ # class. The symbol can then be passed to Logger as the device argument.
24
+ #
25
+ # Registered devices must take only one argument and that is the options hash for the
26
+ # device options.
27
+ #
28
+ # @param name [Symbol] The name of the device
29
+ # @param klass [Class] The device class to register
30
+ # @return [void]
31
+ def add(name, klass)
32
+ raise ArgumentError.new("name must be a symbol") unless name.is_a?(Symbol)
33
+
34
+ @registry[name] = klass
35
+ end
36
+
37
+ # Remove a device from the registry.
38
+ #
39
+ # @param name [Symbol] The name of the device to remove
40
+ # @return [void]
41
+ def remove(name)
42
+ @registry.delete(name)
43
+ end
44
+
45
+ # Check if a device is registered.
46
+ #
47
+ # @param name [Symbol] The name of the device
48
+ # @return [Boolean] True if the device is registered, false otherwise
49
+ def registered?(name)
50
+ @registry.include?(name)
51
+ end
52
+
53
+ # Instantiate a new device with the specified options from the device registry.
54
+ #
55
+ # @param name [Symbol] The name of the device
56
+ # @param options [Hash] The device options
57
+ # @return [Lumberjack::Device]
58
+ def new_device(name, options)
59
+ klass = device_class(name)
60
+ unless klass
61
+ valid_names = @registry.keys.map(&:inspect).join(", ")
62
+ raise ArgumentError.new("#{name.inspect} is not registered as a device name; valid names are: #{valid_names}")
63
+ end
64
+
65
+ if klass == :stdout
66
+ Device::Writer.new($stdout, options)
67
+ elsif klass == :stderr
68
+ Device::Writer.new($stderr, options)
69
+ else
70
+ klass.new(options)
71
+ end
72
+ end
73
+
74
+ # Retrieve the class registered with the given name or nil if the name is not defined.
75
+ #
76
+ # @param name [Symbol] The name of the device
77
+ # @return [Class, nil] The registered device class or nil if not found
78
+ def device_class(name)
79
+ @registry[name]
80
+ end
81
+
82
+ # Return the map of registered device class names.
83
+ #
84
+ # @return [Hash]
85
+ def registered_devices
86
+ @registry.dup
87
+ end
88
+ end
89
+ end
90
+ end