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,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # EntryFormatter provides a unified interface for formatting complete log entries by combining
5
+ # message formatting and attribute formatting into a single, coordinated system.
6
+ #
7
+ # This class serves as the central formatting coordinator in the Lumberjack logging pipeline,
8
+ # bringing together two specialized formatters:
9
+ # 1. Message Formatter ({Lumberjack::Formatter}) - Formats the main log message content
10
+ # 2. Attribute Formatter ({Lumberjack::AttributeFormatter}) - Formats key-value attribute pairs
11
+ #
12
+ # @example Complete entry formatting setup
13
+ # entry_formatter = Lumberjack::EntryFormatter.build do |formatter|
14
+ # # Formatter will be used in both log messages and attributes
15
+ # formatter.format_class(ActiveRecord::Base, :id)
16
+ #
17
+ # # Message specific formatters
18
+ # formatter.format_message(Exception, :exception)
19
+ # formatter.format_message(Time, :date_time, "%Y-%m-%d %H:%M:%S")
20
+ #
21
+ # # Attribute specific formatters
22
+ # formatter.format_attributes(Time, :date_time, "%Y-%m-%d")
23
+ # formatter.format_attribute_name("password") { |value| "[REDACTED]" }
24
+ # formatter.format_attribute_name("user_id", :id)
25
+ # formatter.default_attribute_format { |value| value.to_s.strip }
26
+ # end
27
+ #
28
+ # @example Using with a logger
29
+ # logger = Lumberjack::Logger.new(STDOUT, formatter: entry_formatter)
30
+ # logger.info("User login", user: user_object, timestamp: Time.now)
31
+ # # Both the message and attributes are formatted according to the rules
32
+ #
33
+ # @see Lumberjack::Formatter
34
+ # @see Lumberjack::AttributeFormatter
35
+ # @see Lumberjack::Logger
36
+ class EntryFormatter
37
+ # The message formatter used to format log message content.
38
+ # @return [Lumberjack::Formatter] The message formatter instance.
39
+ attr_reader :message_formatter
40
+
41
+ # The attribute formatter used to format log entry attributes.
42
+ # @return [Lumberjack::AttributeFormatter] The attribute formatter instance.
43
+ attr_reader :attribute_formatter
44
+
45
+ class << self
46
+ # Build a new entry formatter using a configuration block. The block receives the new formatter
47
+ # as a parameter, allowing you to configure it with various configuration methods.
48
+ #
49
+ # @param message_formatter [Lumberjack::Formatter, Symbol, nil] The message formatter to use.
50
+ # Can be a Formatter instance, :default for standard formatter, :none for empty formatter, or nil.
51
+ # @param attribute_formatter [Lumberjack::AttributeFormatter, nil] The attribute formatter to use.
52
+ # @yield [formatter] A block that configures the entry formatter.
53
+ # @return [Lumberjack::EntryFormatter] A new configured entry formatter.
54
+ #
55
+ # @example
56
+ # formatter = Lumberjack::EntryFormatter.build do |config|
57
+ # config.add(User, :id) # Message formatting
58
+ # config.add(Time, :date_time, "%Y-%m-%d")
59
+ #
60
+ # config.add_attribute("password") { "[REDACTED]" } # ensure passwords are not logged
61
+ # config.add_attribute_class(Exception) { |e| {error: e.class.name, message: e.message} }
62
+ # end
63
+ def build(message_formatter: nil, attribute_formatter: nil, &block)
64
+ formatter = new(message_formatter: message_formatter, attribute_formatter: attribute_formatter)
65
+ block&.call(formatter)
66
+ formatter
67
+ end
68
+ end
69
+
70
+ # Create a new entry formatter with the specified message and attribute formatters.
71
+ #
72
+ # @param message_formatter [Lumberjack::Formatter, Symbol, nil] The message formatter to use:
73
+ # - Formatter instance: Used directly
74
+ # - :default or nil: Creates a new Formatter with default mappings
75
+ # - :none: Creates an empty Formatter with no default mappings
76
+ # @param attribute_formatter [Lumberjack::AttributeFormatter, nil] The attribute formatter to use.
77
+ # If nil, no attribute formatting will be performed unless configured later.
78
+ def initialize(message_formatter: nil, attribute_formatter: nil)
79
+ self.message_formatter = message_formatter
80
+ self.attribute_formatter = attribute_formatter
81
+ end
82
+
83
+ # Set the message formatter used to format log message content.
84
+ #
85
+ # @param value [Lumberjack::Formatter, Symbol, nil] The message formatter to use. If the value
86
+ # is :default, a standard formatter with default mappings is created. If nil, a new empty
87
+ # formatter is created.
88
+ # @return [void]
89
+ def message_formatter=(value)
90
+ @message_formatter = if value == :default
91
+ Lumberjack::Formatter.default
92
+ elsif value.nil?
93
+ Lumberjack::Formatter.new
94
+ else
95
+ value
96
+ end
97
+ end
98
+
99
+ # Set the attribute formatter used to format log entry attributes.
100
+ #
101
+ # @param value [Lumberjack::AttributeFormatter, nil] The attribute formatter to use.
102
+ # If nil, a new empty AttributeFormatter is created.
103
+ # @return [void]
104
+ def attribute_formatter=(value)
105
+ @attribute_formatter = value || AttributeFormatter.new
106
+ end
107
+
108
+ # Add a formatter for specific classes or modules. This method adds the formatter
109
+ # for both log messages and attributes.
110
+ #
111
+ # @param classes_or_names [Class, Module, String, Array<Class, Module, String>] The class(es) to format.
112
+ # @param formatter [Symbol, Class, #call, nil] The formatter to use.
113
+ # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
114
+ # @yield [obj] Block-based formatter that receives the object to format.
115
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
116
+ #
117
+ # @example Adding formatters
118
+ # formatter.format_class(User, :id) # Use ID formatter for User objects
119
+ # formatter.format_class(Array) { |vals| vals.join(", ") } # Handle arrays
120
+ # formatter.format_class(Time, :date_time, "%Y-%m-%d") # Custom time format only
121
+ #
122
+ # @see Lumberjack::Formatter#add
123
+ def format_class(classes_or_names, formatter = nil, *args, &block)
124
+ Array(classes_or_names).each do |class_or_name|
125
+ unless @message_formatter.include?(class_or_name)
126
+ @message_formatter.add(class_or_name, formatter, *args, &block)
127
+ end
128
+
129
+ unless @attribute_formatter.include_class?(class_or_name)
130
+ @attribute_formatter.add_class(class_or_name, formatter, *args, &block)
131
+ end
132
+ end
133
+
134
+ self
135
+ end
136
+
137
+ # Remove a formatter for specific classes or modules. This method removes the formatter
138
+ # for both log messages and attributes.
139
+ #
140
+ # @param classes_or_names [Class, Module, String, Array<Class, Module, String>] The class(es) to remove formatters for.
141
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
142
+ #
143
+ # @see Lumberjack::Formatter#remove
144
+ def remove_class(classes_or_names)
145
+ @message_formatter.remove(classes_or_names)
146
+ @attribute_formatter.remove_class(classes_or_names)
147
+ self
148
+ end
149
+
150
+ # Add a message formatter for specific classes or modules.
151
+ #
152
+ # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
153
+ # @param formatter [Symbol, Class, #call, nil] The formatter to use
154
+ # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
155
+ # @yield [obj] Block-based formatter that receives the object to format.
156
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
157
+ #
158
+ # @see Lumberjack::Formatter#add
159
+ def format_message(classes_or_names, formatter = nil, *args, &block)
160
+ @message_formatter.add(classes_or_names, formatter, *args, &block)
161
+ self
162
+ end
163
+
164
+ # Remove a message formatter for specific classes or modules.
165
+ #
166
+ # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
167
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
168
+ #
169
+ # @see Lumberjack::Formatter#remove
170
+ def remove_message_formatter(classes_or_names)
171
+ @message_formatter.remove(classes_or_names)
172
+ self
173
+ end
174
+
175
+ # Add a formatter for the named attribute.
176
+ #
177
+ # @param names [String, Symbol, Array<String, Symbol>] The attribute names to format.
178
+ # @param formatter [Symbol, Class, #call, nil] The formatter to use
179
+ # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
180
+ # @yield [value] Block-based formatter that receives the attribute value.
181
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
182
+ #
183
+ # @see Lumberjack::AttributeFormatter#add_attribute
184
+ def format_attribute_name(names, formatter = nil, *args, &block)
185
+ @attribute_formatter.add_attribute(names, formatter, *args, &block)
186
+ self
187
+ end
188
+
189
+ # Add a formatter for the specified class or module when it appears as an attribute value.
190
+ #
191
+ # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
192
+ # @param formatter [Symbol, Class, #call, nil] The formatter to use
193
+ # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
194
+ # @yield [value] Block-based formatter that receives the attribute value.
195
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
196
+ #
197
+ # @see Lumberjack::AttributeFormatter#add_attribute
198
+ def format_attributes(classes_or_names, formatter = nil, *args, &block)
199
+ @attribute_formatter.add_class(classes_or_names, formatter, *args, &block)
200
+ self
201
+ end
202
+
203
+ # Set the default attribute formatter to apply to any attributes that do not
204
+ # have a specific formatter defined.
205
+ #
206
+ # @param formatter [Symbol, Class, #call, nil] The default formatter to use.
207
+ # If nil, removes any existing default formatter.
208
+ # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
209
+ # @yield [value] Block-based formatter that receives the attribute value.
210
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
211
+ #
212
+ # @see Lumberjack::AttributeFormatter#default
213
+ def default_attribute_format(formatter = nil, *args, &block)
214
+ @attribute_formatter.default(formatter, *args, &block)
215
+ self
216
+ end
217
+
218
+ # Remove an attribute formatter for the specified attribute names.
219
+ #
220
+ # @param names [String, Symbol, Array<String, Symbol>] The attribute names to remove formatters for.
221
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
222
+ #
223
+ # @see Lumberjack::AttributeFormatter#remove_attribute
224
+ def remove_attribute_name(names)
225
+ @attribute_formatter.remove_attribute(names)
226
+ self
227
+ end
228
+
229
+ # Remove an attribute formatter for the specified classes or modules.
230
+ #
231
+ # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
232
+ # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
233
+ #
234
+ # @see Lumberjack::AttributeFormatter#remove_class
235
+ def remove_attribute_class(classes_or_names)
236
+ @attribute_formatter.remove_class(classes_or_names)
237
+ self
238
+ end
239
+
240
+ # Extend this formatter by adding the formats defined in the provided formatter into this one.
241
+ #
242
+ # @param formatter [Lumberjack::EntryFormatter] The formatter to merge.
243
+ # @return [self] Returns self for method chaining.
244
+ def include(formatter)
245
+ unless formatter.is_a?(Lumberjack::EntryFormatter)
246
+ raise ArgumentError.new("formatter must be a Lumberjack::EntryFormatter")
247
+ end
248
+
249
+ @message_formatter ||= Lumberjack::Formatter.new
250
+ @message_formatter.include(formatter.message_formatter)
251
+
252
+ @attribute_formatter ||= Lumberjack::AttributeFormatter.new
253
+ @attribute_formatter.include(formatter.attribute_formatter)
254
+
255
+ self
256
+ end
257
+
258
+ # Extend this formatter by adding the formats defined in the provided formatter into this one.
259
+ # Formats defined in this formatter will take precedence and not be overridden.
260
+ #
261
+ # @param formatter [Lumberjack::EntryFormatter] The formatter to merge.
262
+ # @return [self] Returns self for method chaining.
263
+ def prepend(formatter)
264
+ unless formatter.is_a?(Lumberjack::EntryFormatter)
265
+ raise ArgumentError.new("formatter must be a Lumberjack::EntryFormatter")
266
+ end
267
+
268
+ @message_formatter ||= Lumberjack::Formatter.new
269
+ @message_formatter.prepend(formatter.message_formatter)
270
+
271
+ @attribute_formatter ||= Lumberjack::AttributeFormatter.new
272
+ @attribute_formatter.prepend(formatter.attribute_formatter)
273
+
274
+ self
275
+ end
276
+
277
+ # Format a complete log entry by applying both message and attribute formatting.
278
+ # This is the main method that coordinates the formatting of both the message content
279
+ # and any associated attributes.
280
+ #
281
+ # @param message [Object, Proc, nil] The log message to format. Can be any object, a Proc that returns the message, or nil.
282
+ # @param attributes [Hash, nil] The log entry attributes to format.
283
+ # @return [Array<Object, Hash>] A two-element array containing [formatted_message, formatted_attributes].
284
+ def format(message, attributes)
285
+ message = message.call if message.is_a?(Proc)
286
+ message = message_formatter.format(message) if message_formatter.respond_to?(:format)
287
+
288
+ message_attributes = nil
289
+ if message.is_a?(MessageAttributes)
290
+ message_attributes = message.attributes
291
+ message = message.message
292
+ end
293
+ message_attributes = Utils.flatten_attributes(message_attributes) if message_attributes
294
+
295
+ attributes = merge_attributes(attributes, message_attributes) if message_attributes
296
+ attributes = AttributesHelper.expand_runtime_values(attributes)
297
+ attributes = attribute_formatter.format(attributes) if attributes && attribute_formatter
298
+
299
+ [message, attributes]
300
+ end
301
+
302
+ # Compatibility method for Ruby's standard Logger::Formatter interface. This delegates
303
+ # to the message formatter's call method for basic Logger compatibility.
304
+ #
305
+ # @param severity [Integer, String, Symbol] The log severity (passed to message formatter).
306
+ # @param timestamp [Time] The log timestamp (passed to message formatter).
307
+ # @param progname [String] The program name (passed to message formatter).
308
+ # @param msg [Object] The message object to format (passed to message formatter).
309
+ # @return [String, nil] The formatted message string, or nil if no message formatter.
310
+ #
311
+ # @see Lumberjack::Formatter#call
312
+ def call(severity, timestamp, progname, msg)
313
+ message_formatter&.call(severity, timestamp, progname, msg)
314
+ end
315
+
316
+ private
317
+
318
+ # Merge two attribute hashes, handling nil values gracefully.
319
+ # Used to combine explicit log attributes with attributes embedded in MessageAttributes objects.
320
+ #
321
+ # @param current_attributes [Hash, nil] The primary attributes hash.
322
+ # @param attributes [Hash, nil] Additional attributes to merge in.
323
+ # @return [Hash, nil] The merged attributes hash, or nil if both inputs are nil/empty.
324
+ # @api private
325
+ def merge_attributes(current_attributes, attributes)
326
+ if current_attributes.nil? || current_attributes.empty?
327
+ attributes
328
+ elsif attributes.nil?
329
+ current_attributes
330
+ else
331
+ current_attributes.merge(attributes)
332
+ end
333
+ end
334
+
335
+ # Check if a formatter accepts an attributes parameter in its call method.
336
+ # This is used for determining formatter compatibility but is currently unused (TODO).
337
+ #
338
+ # @param formatter [#call] The formatter to check.
339
+ # @return [Boolean] true if the formatter accepts 5+ parameters or has a splat parameter.
340
+ # @api private
341
+ # @todo This method needs to be integrated into the logger functionality.
342
+ def accepts_attributes_parameter?(formatter)
343
+ method_obj = if formatter.is_a?(Proc)
344
+ formatter
345
+ elsif formatter.respond_to?(:call)
346
+ formatter.method(:call)
347
+ end
348
+ return false unless method_obj
349
+
350
+ params = method_obj.parameters
351
+ positional = params.slice(:req, :opt)
352
+ has_splat = params.any? { |type, _| type == :rest }
353
+ positional_count = positional.size
354
+ positional_count >= 5 || has_splat
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # Provides isolated fiber-local storage for thread-safe data access.
5
+ module FiberLocals
6
+ # Lightweight structure to hold fiber-local data.
7
+ #
8
+ # @api private
9
+ class Data
10
+ attr_accessor :context, :logging, :cleared
11
+
12
+ def initialize(copy = nil)
13
+ @context = copy&.context
14
+ @logging = copy&.logging
15
+ @cleared = copy&.cleared
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def fiber_locals(&block)
22
+ init_fiber_locals! unless defined?(@fiber_locals)
23
+
24
+ fiber_id = Fiber.current.object_id
25
+ current = @fiber_locals[fiber_id]
26
+ data = Data.new(current)
27
+ begin
28
+ @fiber_locals_mutex.synchronize do
29
+ @fiber_locals[fiber_id] = data
30
+ end
31
+ yield data
32
+ ensure
33
+ @fiber_locals_mutex.synchronize do
34
+ if current.nil?
35
+ @fiber_locals.delete(fiber_id)
36
+ else
37
+ @fiber_locals[fiber_id] = current
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def fiber_local
44
+ return nil unless defined?(@fiber_locals)
45
+
46
+ @fiber_locals[Fiber.current.object_id]
47
+ end
48
+
49
+ # Initialize the fiber locals storage and mutex.
50
+ def init_fiber_locals!
51
+ @fiber_locals ||= {}
52
+ @fiber_locals_mutex ||= Mutex.new
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # ForkedLogger provides an isolated logging context that forwards all log entries to a parent logger
5
+ # while maintaining its own independent configuration (level, progname, attributes).
6
+ #
7
+ # This class allows you to create specialized logger instances that:
8
+ # - Inherit initial configuration from a parent logger
9
+ # - Maintain isolated settings (level, progname, attributes) that don't affect the parent
10
+ # - Forward all log entries to the parent logger's output device
11
+ # - Combine their own attributes with the parent logger's attributes
12
+ # - Provide scoped logging behavior without duplicating output infrastructure
13
+ #
14
+ # ForkedLogger is particularly useful for:
15
+ #
16
+ # - Component isolation: Give each component its own logger with specific attributes
17
+ # - Request tracing: Create request-specific loggers with request IDs
18
+ # - Temporary debugging: Create debug-level loggers for specific code paths
19
+ # - Library integration: Allow libraries to have their own logging configuration
20
+ # - Multi-tenant logging: Isolate tenant-specific logging configuration
21
+ #
22
+ # The forked logger inherits the parent's initial state but changes are isolated:
23
+ #
24
+ # - Inherited: Initial level, progname, and device (through forwarding)
25
+ # - Isolated: subsequent changes to level, progname, and attributes do not affect the parent logger
26
+ # - Combined: attributes from the parent and the forked loggers are merged when logging
27
+ #
28
+ # @example Basic forked logger
29
+ # parent = Lumberjack::Logger.new(STDOUT, level: :info)
30
+ # forked = Lumberjack::ForkedLogger.new(parent)
31
+ # forked.level = :debug # Only affects the forked logger
32
+ # forked.debug("Debug message") # Logged because forked logger is debug level
33
+ #
34
+ # @example Component-specific logging
35
+ # main_logger = Lumberjack::Logger.new("/var/log/app.log")
36
+ # db_logger = Lumberjack::ForkedLogger.new(main_logger)
37
+ # db_logger.progname = "Database"
38
+ # db_logger.tag!(component: "database", version: "1.2.3")
39
+ # db_logger.info("Connection established") # Includes component attributes and different progname
40
+ #
41
+ # @example Request-scoped logging
42
+ # def handle_request(request_id)
43
+ # request_logger = Lumberjack::ForkedLogger.new(@logger)
44
+ # request_logger.tag!(request_id: request_id, user_id: current_user.id)
45
+ #
46
+ # request_logger.info("Processing request") # Includes request context
47
+ # # ... process request ...
48
+ # request_logger.info("Request completed") # All logs tagged with request info
49
+ # end
50
+ #
51
+ # @see Lumberjack::Logger
52
+ # @see Lumberjack::ContextLogger
53
+ # @see Lumberjack::Context
54
+ class ForkedLogger < Logger
55
+ include ContextLogger
56
+
57
+ # The parent logger that receives all log entries from this forked logger.
58
+ # @return [Lumberjack::Logger, #add_entry] The parent logger instance.
59
+ attr_reader :parent_logger
60
+
61
+ # Create a new forked logger that forwards all log entries to the specified parent logger.
62
+ # The forked logger inherits the parent's initial level and progname but maintains
63
+ # its own independent context for future changes.
64
+ #
65
+ # @param logger [Lumberjack::ContextLogger, #add_entry] The parent logger to forward entries to.
66
+ # Must respond to either +add_entry+ (for Lumberjack loggers) or standard Logger methods.
67
+ def initialize(logger)
68
+ init_fiber_locals!
69
+ @parent_logger = logger
70
+ @context = Context.new
71
+ @context.level ||= logger.level
72
+ @context.progname ||= logger.progname
73
+ end
74
+
75
+ # Forward a log entry to the parent logger with the forked logger's configuration applied.
76
+ # This method coordinates between the forked logger's settings and the parent logger's
77
+ # output capabilities.
78
+ #
79
+ # @param severity [Integer, Symbol, String] The severity level of the log entry.
80
+ # @param message [Object] The message to log.
81
+ # @param progname [String, nil] The program name (defaults to this logger's progname).
82
+ # @param attributes [Hash, nil] Additional attributes to include with the log entry.
83
+ # @return [Boolean] Returns the result of the parent logger's logging operation.
84
+ #
85
+ # @api private
86
+ def add_entry(severity, message, progname = nil, attributes = nil)
87
+ parent_logger.with_level(level || Logger::DEBUG) do
88
+ attributes = merge_attributes(attributes, local_attributes)
89
+ progname ||= self.progname
90
+
91
+ if parent_logger.is_a?(ContextLogger)
92
+ parent_logger.add_entry(severity, message, progname, attributes)
93
+ else
94
+ parent_logger.tag(attributes) do
95
+ parent_logger.add(severity, message, progname)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Flush any buffered log entries in the parent logger's device.
102
+ #
103
+ # @return [void]
104
+ def flush
105
+ parent_logger.flush
106
+ end
107
+
108
+ # Return the log device of the parent logger, if available.
109
+ #
110
+ # @return [Lumberjack::Device] The parent logger's output device.
111
+ def device
112
+ parent_logger.device if parent_logger.respond_to?(:device)
113
+ end
114
+
115
+ # Return the formatter of the parent logger, if available.
116
+ #
117
+ # @return [Lumberjack::EntryFormatter] The parent logger's formatter.
118
+ def formatter
119
+ parent_logger.formatter if parent_logger.respond_to?(:formatter)
120
+ end
121
+
122
+ private
123
+
124
+ # Return the default context for this forked logger. This provides the isolated
125
+ # configuration context that doesn't affect the parent logger.
126
+ #
127
+ # @return [Lumberjack::Context] The forked logger's private context.
128
+ # @api private
129
+ def default_context
130
+ @context
131
+ end
132
+
133
+ # Merge all attributes that should be included with log entries from this forked logger.
134
+ # This combines the default context attributes (set with tag!) and any local context
135
+ # attributes (set within context blocks).
136
+ #
137
+ # @return [Hash, nil] The merged attributes hash, or nil if no attributes are set.
138
+ # @api private
139
+ def local_attributes
140
+ merge_attributes(default_context&.attributes, local_context&.attributes)
141
+ end
142
+ end
143
+ end
@@ -2,16 +2,27 @@
2
2
 
