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,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
|
-
#
|
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]
|
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
|
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]
|
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
|
-
|
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
|