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
|