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
data/UPGRADE_GUIDE.md ADDED
@@ -0,0 +1,80 @@
1
+ # Lumberjack Upgrade Guide
2
+
3
+ Version 2.0 is a major update to the framework with several changes to the public API.
4
+
5
+ ## Constructor
6
+
7
+ `Lumberjack::Logger` now takes keyword arguments instead of an options hash in order to be compatible with the standard library `Logger` class. If you were previously using an options hash, you will need to double splat the hash to convert them to keyword arguments.
8
+
9
+ ```ruby
10
+ logger = Lumberjack::Logger.new(stream, **options)
11
+ ```
12
+
13
+ The default log level is now DEBUG instead of INFO.
14
+
15
+ ## Log Files
16
+
17
+ One of the original goals of Lumberjack was to properly handle rotating log files in a multi-process, production environment. The standard library `Logger` class in modern versions of Ruby now does this properly, so log rotation devices have been removed from Lumberjack.
18
+
19
+ The `:roll` and `:max_size` constructor options are no longer used. Log file rotation is specified with the same constructor arguments that standard library `Logger` class uses.
20
+
21
+ ```ruby
22
+ # Rotate the logs daily
23
+ logger = Lumberjack::Logger.new(stream, :daily)
24
+
25
+ # Rotate the logs when they reach 10MB and keeping the last 4 files
26
+ logger = Lumberjack::Logger.new(stream, 4, 10 * 1024 * 1024)
27
+ ```
28
+
29
+ These devices have been removed:
30
+
31
+ - `Lumberjack::Device::LogFile`
32
+ - `Lumberjack::Device::RollingLogFile`
33
+ - `Lumberjack::Device::SizeRollingLogFile`
34
+ - `Lumberjack::Device::DateRollingLogFile`
35
+
36
+ ## Attributes
37
+
38
+ Tags have been renamed "attributes" to keep inline with terminology used in other logging frameworks.
39
+
40
+ The method name `tag` is still used as the main interface as verb (i.e. "to tag logs with attributes").
41
+
42
+ ```ruby
43
+ logger.tag(attributes) do
44
+ logger.info("Something happened")
45
+ end
46
+ ```
47
+
48
+ Internal uses of the word "tag" have all been updated to use "attribute" instead. The "tag" versions of the methods will still work, but they have been [marked as deprecated](CHANGELOG.md#deprecated) and will be removed in a future version.
49
+
50
+ Global attributes are now set with the `tag!` method instead of `tag_globally` or calling `tag` outside of a context.
51
+
52
+ ```ruby
53
+ logger.tag!(host: Lumberjack::Utils.hostname)
54
+ ```
55
+
56
+ ## Rails Integration
57
+
58
+ Rails has it's own extensions for logging. The support for these has been removed from the main `lumberjack` gem and moved to the `lumberjack_rails` gem. This change allows for much better support for integrating Lumberjack into the Rails ecosystem.
59
+
60
+ Using the `tagged` method in Rails will now add the tags to the `"tags"` attribute. Previously it had added it to the `"tagged"` attribute.
61
+
62
+ ## Templates
63
+
64
+ Templates used for writing to streams now use mustache syntax (i.e. `{{message}}` instead of `:message`). The field names are all the same except `:tags` should be replace with `{{attributes}}`. There are also options for how to format the severity on a log entry (see `Lumberjack::Template` for details).
65
+
66
+ ## Formatters
67
+
68
+ Message formatters and attribute formatters have been unified under a single `Lumberjack::EntryFormatter` class. This class supports a builder pattern so it is much easier to define custom formats for classes or attributes in the logs. You should now pass an entry formatter in the `Lumberjack::Logger` constructor `:formatter` option instead of a `Lumberjack::Formatter`.
69
+
70
+ The default formatter has also been removed. This created problems when creating custom formats. You can use the old default formatter by passing `formatter: :default` to the logger constructor.
71
+
72
+ ## Deprecation Warnings
73
+
74
+ Lumberjack will print deprecation warnings to standard error when deprecated methods are used. If you want to suppress these warnings, set `Lumberjack.deprecation_mode` to `:silent`.
75
+
76
+ For performance reasons, deprecation warnings will only be shown the first time a deprecated method is called. You can show all instances where a method is called by setting `Lumberjack.deprecation_mode` to `:verbose`.
77
+
78
+ You can also raise an exception when a deprecated method is called by setting `Lumberjack.deprecation_mode` to `:raise` in your test suite.
79
+
80
+ You can also set the deprecation mode with the `LUMBERJACK_DEPRECATION_WARNINGS` environment variable.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.2
1
+ 2.0.0
@@ -0,0 +1,451 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # AttributeFormatter provides flexible formatting control for log entry attributes (key-value pairs).
5
+ # It allows you to specify different formatting rules for attribute names, object classes, or
6
+ # provide a default formatter for all attributes.
7
+ #
8
+ # The formatter system works in a hierarchical manner:
9
+ # 1. Attribute-specific formatters - Applied to specific attribute names (highest priority)
10
+ # 2. Class-specific formatters - Applied based on the attribute value's class
11
+ # 3. Default formatter - Applied to all other attributes (lowest priority)
12
+ #
13
+ # Formatters can be specified as:
14
+ #
15
+ # - Lumberjack::Formatter objects: Full formatter instances with complex logic
16
+ # - Callable objects: Any object responding to +#call(value)+
17
+ # - Blocks: Inline formatting logic
18
+ # - Symbols: References to predefined formatter classes (e.g., +:strip+, +:truncate+)
19
+ #
20
+ # @example Basic usage with build
21
+ # formatter = Lumberjack::AttributeFormatter.build do |config|
22
+ # config.add_attribute(["password", "secret", "token"]) { |value| "[REDACTED]" }
23
+ # config.add_attribute("user.email") { |email| email.downcase }
24
+ # config.add_class(Time, :date_time, "%Y-%m-%d %H:%M:%S")
25
+ # end
26
+ #
27
+ # If the value returned by a formatter is a +Lumberjack::RemapAttributes+ instance, then
28
+ # the attributes will be remapped to the new attributes.
29
+ #
30
+ # @example
31
+ # formatter = Lumberjack::AttributeFormatter.new
32
+ # formatter.add_attribute("duration_ms") { |value| Lumberjack::RemapAttributes.new(duration: value.to_f / 1000) }
33
+ # formatter.format({ "duration_ms" => 1234 }) # => { "duration" => 1.234 }
34
+ #
35
+ # @see Lumberjack::Formatter
36
+ # @see Lumberjack::EntryFormatter
37
+ class AttributeFormatter
38
+ class << self
39
+ # Build a new attribute formatter using a configuration block. The block receives the
40
+ # new formatter as a parameter, allowing you to configure it with methods like +add_attribute+,
41
+ # +add_class+, +default+, etc.
42
+ #
43
+ # @yield [formatter] A block that configures the attribute formatter.
44
+ # @return [Lumberjack::AttributeFormatter] A new configured attribute formatter.
45
+ #
46
+ # @example
47
+ # formatter = Lumberjack::AttributeFormatter.build do |config|
48
+ # config.default { |value| value.to_s.strip }
49
+ # config.add_attribute(["password", "secret"]) { |value| "[REDACTED]" }
50
+ # config.add_attribute("email") { |email| email.downcase }
51
+ # config.add_class(Time, :date_time, "%Y-%m-%d %H:%M:%S")
52
+ # end
53
+ def build(&block)
54
+ formatter = new
55
+ block&.call(formatter)
56
+ formatter
57
+ end
58
+ end
59
+
60
+ # Create a new attribute formatter with no default formatters configured.
61
+ # You'll need to add specific formatters using {#add_class}, {#add_attribute}, or {#default}.
62
+ #
63
+ # @return [Lumberjack::AttributeFormatter] A new empty attribute formatter.
64
+ def initialize
65
+ @attribute_formatter = {}
66
+ @class_formatter = Formatter.new
67
+ @default_formatter = nil
68
+ end
69
+
70
+ # Set a default formatter applied to all attribute values that don't have specific formatters.
71
+ # This serves as the fallback formatting behavior for any attributes not covered by
72
+ # attribute-specific or class-specific formatters.
73
+ #
74
+ # @param formatter [Lumberjack::Formatter, #call, Class, nil] The formatter to use.
75
+ # If nil, the block will be used as the formatter. If a class is passed, it will be
76
+ # instantiated with the args passed in.
77
+ # @param args [Array] The arguments to pass to the constructor if formatter is a Class.
78
+ # @yield [value] Block-based formatter that receives the attribute value.
79
+ # @yieldparam value [Object] The attribute value to format.
80
+ # @yieldreturn [Object] The formatted attribute value.
81
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
82
+ def default(formatter = nil, *args, &block)
83
+ formatter ||= block
84
+ formatter = dereference_formatter(formatter, args)
85
+ @default_formatter = formatter
86
+ self
87
+ end
88
+
89
+ # Remove the default formatter. After calling this, attributes without specific formatters
90
+ # will be passed through unchanged.
91
+ #
92
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
93
+ def remove_default
94
+ @default_formatter = nil
95
+ self
96
+ end
97
+
98
+ # Add formatters for specific attribute names or object classes. This is a convenience method
99
+ # that automatically delegates to {#add_class} or {#add_attribute} based on the input type.
100
+ #
101
+ # When you pass a Module/Class, it creates a class-based formatter that applies to all
102
+ # attribute values of that type. When you pass a String, it creates an attribute-specific
103
+ # formatter for that exact attribute name.
104
+ #
105
+ # Class formatters are applied recursively to nested hashes and arrays, making them
106
+ # powerful for formatting complex nested structures.
107
+ #
108
+ # @param names_or_classes [String, Module, Array<String, Module>] Attribute names or object classes.
109
+ # @param formatter [Lumberjack::Formatter, #call, Symbol, nil] The formatter to use.
110
+ # @yield [value] Block-based formatter that receives the attribute value.
111
+ # @yieldparam value [Object] The attribute value to format.
112
+ # @yieldreturn [Object] The formatted attribute value.
113
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
114
+ # @deprecated Use {#add_class} or {#add_attribute} instead.
115
+ def add(names_or_classes, formatter = nil, *args, &block)
116
+ Utils.deprecated("AttributeFormatter#add", "AttributeFormatter#add is deprecated and will be removed in version 2.1; use #add_class or #add_attribute instead.") do
117
+ Array(names_or_classes).each do |obj|
118
+ if obj.is_a?(Module)
119
+ add_class(obj, formatter, *args, &block)
120
+ else
121
+ add_attribute(obj, formatter, *args, &block)
122
+ end
123
+ end
124
+ end
125
+
126
+ self
127
+ end
128
+
129
+ # Add formatters for specific object classes. The formatter will be applied to any attribute
130
+ # value that is an instance of the registered class. This is particularly useful for formatting
131
+ # all instances of specific data types consistently across your logs.
132
+ #
133
+ # Class formatters are recursive - they will be applied to matching objects found within
134
+ # nested hashes and arrays.
135
+ #
136
+ # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
137
+ # @param formatter [Lumberjack::Formatter, #call, Symbol, Class, nil] The formatter to use.
138
+ # If a Class is provided, it will be instantiated with the provided args.
139
+ # @param args [Array] The arguments to pass to the constructor if formatter is a Class.
140
+ # @yield [value] Block-based formatter that receives the attribute value.
141
+ # @yieldparam value [Object] The attribute value to format.
142
+ # @yieldreturn [Object] The formatted attribute value.
143
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
144
+ #
145
+ # @example Time formatting
146
+ # formatter.add_class(Time, :date_time, "%Y-%m-%d %H:%M:%S")
147
+ # formatter.add_class([Date, DateTime]) { |dt| dt.strftime("%Y-%m-%d") }
148
+ def add_class(classes_or_names, formatter = nil, *args, &block)
149
+ formatter ||= block
150
+ formatter = dereference_formatter(formatter, args)
151
+
152
+ Array(classes_or_names).each do |class_or_name|
153
+ class_name = class_or_name.to_s
154
+ if formatter.nil?
155
+ @class_formatter.remove(class_name)
156
+ else
157
+ @class_formatter.add(class_name, formatter)
158
+ end
159
+ end
160
+
161
+ self
162
+ end
163
+
164
+ # Add formatters for specific attribute names. These formatters take precedence over
165
+ # class formatters and the default formatter.
166
+ #
167
+ # Supports dot notation for nested attributes (e.g., "user.profile.email"). This allows
168
+ # you to format specific values deep within nested hash structures.
169
+ #
170
+ # @param attribute_names [String, Symbol, Array<String, Symbol>] The attribute names to format.
171
+ # @param formatter [Lumberjack::Formatter, #call, Symbol, nil] The formatter to use.
172
+ # @yield [value] Block-based formatter that receives the attribute value.
173
+ # @yieldparam value [Object] The attribute value to format.
174
+ # @yieldreturn [Object] The formatted attribute value.
175
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
176
+ #
177
+ # @example Basic attribute formatting
178
+ # formatter.add_attribute("password") { |pwd| "[REDACTED]" }
179
+ # formatter.add_attribute("email") { |email| email.downcase }
180
+ #
181
+ # @example Nested attribute formatting
182
+ # formatter.add_attribute("user.profile.email") { |email| email.downcase }
183
+ # formatter.add_attribute("config.database.password") { "[HIDDEN]" }
184
+ #
185
+ # @example Multiple attributes
186
+ # formatter.add_attribute(["secret", "token", "api_key"]) { "[REDACTED]" }
187
+ def add_attribute(attribute_names, formatter = nil, *args, &block)
188
+ formatter ||= block
189
+ formatter = dereference_formatter(formatter, args)
190
+
191
+ Array(attribute_names).collect(&:to_s).each do |attribute_name|
192
+ if attribute_name.is_a?(Module)
193
+ raise ArgumentError.new("attribute_name cannot be a Module/Class; use #add_class to add class-based formatters")
194
+ end
195
+
196
+ if formatter.nil?
197
+ @attribute_formatter.delete(attribute_name)
198
+ else
199
+ @attribute_formatter[attribute_name] = formatter
200
+ end
201
+ end
202
+
203
+ self
204
+ end
205
+
206
+ # Remove formatters for specific attribute names or classes. This reverts the specified
207
+ # attributes or classes to use the default formatter (if configured) or no formatting.
208
+ #
209
+ # @param names_or_classes [String, Module, Array<String, Module>] Attribute names or classes
210
+ # to remove formatters for.
211
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
212
+ # @deprecated Use {#remove_class} or {#remove_attribute} instead.
213
+ def remove(names_or_classes)
214
+ Utils.deprecated("AttributeFormatter#remove", "AttributeFormatter#remove is deprecated and will be removed in version 2.1; use #remove_class or #remove_attribute instead.") do
215
+ Array(names_or_classes).each do |key|
216
+ if key.is_a?(Module)
217
+ @class_formatter.remove(key)
218
+ else
219
+ @attribute_formatter.delete(key.to_s)
220
+ end
221
+ end
222
+ end
223
+ self
224
+ end
225
+
226
+ # Remove formatters for specific object classes. This reverts the specified classes
227
+ # to use the default formatter (if configured) or no formatting.
228
+ #
229
+ # @param classes_or_names [String, Module, Array<String, Module>] The classes or names to remove.
230
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
231
+ def remove_class(classes_or_names)
232
+ Array(classes_or_names).each do |class_or_name|
233
+ @class_formatter.remove(class_or_name)
234
+ end
235
+ self
236
+ end
237
+
238
+ # Remove formatters for specific attribute names. This reverts the specified attributes
239
+ # to use the default formatter (if configured) or no formatting.
240
+ #
241
+ # @param attribute_names [String, Symbol, Array<String, Symbol>] The attribute names to remove.
242
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
243
+ def remove_attribute(attribute_names)
244
+ Array(attribute_names).collect(&:to_s).each do |attribute_name|
245
+ @attribute_formatter.delete(attribute_name)
246
+ end
247
+ self
248
+ end
249
+
250
+ # Extend this formatter by merging the formats defined in the provided formatter into this one.
251
+ #
252
+ # @param formatter [Lumberjack::AttributeFormatter] The formatter to merge.
253
+ # @return [self] Returns self for method chaining.
254
+ def include(formatter)
255
+ unless formatter.is_a?(Lumberjack::AttributeFormatter)
256
+ raise ArgumentError.new("formatter must be a Lumberjack::AttributeFormatter")
257
+ end
258
+
259
+ @class_formatter.include(formatter.instance_variable_get(:@class_formatter))
260
+ @attribute_formatter.merge!(formatter.instance_variable_get(:@attribute_formatter))
261
+
262
+ default_formatter = formatter.instance_variable_get(:@default_formatter)
263
+ @default_formatter = default_formatter if default_formatter
264
+
265
+ self
266
+ end
267
+
268
+ # Extend this formatter by merging the formats defined in the provided formatter into this one.
269
+ # Formats defined in this formatter will take precedence and not be overridden.
270
+ #
271
+ # @param formatter [Lumberjack::AttributeFormatter] The formatter to merge.
272
+ # @return [self] Returns self for method chaining.
273
+ def prepend(formatter)
274
+ unless formatter.is_a?(Lumberjack::AttributeFormatter)
275
+ raise ArgumentError.new("formatter must be a Lumberjack::AttributeFormatter")
276
+ end
277
+
278
+ @class_formatter.prepend(formatter.instance_variable_get(:@class_formatter))
279
+
280
+ formatter.instance_variable_get(:@attribute_formatter).each do |key, value|
281
+ @attribute_formatter[key] = value unless @attribute_formatter.include?(key)
282
+ end
283
+
284
+ @default_formatter ||= formatter.instance_variable_get(:@default_formatter)
285
+
286
+ self
287
+ end
288
+
289
+ # Remove all configured formatters, including the default formatter. This resets the
290
+ # formatter to a completely empty state where all attributes pass through unchanged.
291
+ #
292
+ # @return [Lumberjack::AttributeFormatter] Returns self for method chaining.
293
+ def clear
294
+ @default_formatter = nil
295
+ @attribute_formatter.clear
296
+ @class_formatter.clear
297
+ self
298
+ end
299
+
300
+ # Check if the formatter has any configured formatters (attribute, class, or default).
301
+ #
302
+ # @return [Boolean] true if no formatters are configured, false otherwise.
303
+ def empty?
304
+ @attribute_formatter.empty? && @class_formatter.empty? && @default_formatter.nil?
305
+ end
306
+
307
+ # Format a hash of attributes using the configured formatters. This is the main
308
+ # method that applies all formatting rules to transform attribute values.
309
+ #
310
+ # The formatting process follows this precedence:
311
+ # 1. Attribute-specific formatters (highest priority)
312
+ # 2. Class-specific formatters
313
+ # 3. Default formatter (lowest priority)
314
+ #
315
+ # Nested hashes and arrays are processed recursively, and dot notation attribute
316
+ # formatters are applied to nested structures.
317
+ #
318
+ # @param attributes [Hash, nil] The attributes hash to format.
319
+ # @return [Hash, nil] The formatted attributes hash, or nil if input was nil.
320
+ def format(attributes)
321
+ return nil if attributes.nil?
322
+ return attributes if empty?
323
+
324
+ formated_attributes(attributes)
325
+ end
326
+
327
+ # Get the formatter for a specific class or class name.
328
+ #
329
+ # @param klass [String, Module] The class or class name to get the formatter for.
330
+ # @return [#call, nil] The formatter for the class, or nil if not found.
331
+ def formatter_for_class(klass)
332
+ @class_formatter.formatter_for(klass)
333
+ end
334
+
335
+ # Get the formatter for a specific attribute.
336
+ #
337
+ # @param name [String, Symbol] The attribute name to get the formatter for.
338
+ # @return [#call, nil] The formatter for the attribute, or nil if not found.
339
+ def formatter_for_attribute(name)
340
+ @attribute_formatter[name.to_s]
341
+ end
342
+
343
+ # Check if a formatter exists for a specific class or class name.
344
+ #
345
+ # @param class_or_name [Class, Module, String] The class or class name to check.
346
+ # @return [Boolean] true if a formatter exists, false otherwise.
347
+ def include_class?(class_or_name)
348
+ @class_formatter.include?(class_or_name.to_s)
349
+ end
350
+
351
+ # Check if a formatter exists for a specific attribute name.
352
+ #
353
+ # @param name [String, Symbol] The attribute name to check.
354
+ # @return [Boolean] true if a formatter exists, false otherwise.
355
+ def include_attribute?(name)
356
+ @attribute_formatter.include?(name.to_s)
357
+ end
358
+
359
+ private
360
+
361
+ # Recursively format all attributes in a hash, handling nested structures.
362
+ #
363
+ # @param attributes [Hash] The attributes to format.
364
+ # @param skip_classes [Array<Class>, nil] Classes to skip during recursive formatting.
365
+ # @param prefix [String, nil] Dot notation prefix for nested attribute names.
366
+ # @return [Hash] The formatted attributes hash.
367
+ def formated_attributes(attributes, skip_classes: nil, prefix: nil)
368
+ formatted = {}
369
+
370
+ attributes.each do |name, value|
371
+ name = name.to_s
372
+ value = formatted_attribute_value(name, value, skip_classes: skip_classes, prefix: prefix)
373
+ if value.is_a?(RemapAttribute)
374
+ formatted.merge!(value.attributes)
375
+ else
376
+ formatted[name] = value
377
+ end
378
+ end
379
+
380
+ formatted
381
+ end
382
+
383
+ # Format a single attribute value using the appropriate formatter.
384
+ #
385
+ # @param name [String] The attribute name.
386
+ # @param value [Object] The attribute value to format.
387
+ # @param skip_classes [Array<Class>, nil] Classes to skip during recursive formatting.
388
+ # @param prefix [String, nil] Dot notation prefix for nested attribute names.
389
+ # @return [Object] The formatted attribute value.
390
+ def formatted_attribute_value(name, value, skip_classes: nil, prefix: nil)
391
+ prefixed_name = prefix ? "#{prefix}#{name}" : name
392
+ using_class_formatter = false
393
+
394
+ formatter = @attribute_formatter[prefixed_name]
395
+ if formatter.nil? && (skip_classes.nil? || !skip_classes.include?(value.class))
396
+ formatter = @class_formatter.formatter_for(value.class)
397
+ using_class_formatter = true if formatter
398
+ end
399
+
400
+ formatter ||= @default_formatter
401
+
402
+ formatted_value = begin
403
+ if formatter.is_a?(Lumberjack::Formatter)
404
+ formatter.format(value)
405
+ elsif formatter.respond_to?(:call)
406
+ formatter.call(value)
407
+ else
408
+ value
409
+ end
410
+ rescue SystemStackError, StandardError => e
411
+ error_message = e.class.name
412
+ error_message = "#{error_message} #{e.message}" if e.message && e.message != ""
413
+ warn("<Error formatting #{value.class.name}: #{error_message}>")
414
+ "<Error formatting #{value.class.name}: #{error_message}>"
415
+ end
416
+
417
+ if formatted_value.is_a?(MessageAttributes)
418
+ formatted_value = formatted_value.attributes
419
+ end
420
+
421
+ if formatted_value.is_a?(Enumerable)
422
+ skip_classes ||= []
423
+ skip_classes << value.class if using_class_formatter
424
+ sub_prefix = "#{prefixed_name}."
425
+
426
+ formatted_value = if formatted_value.is_a?(Hash)
427
+ formated_attributes(formatted_value, skip_classes: skip_classes, prefix: sub_prefix)
428
+ else
429
+ formatted_value.collect do |item|
430
+ formatted_attribute_value(nil, item, skip_classes: skip_classes, prefix: sub_prefix)
431
+ end
432
+ end
433
+ end
434
+
435
+ formatted_value
436
+ end
437
+
438
+ # Convert symbol formatter references to actual formatter instances.
439
+ #
440
+ # @param formatter [Symbol, Class, #call] The formatter to dereference.
441
+ # @param args [Array] The arguments to pass to the constructor if formatter is a Class.
442
+ # @return [#call] The actual formatter instance.
443
+ def dereference_formatter(formatter, args)
444
+ if formatter.is_a?(Symbol)
445
+ FormatterRegistry.formatter(formatter, *args)
446
+ else
447
+ formatter
448
+ end
449
+ end
450
+ end
451
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ # This class provides an interface for manipulating a attribute hash in a consistent manner.
5
+ class AttributesHelper
6
+ class << self
7
+ # Expand any values in a hash that are Proc's by calling them and replacing
8
+ # the value with the result. This allows setting global tags with runtime values.
9
+ #
10
+ # @param hash [Hash] The hash to transform.
11
+ # @return [Hash] The hash with string keys and expanded values.
12
+ def expand_runtime_values(hash)
13
+ return nil if hash.nil?
14
+ return hash if hash.all? { |key, value| key.is_a?(String) && !value.is_a?(Proc) }
15
+
16
+ copy = {}
17
+ hash.each do |key, value|
18
+ if value.is_a?(Proc) && (value.arity == 0 || value.arity == -1)
19
+ value = value.call
20
+ end
21
+ copy[key.to_s] = value
22
+ end
23
+ copy
24
+ end
25
+ end
26
+
27
+ def initialize(attributes)
28
+ @attributes = attributes
29
+ end
30
+
31
+ # Merge new attributes into the context attributes. Attribute values will be flattened using dot notation
32
+ # on the keys. So +{ a: { b: 'c' } }+ will become +{ 'a.b' => 'c' }+.
33
+ #
34
+ # If a block is given, then the attributes will only be added for the duration of the block.
35
+ #
36
+ # @param attributes [Hash] The attributes to set.
37
+ # @return [void]
38
+ def update(attributes)
39
+ @attributes.merge!(Utils.flatten_attributes(attributes))
40
+ end
41
+
42
+ # Get a attribute value.
43
+ #
44
+ # @param name [String, Symbol] The attribute key.
45
+ # @return [Object] The attribute value.
46
+ def [](name)
47
+ return nil if @attributes.empty?
48
+
49
+ name = name.to_s
50
+ return @attributes[name] if @attributes.include?(name)
51
+
52
+ # Check for partial matches in dot notation and return the hash representing the partial match.
53
+ prefix_key = "#{name}."
54
+ matching_attributes = {}
55
+ @attributes.each do |key, value|
56
+ if key.start_with?(prefix_key)
57
+ # Remove the prefix to get the relative key
58
+ relative_key = key[prefix_key.length..]
59
+ matching_attributes[relative_key] = value
60
+ end
61
+ end
62
+
63
+ return nil if matching_attributes.empty?
64
+
65
+ matching_attributes
66
+ end
67
+
68
+ # Set a attribute value.
69
+ #
70
+ # @param name [String, Symbol] The attribute name.
71
+ # @param value [Object] The attribute value.
72
+ # @return [void]
73
+ def []=(name, value)
74
+ if value.is_a?(Hash)
75
+ @attributes.merge!(Utils.flatten_attributes(name => value))
76
+ else
77
+ @attributes[name.to_s] = value
78
+ end
79
+ end
80
+
81
+ # Remove attributes from the context.
82
+ #
83
+ # @param names [Array<String, Symbol>] The attribute names to remove.
84
+ # @return [void]
85
+ def delete(*names)
86
+ names.each do |name|
87
+ prefix_key = "#{name}."
88
+ @attributes.delete_if { |k, _| k == name.to_s || k.start_with?(prefix_key) }
89
+ end
90
+ nil
91
+ end
92
+
93
+ # Return a copy of the attributes as a hash.
94
+ #
95
+ # @return [Hash]
96
+ def to_h
97
+ @attributes.dup
98
+ end
99
+ end
100
+ end