lib/lumberjack/formatter.rb
# frozen_string_literal: true module Lumberjack # This class controls the conversion of log entry messages into a loggable format. This allows you # to log any object you want and have the logging system deal with converting it into a string. # # Formats are added to a Formatter by associating them with a class using the +add+ method. Formats # are any object that responds to the +call+ method. # # By default, all object will be converted to strings using their inspect method except for Strings # and Exceptions. Strings are not converted and Exceptions are converted using the ExceptionFormatter. # # Enumerable objects (including Hash and Array) will call the formatter recursively for each element. class Formatter require_relative "formatter/date_time_formatter" require_relative "formatter/exception_formatter" require_relative "formatter/id_formatter" require_relative "formatter/inspect_formatter" require_relative "formatter/object_formatter" require_relative "formatter/pretty_print_formatter" require_relative "formatter/string_formatter" require_relative "formatter/strip_formatter" require_relative "formatter/structured_formatter" require_relative "formatter/truncate_formatter" class << self # Returns a new empty formatter with no mapping. For historical reasons, a formatter # is initialized with mappings to help output objects as strings. This will return one # without the default mappings. # # @return [Lumberjack::Formatter] a new empty formatter def empty new.clear end end def initialize @class_formatters = {} @module_formatters = {} structured_formatter = StructuredFormatter.new(self) add([String, Numeric, TrueClass, FalseClass], :object) add(Object, InspectFormatter.new) add(Exception, :exception) add(Enumerable, structured_formatter) end # Add a formatter for a class. The formatter can be specified as either an object # that responds to the +call+ method or as a symbol representing one of the predefined # formatters, or as a block to the method call. # # The predefined formatters are: # - :date_time # - :exception # - :id # - :inspect # - :object # - :pretty_print # - :string # - :strip # - :structured # - :truncate # # You can add multiple classes at once by passing an array of classes. # # You can also pass class names as strings instead of the classes themselves. This can # help avoid loading dependency issues. This applies only to classes; modules cannot be # passed in as strings. # # @param [Class, Module, String, Array<Class, Module, String>] klass The class or module to add a formatter for. # @param [Symbol, Class, String, #call] formatter The formatter to use for the class. # If a symbol is passed in, it will be used to load one of the predefined formatters. # If a class is passed in, it will be initialized with the args passed in. # Otherwise, the object will be used as the formatter and must respond to call method. # @param [Array] args Arguments to pass to the formatter when it is initialized. # @yield [obj] A block that will be used as the formatter for the class. # @yieldparam [Object] obj The object to format. # @yieldreturn [String] The formatted string. # @return [self] Returns itself so that add statements can be chained together. # # @example # # # Use a predefined formatter # formatter.add(MyClass, :pretty_print) # # # Pass in a formatter object # formatter.add(MyClass, Lumberjack::Formatter::PrettyPrintFormatter.new) # # # Use a block # formatter.add(MyClass){|obj| obj.humanize} # # # Add statements can be chained together # formatter.add(MyClass, :pretty_print).add(YourClass){|obj| obj.humanize} def add(klass, formatter = nil, *args, &block) formatter ||= block if formatter.nil? remove(klass) else formatter_class_name = nil if formatter.is_a?(Symbol) formatter_class_name = "#{formatter.to_s.gsub(/(^|_)([a-z])/) { |m| $~[2].upcase }}Formatter" elsif formatter.is_a?(String) formatter_class_name = formatter end if formatter_class_name formatter = Formatter.const_get(formatter_class_name) end if formatter.is_a?(Class) formatter = formatter.new(*args) end Array(klass).each do |k| if k.instance_of?(Module) @module_formatters[k] = formatter else k = k.name if k.is_a?(Class) @class_formatters[k] = formatter end end end self end # Remove the formatter associated with a class. Remove statements can be chained together. # # You can remove multiple classes at once by passing an array of classes. # # You can also pass class names as strings instead of the classes themselves. This can # help avoid loading dependency issues. This applies only to classes; modules cannot be # passed in as strings. # # @param [Class, Module, String, Array<Class, Module, String>] klass The class or module to remove the formatters for. # @return [self] Returns itself so that remove statements can be chained together. def remove(klass) Array(klass).each do |k| if k.instance_of?(Module) @module_formatters.delete(k) else k = k.name if k.is_a?(Class) @class_formatters.delete(k) end end self end # Remove all formatters including the default formatter. Can be chained to add method calls. # # @return [self] Returns itself so that clear statements can be chained together. def clear @class_formatters.clear @module_formatters.clear self end # Format a message object by applying all formatters attached to it. # # @param [Object] message The message object to format. # @return [Object] The formatted object. def format(message) formatter = formatter_for(message.class) if formatter&.respond_to?(:call) formatter.call(message) else message end end # Compatibility with the Logger::Formatter signature. This method will just convert the message # object to a string and ignores the other parameters. # # @param [Integer, String, Symbol] severity The severity of the message. # @param [Time] timestamp The time the message was logged. # @param [String] progname The name of the program logging the message. # @param [Object] msg The message object to format. def call(severity, timestamp, progname, msg) "#{format(msg)}#{Lumberjack::LINE_SEPARATOR}" end private # Find the formatter for a class by looking it up using the class hierarchy. def formatter_for(klass) # :nodoc: check_modules = true until klass.nil? formatter = @class_formatters[klass.name] return formatter if formatter if check_modules _, formatter = @module_formatters.detect { |mod, f| klass.include?(mod) } check_modules = false return formatter if formatter end klass = klass.superclass end end end end