3
3
  module Lumberjack
4
4
  class Formatter
5
- # Format a Date, Time, or DateTime object. If you don't specify a format in the constructor,
6
- # it will use the ISO-8601 format.
5
+ # Format a Date, Time, or DateTime object. If you don't specify a format in the constructor, it will use
6
+ # the ISO-8601 format with microsecond precision. This formatter provides consistent date/time representation
7
+ # across your application logs.
7
8
  class DateTimeFormatter
9
+ FormatterRegistry.add(:date_time, self)
10
+
11
+ # @!attribute [r] format
12
+ # @return [String, nil] The strftime format string, or nil for ISO-8601 default.
8
13
  attr_reader :format
9
14
 
10
- # @param [String] format The format to use when formatting the date/time object.
15
+ # @param format [String, nil] The format to use when formatting the date/time object.
16
+ # If nil, uses ISO-8601 format with microsecond precision.
11
17
  def initialize(format = nil)
12
18
  @format = format.dup.to_s.freeze unless format.nil?
13
19
  end
14
20
 
21
+ # Format a date/time object using the configured format.
22
+ #
23
+ # @param obj [Date, Time, DateTime, Object] The object to format. Should respond to
24
+ # strftime (for custom format) or iso8601 (for default format).
25
+ # @return [String] The formatted date/time string.
15
26
  def call(obj)
