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