File: entry_formatter.rb

package info (click to toggle)
ruby-lumberjack 2.0.4-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 956 kB
  • sloc: ruby: 7,957; makefile: 2
file content (357 lines) | stat: -rw-r--r-- 16,206 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# frozen_string_literal: true

module Lumberjack
  # EntryFormatter provides a unified interface for formatting complete log entries by combining
  # message formatting and attribute formatting into a single, coordinated system.
  #
  # This class serves as the central formatting coordinator in the Lumberjack logging pipeline,
  # bringing together two specialized formatters:
  # 1. Message Formatter ({Lumberjack::Formatter}) - Formats the main log message content
  # 2. Attribute Formatter ({Lumberjack::AttributeFormatter}) - Formats key-value attribute pairs
  #
  # @example Complete entry formatting setup
  #   entry_formatter = Lumberjack::EntryFormatter.build do |formatter|
  #     # Formatter will be used in both log messages and attributes
  #     formatter.format_class(ActiveRecord::Base, :id)
  #
  #     # Message specific formatters
  #     formatter.format_message(Exception, :exception)
  #     formatter.format_message(Time, :date_time, "%Y-%m-%d %H:%M:%S")
  #
  #     # Attribute specific formatters
  #     formatter.format_attributes(Time, :date_time, "%Y-%m-%d")
  #     formatter.format_attribute_name("password") { |value| "[REDACTED]" }
  #     formatter.format_attribute_name("user_id", :id)
  #     formatter.default_attribute_format { |value| value.to_s.strip }
  #   end
  #
  # @example Using with a logger
  #   logger = Lumberjack::Logger.new(STDOUT, formatter: entry_formatter)
  #   logger.info("User login", user: user_object, timestamp: Time.now)
  #   # Both the message and attributes are formatted according to the rules
  #
  # @see Lumberjack::Formatter
  # @see Lumberjack::AttributeFormatter
  # @see Lumberjack::Logger
  class EntryFormatter
    # The message formatter used to format log message content.
    # @return [Lumberjack::Formatter] The message formatter instance.
    attr_reader :message_formatter

    # The attribute formatter used to format log entry attributes.
    # @return [Lumberjack::AttributeFormatter] The attribute formatter instance.
    attr_reader :attribute_formatter

    class << self
      # Build a new entry formatter using a configuration block. The block receives the new formatter
      # as a parameter, allowing you to configure it with various configuration methods.
      #
      # @param message_formatter [Lumberjack::Formatter, Symbol, nil] The message formatter to use.
      #   Can be a Formatter instance, :default for standard formatter, :none for empty formatter, or nil.
      # @param attribute_formatter [Lumberjack::AttributeFormatter, nil] The attribute formatter to use.
      # @yield [formatter] A block that configures the entry formatter.
      # @return [Lumberjack::EntryFormatter] A new configured entry formatter.
      #
      # @example
      #   formatter = Lumberjack::EntryFormatter.build do |config|
      #     config.add(User, :id)  # Message formatting
      #     config.add(Time, :date_time, "%Y-%m-%d")
      #
      #     config.add_attribute("password") { "[REDACTED]" } # ensure passwords are not logged
      #     config.add_attribute_class(Exception) { |e| {error: e.class.name, message: e.message} }
      #   end
      def build(message_formatter: nil, attribute_formatter: nil, &block)
        formatter = new(message_formatter: message_formatter, attribute_formatter: attribute_formatter)
        block&.call(formatter)
        formatter
      end
    end

    # Create a new entry formatter with the specified message and attribute formatters.
    #
    # @param message_formatter [Lumberjack::Formatter, Symbol, nil] The message formatter to use:
    #   - Formatter instance: Used directly
    #   - :default or nil: Creates a new Formatter with default mappings
    #   - :none: Creates an empty Formatter with no default mappings
    # @param attribute_formatter [Lumberjack::AttributeFormatter, nil] The attribute formatter to use.
    #   If nil, no attribute formatting will be performed unless configured later.
    def initialize(message_formatter: nil, attribute_formatter: nil)
      self.message_formatter = message_formatter
      self.attribute_formatter = attribute_formatter
    end

    # Set the message formatter used to format log message content.
    #
    # @param value [Lumberjack::Formatter, Symbol, nil] The message formatter to use. If the value
    #   is :default, a standard formatter with default mappings is created. If nil, a new empty
    #   formatter is created.
    # @return [void]
    def message_formatter=(value)
      @message_formatter = if value == :default
        Lumberjack::Formatter.default
      elsif value.nil?
        Lumberjack::Formatter.new
      else
        value
      end
    end

    # Set the attribute formatter used to format log entry attributes.
    #
    # @param value [Lumberjack::AttributeFormatter, nil] The attribute formatter to use.
    #   If nil, a new empty AttributeFormatter is created.
    # @return [void]
    def attribute_formatter=(value)
      @attribute_formatter = value || AttributeFormatter.new
    end

    # Add a formatter for specific classes or modules. This method adds the formatter
    # for both log messages and attributes.
    #
    # @param classes_or_names [Class, Module, String, Array<Class, Module, String>] The class(es) to format.
    # @param formatter [Symbol, Class, #call, nil] The formatter to use.
    # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
    # @yield [obj] Block-based formatter that receives the object to format.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @example Adding formatters
    #   formatter.format_class(User, :id)  # Use ID formatter for User objects
    #   formatter.format_class(Array) { |vals| vals.join(", ") }  # Handle arrays
    #   formatter.format_class(Time, :date_time, "%Y-%m-%d")  # Custom time format only
    #
    # @see Lumberjack::Formatter#add
    def format_class(classes_or_names, formatter = nil, *args, &block)
      Array(classes_or_names).each do |class_or_name|
        unless @message_formatter.include?(class_or_name)
          @message_formatter.add(class_or_name, formatter, *args, &block)
        end

        unless @attribute_formatter.include_class?(class_or_name)
          @attribute_formatter.add_class(class_or_name, formatter, *args, &block)
        end
      end

      self
    end

    # Remove a formatter for specific classes or modules. This method removes the formatter
    # for both log messages and attributes.
    #
    # @param classes_or_names [Class, Module, String, Array<Class, Module, String>] The class(es) to remove formatters for.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::Formatter#remove
    def remove_class(classes_or_names)
      @message_formatter.remove(classes_or_names)
      @attribute_formatter.remove_class(classes_or_names)
      self
    end

    # Add a message formatter for specific classes or modules.
    #
    # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
    # @param formatter [Symbol, Class, #call, nil] The formatter to use
    # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
    # @yield [obj] Block-based formatter that receives the object to format.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::Formatter#add
    def format_message(classes_or_names, formatter = nil, *args, &block)
      @message_formatter.add(classes_or_names, formatter, *args, &block)
      self
    end

    # Remove a message formatter for specific classes or modules.
    #
    # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::Formatter#remove
    def remove_message_formatter(classes_or_names)
      @message_formatter.remove(classes_or_names)
      self
    end

    # Add a formatter for the named attribute.
    #
    # @param names [String, Symbol, Array<String, Symbol>] The attribute names to format.
    # @param formatter [Symbol, Class, #call, nil] The formatter to use
    # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
    # @yield [value] Block-based formatter that receives the attribute value.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::AttributeFormatter#add_attribute
    def format_attribute_name(names, formatter = nil, *args, &block)
      @attribute_formatter.add_attribute(names, formatter, *args, &block)
      self
    end

    # Add a formatter for the specified class or module when it appears as an attribute value.
    #
    # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
    # @param formatter [Symbol, Class, #call, nil] The formatter to use
    # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
    # @yield [value] Block-based formatter that receives the attribute value.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::AttributeFormatter#add_attribute
    def format_attributes(classes_or_names, formatter = nil, *args, &block)
      @attribute_formatter.add_class(classes_or_names, formatter, *args, &block)
      self
    end

    # Set the default attribute formatter to apply to any attributes that do not
    # have a specific formatter defined.
    #
    # @param formatter [Symbol, Class, #call, nil] The default formatter to use.
    #   If nil, removes any existing default formatter.
    # @param args [Array] Arguments to pass to the formatter constructor (when formatter is a Class).
    # @yield [value] Block-based formatter that receives the attribute value.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::AttributeFormatter#default
    def default_attribute_format(formatter = nil, *args, &block)
      @attribute_formatter.default(formatter, *args, &block)
      self
    end

    # Remove an attribute formatter for the specified attribute names.
    #
    # @param names [String, Symbol, Array<String, Symbol>] The attribute names to remove formatters for.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::AttributeFormatter#remove_attribute
    def remove_attribute_name(names)
      @attribute_formatter.remove_attribute(names)
      self
    end

    # Remove an attribute formatter for the specified classes or modules.
    #
    # @param classes_or_names [String, Module, Array<String, Module>] Class names or modules.
    # @return [Lumberjack::EntryFormatter] Returns self for method chaining.
    #
    # @see Lumberjack::AttributeFormatter#remove_class
    def remove_attribute_class(classes_or_names)
      @attribute_formatter.remove_class(classes_or_names)
      self
    end

    # Extend this formatter by adding the formats defined in the provided formatter into this one.
    #
    # @param formatter [Lumberjack::EntryFormatter] The formatter to merge.
    # @return [self] Returns self for method chaining.
    def include(formatter)
      unless formatter.is_a?(Lumberjack::EntryFormatter)
        raise ArgumentError.new("formatter must be a Lumberjack::EntryFormatter")
      end

      @message_formatter ||= Lumberjack::Formatter.new
      @message_formatter.include(formatter.message_formatter)

      @attribute_formatter ||= Lumberjack::AttributeFormatter.new
      @attribute_formatter.include(formatter.attribute_formatter)

      self
    end

    # Extend this formatter by adding the formats defined in the provided formatter into this one.
    # Formats defined in this formatter will take precedence and not be overridden.
    #
    # @param formatter [Lumberjack::EntryFormatter] The formatter to merge.
    # @return [self] Returns self for method chaining.
    def prepend(formatter)
      unless formatter.is_a?(Lumberjack::EntryFormatter)
        raise ArgumentError.new("formatter must be a Lumberjack::EntryFormatter")
      end

      @message_formatter ||= Lumberjack::Formatter.new
      @message_formatter.prepend(formatter.message_formatter)

      @attribute_formatter ||= Lumberjack::AttributeFormatter.new
      @attribute_formatter.prepend(formatter.attribute_formatter)

      self
    end

    # Format a complete log entry by applying both message and attribute formatting.
    # This is the main method that coordinates the formatting of both the message content
    # and any associated attributes.
    #
    # @param message [Object, Proc, nil] The log message to format. Can be any object, a Proc that returns the message, or nil.
    # @param attributes [Hash, nil] The log entry attributes to format.
    # @return [Array<Object, Hash>] A two-element array containing [formatted_message, formatted_attributes].
    def format(message, attributes)
      message = message.call if message.is_a?(Proc)
      message = message_formatter.format(message) if message_formatter.respond_to?(:format)

      message_attributes = nil
      if message.is_a?(MessageAttributes)
        message_attributes = message.attributes
        message = message.message
      end
      message_attributes = Utils.flatten_attributes(message_attributes) if message_attributes

      attributes = merge_attributes(attributes, message_attributes) if message_attributes
      attributes = AttributesHelper.expand_runtime_values(attributes)
      attributes = attribute_formatter.format(attributes) if attributes && attribute_formatter

      [message, attributes]
    end

    # Compatibility method for Ruby's standard Logger::Formatter interface. This delegates
    # to the message formatter's call method for basic Logger compatibility.
    #
    # @param severity [Integer, String, Symbol] The log severity (passed to message formatter).
    # @param timestamp [Time] The log timestamp (passed to message formatter).
    # @param progname [String] The program name (passed to message formatter).
    # @param msg [Object] The message object to format (passed to message formatter).
    # @return [String, nil] The formatted message string, or nil if no message formatter.
    #
    # @see Lumberjack::Formatter#call
    def call(severity, timestamp, progname, msg)
      message_formatter&.call(severity, timestamp, progname, msg)
    end

    private

    # Merge two attribute hashes, handling nil values gracefully.
    # Used to combine explicit log attributes with attributes embedded in MessageAttributes objects.
    #
    # @param current_attributes [Hash, nil] The primary attributes hash.
    # @param attributes [Hash, nil] Additional attributes to merge in.
    # @return [Hash, nil] The merged attributes hash, or nil if both inputs are nil/empty.
    # @api private
    def merge_attributes(current_attributes, attributes)
      if current_attributes.nil? || current_attributes.empty?
        attributes
      elsif attributes.nil?
        current_attributes
      else
        current_attributes.merge(attributes)
      end
    end

    # Check if a formatter accepts an attributes parameter in its call method.
    # This is used for determining formatter compatibility but is currently unused (TODO).
    #
    # @param formatter [#call] The formatter to check.
    # @return [Boolean] true if the formatter accepts 5+ parameters or has a splat parameter.
    # @api private
    # @todo This method needs to be integrated into the logger functionality.
    def accepts_attributes_parameter?(formatter)
      method_obj = if formatter.is_a?(Proc)
        formatter
      elsif formatter.respond_to?(:call)
        formatter.method(:call)
      end
      return false unless method_obj

      params = method_obj.parameters
      positional = params.slice(:req, :opt)
      has_splat = params.any? { |type, _| type == :rest }
      positional_count = positional.size
      positional_count >= 5 || has_splat
    end
  end
end