16
27
  if @format && obj.respond_to?(:strftime)
17
28
  obj.strftime(@format)
@@ -3,18 +3,28 @@
3
3
  module Lumberjack
4
4
  class Formatter
5
5
  # Format an exception including the backtrace. You can specify an object that
6
- # responds to `call` as a backtrace cleaner. The exception backtrace will be
6
+ # responds to +call+ as a backtrace cleaner. The exception backtrace will be
7
7
  # passed to this object and the returned array is what will be logged. You can
8
8
  # use this to clean out superfluous lines.
9
9
  class ExceptionFormatter
10
+ FormatterRegistry.add(:exception, self)
11
+
12
+ # @!attribute [rw] backtrace_cleaner
13
+ # @return [#call, nil] An object that responds to +call+ and takes
14
+ # an array of strings (the backtrace) and returns an array of strings.
10
15
  attr_accessor :backtrace_cleaner
11
16
 
12
- # @param [#call] backtrace_cleaner An object that responds to `call` and takes
17
+ # @param backtrace_cleaner [#call, nil] An object that responds to +call+ and takes
13
18
  # an array of strings (the backtrace) and returns an array of strings (the
19
+ # cleaned backtrace).
14
20
  def initialize(backtrace_cleaner = nil)
15
21
  self.backtrace_cleaner = backtrace_cleaner
