rudionrails/yell

View on GitHub
lib/yell/formatter.rb

Summary

Maintainability
A
1 hr
Test Coverage
require 'time'

# TODO: Register custom formats
#
# @example The Yell default fomat
#   Yell::Formatter.register(:default)
#
# @example The Ruby standard logger format
#   Yell::Formatter.register(:stdlogger, "%l, [%d #%p] %5L -- : %m", "%Y-%m-%dT%H:%M:%S.%6N")
#
module Yell #:nodoc:

  # No format on the log message
  #
  # @example
  #   logger = Yell.new STDOUT, format: false
  #   logger.info "Hello World!"
  #   #=> "Hello World!"
  NoFormat = "%m"

  # Default Format
  #
  # @example
  #   logger = Yell.new STDOUT, format: Yell::DefaultFormat
  #   logger.info "Hello World!"
  #   #=> "2012-02-29T09:30:00+01:00 [ INFO] 65784 : Hello World!"
  #   #    ^                         ^       ^       ^
  #   #    ISO8601 Timestamp         Level   Pid     Message
  DefaultFormat = "%d [%5L] %p : %m"

  # Basic Format
  #
  # @example
  #   logger = Yell.new STDOUT, format: Yell::BasicFormat
  #   logger.info "Hello World!"
  #   #=> "I, 2012-02-29T09:30:00+01:00 : Hello World!"
  #   #    ^  ^                          ^
  #   #    ^  ISO8601 Timestamp          Message
  #   #    Level (short)
  BasicFormat = "%l, %d : %m"

  # Extended Format
  #
  # @example
  #   logger = Yell.new STDOUT, format: Yell::ExtendedFormat
  #   logger.info "Hello World!"
  #   #=> "2012-02-29T09:30:00+01:00 [ INFO] 65784 localhost : Hello World!"
  #   #    ^                          ^      ^     ^           ^
  #   #    ISO8601 Timestamp          Level  Pid   Hostname    Message
  ExtendedFormat  = "%d [%5L] %p %h : %m"


  # The +Formatter+ provides a handle to configure your log message style.
  class Formatter

    Table = {
      "m" => "message(event.messages)",    # Message
      "l" => "level(event.level, 1)",      # Level (short), e.g.'I', 'W'
      "L" => "level(event.level)",         # Level, e.g. 'INFO', 'WARN'
      "d" => "date(event.time)",           # ISO8601 Timestamp
      "h" => "event.hostname",             # Hostname
      "p" => "event.pid",                  # PID
      "P" => "event.progname",             # Progname
      "t" => "event.thread_id",            # Thread ID
      "F" => "event.file",                 # Path with filename where the logger was called
      "f" => "File.basename(event.file)",  # Filename where the loger was called
      "M" => "event.method",               # Method name where the logger was called
      "n" => "event.line",                 # Line where the logger was called
      "N" => "event.name"                  # Name of the logger
    }

    # For standard formatted backwards compatibility
    LegacyTable = Hash[ Table.keys.map { |k| [k, 'noop'] } ].merge(
      'm' => 'message(msg)',
      'l' => 'level(event, 1)',
      'L' => 'level(event)',
      'd' => 'date(time)',
      "p" => "$$",
      'P' => 'progname'
    )

    PatternMatcher = /([^%]*)(%\d*)?(#{Table.keys.join('|')})?(.*)/m


    attr_reader :pattern, :date_pattern


    # Initializes a new +Yell::Formatter+.
    #
    # Upon initialization it defines a format method. `format` takes
    # a {Yell::Event} instance as agument in order to apply for desired log
    # message formatting.
    #
    # @example Blank formatter
    #   Formatter.new
    #
    # @example Formatter with a message pattern
    #   Formatter.new("%d [%5L] %p : %m")
    #
    # @example Formatter with a message and date pattern
    #   Formatter.new("%d [%5L] %p : %m", "%D %H:%M:%S.%L")
    #
    # @example Formatter with a message modifier
    #   Formatter.new do |f|
    #     f.modify(Hash) { |h| "Hash: #{h.inspect}" }
    #   end
    def initialize( *args, &block )
      builder = Builder.new(*args, &block)

      @pattern = builder.pattern
      @date_pattern = builder.date_pattern
      @modifier = builder.modifier

      define_date_method!
      define_call_method!
    end

    # Get a pretty string
    def inspect
      "#<#{self.class.name} pattern: #{@pattern.inspect}, date_pattern: #{@date_pattern.inspect}>"
    end


    private

    # Message modifier class to allow different modifiers for different requirements.
    class Modifier
      def initialize
        @repository = {}
      end

      def set( key, &block )
        @repository.merge!(key => block)
      end

      def call( message )
        case
        when mod = @repository[message.class] || @repository[message.class.to_s]
          mod.call(message)
        when message.is_a?(Array)
          message.map { |m| call(m) }.join(" ")
        when message.is_a?(Hash)
          message.map { |k, v| "#{k}: #{v}" }.join(", ")
        when message.is_a?(Exception)
          backtrace = message.backtrace ? "\n\t#{message.backtrace.join("\n\t")}" : ""
          sprintf("%s: %s%s", message.class, message.message, backtrace)
        else
          message
        end
      end
    end

    # Builder class to allow setters that won't be accessible once
    # transferred to the Formatter
    class Builder
      attr_accessor :pattern, :date_pattern
      attr_reader :modifier

      def initialize( pattern = nil, date_pattern = nil, &block )
        @modifier = Modifier.new

        @pattern = case pattern
        when false then Yell::NoFormat
        when nil then Yell::DefaultFormat
        else pattern
        end.dup

        @pattern << "\n" unless @pattern[-1] == ?\n # add newline if not present
        @date_pattern = date_pattern || :iso8601

        block.call(self) if block
      end

      def modify( key, &block )
        modifier.set(key, &block)
      end
    end

    def define_date_method!
      buf = case @date_pattern
      when String then "t.strftime(@date_pattern)"
      when Symbol then respond_to?(@date_pattern, true) ? "#{@date_pattern}(t)" : "t.#{@date_pattern}"
      else t.iso8601
      end

      # define the method
      instance_eval <<-METHOD, __FILE__, __LINE__
        def date(t = Time.now)
          #{buf}
        end
       METHOD
    end

    # define a standard +Logger+ backwards compatible #call method for the formatter
    def define_call_method!
      instance_eval <<-METHOD, __FILE__, __LINE__
        def call(event, time = nil, progname = nil, msg = nil)
          event.is_a?(Yell::Event) ? #{to_sprintf(Table)} : #{to_sprintf(LegacyTable)}
        end
      METHOD
    end

    def to_sprintf( table )
      buff, args, _pattern = "", [], @pattern.dup

      while true
        match = PatternMatcher.match(_pattern)

        buff << match[1] unless match[1].empty?
        break if match[2].nil?

        buff << match[2] + 's'
        args << table[ match[3] ]

        _pattern = match[4]
      end

      %Q{sprintf("#{buff.gsub(/"/, '\"')}", #{args.join(', ')})}
    end

    def level( sev, length = nil )
      severity = case sev
      when Integer then Yell::Severities[sev] || 'ANY'
      else sev
      end

      length.nil? ? severity : severity[0, length]
    end

    def message( messages )
      @modifier.call(messages.is_a?(Array) && messages.size == 1 ? messages.first : messages)
    end

    # do nothing
    def noop
      ''
    end

  end
end