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
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
|
+
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
|