16
22
  end
17
23
 
24
+ # Format an exception with its message and backtrace.
25
+ #
26
+ # @param exception [Exception] The exception to format.
27
+ # @return [String] The formatted exception with class name, message, and backtrace.
18
28
  def call(exception)
19
29
  message = +"#{exception.class.name}: #{exception.message}"
20
30
  trace = exception.backtrace
@@ -5,12 +5,24 @@ module Lumberjack
5
5
  # Format an object that has an id as a hash with keys for class and id. This formatter is useful
6
6
  # as a default formatter for objects pulled from a data store. By default it will use :id as the
7
7
  # id attribute.
8
+ #
9
+ # The formatter creates a standardized representation of objects by extracting their class name
10
+ # and identifier, making it easy to identify objects in logs without exposing all their data.
11
+ # This is particularly useful for ActiveRecord models, database objects, or any objects that
12
+ # have a unique identifier attribute.
8
13
  class IdFormatter
9
- # @param [Symbol, String] id_attribute The attribute to use as the id.
14
+ FormatterRegistry.add(:id, self)
15
+
16
+ # @param id_attribute [Symbol, String] The attribute to use as the id (defaults to :id).
10
17
  def initialize(id_attribute = :id)
11
18
  @id_attribute = id_attribute
12
19
  end
13
20
 
21
+ # Format an object by extracting its class name and ID attribute.
22
+ #
23
+ # @param obj [Object] The object to format. Must respond to the configured ID attribute.
24
+ # @return [Hash, String] A hash with "class" and "id" keys if the object has the ID attribute,
25
+ # otherwise returns the object's string representation.
14
26
  def call(obj)
15
27
  if obj.respond_to?(@id_attribute)
16
28
  id = obj.send(@id_attribute)
@@ -2,8 +2,21 @@
2
2
 
3
3
  module Lumberjack
4
4
  class Formatter
5
- # Format an object by calling +inspect+ on it.
5
+ # Format an object by calling +inspect+ on it. This formatter provides
6
+ # a debugging-friendly representation of objects, showing their internal
7
+ # structure and contents in a readable format.
8
+ #
9
+ # The InspectFormatter is particularly useful for logging complex objects
10
+ # where you need to see their complete state, such as arrays, hashes,
11
+ # or custom objects. It relies on Ruby's built-in +inspect+ method,
12
+ # which provides detailed object representations.
6
13
  class InspectFormatter
14
+ FormatterRegistry.add(:inspect, self)
15
+
16
+ # Convert an object to its inspect representation.
17
+ #
18
+ # @param obj [Object] The object to format.
19
+ # @return [String] The inspect representation of the object.
7
20
  def call(obj)
8
21
  obj.inspect
9
22
  end