Permalink
c19fef5 Dec 21, 2016
@TwP @pschrammel
567 lines (509 sloc) 21.2 KB
module Logging::Layouts
# Accessor / Factory for the Pattern layout.
#
# Returns a new Pattern layout instance
def self.pattern( *args )
return ::Logging::Layouts::Pattern if args.empty?
::Logging::Layouts::Pattern.new(*args)
end
# A flexible layout configurable via a conversion pattern string.
#
# The goal of this class is to format a LogEvent and return the results as
# a String. The results depend on the conversion pattern.
#
# The conversion pattern is closely related to the conversion pattern of
# the sprintf function. A conversion pattern is composed of literal text
# and format control expressions called conversion specifiers.
#
# You are free to insert any literal text within the conversion pattern.
#
# Each conversion specifier starts with a percent sign (%) and is followed
# by optional format modifiers and a conversion character. The conversion
# character specifies the type of data, e.g. logger, level, date, thread
# ID. The format modifiers control such things as field width, padding,
# left and right justification. The following is a simple example.
#
# Let the conversion pattern be "%-5l [%c]: %m\n" and assume that the
# logging environment was set to use a Pattern layout. Then the statements
#
# root = Logging.logger[:root]
# root.debug("Message 1")
# root.warn("Message 2")
#
# would yield the output
#
# DEBUG [root]: Message 1
# WARN [root]: Message 2
#
# Note that there is no explicit separator between text and conversion
# specifiers. The pattern parser knows when it has reached the end of a
# conversion specifier when it reads a conversion character. In the example
# above the conversion specifier %-5l means the level of the logging event
# should be left justified to a width of five characters. The recognized
# conversion characters are
#
# [c] Used to output the name of the logger that generated the log
# event. Supports an optional "precision" described further below.
# [d] Used to output the date of the log event. The format of the
# date is specified using the :date_pattern option when the Layout
# is created. ISO8601 format is assumed if not date pattern is given.
# [F] Used to output the file name where the logging request was issued.
# [l] Used to output the level of the log event.
# [L] Used to output the line number where the logging request was
# issued.
# [m] Used to output the application supplied message associated with
# the log event.
# [M] Used to output the method name where the logging request was
# issued.
# [h] Used to output the hostname
# [p] Used to output the process ID of the currently running program.
# [r] Used to output the number of milliseconds elapsed from the
# construction of the Layout until creation of the log event.
# [t] Used to output the object ID of the thread that generated the
# log event.
# [T] Used to output the name of the thread that generated the log event.
# Name can be specified using Thread.current[:name] notation. Output
# empty string if name not specified. This option helps to create
# more human readable output for multi-threaded application logs.
# [X] Used to output values from the Mapped Diagnostic Context. Requires
# a key name to lookup the value from the context. More details are
# listed below.
# [x] Used to output values from the Nested Diagnostic Context. Supports
# an optional context separator string. More details are listed below.
# [%] The sequence '%%' outputs a single percent sign.
#
# The logger name directive 'c' accepts an optional precision that will
# only print the rightmost number of name space identifiers for the logger.
# By default the logger name is printed in full. For example, for the
# logger name "Foo::Bar::Baz" the pattern %c{2} will output "Bar::Baz".
#
# The directives F, L, and M will only work if the Logger generating the
# events is configured to generate tracing information. If this is not
# the case these fields will always be empty.
#
# The directives for including diagnostic context information in the log
# messages are X and x. For the Mapped Diagnostic Context the directive must
# be accompanied by the key identifying the value to insert into the log
# message. The X directive can appear multiple times to include multiple
# values from the mapped context.
#
# %X{Cookie} Insert the current session cookie
# %X{X-Session} Insert a session identifier
#
# For the Nested Diagnostic Context you need only include the directive
# once. All contexts currently in the stack will be added to the log message
# separated by spaces. If spaces are not your style, a separator string can
# be given, too.
#
# %x Insert all contexts separated by spaces
# %x{, } Insert all contexts separate by a comma and a space
#
# By default the relevant information is output as is. However, with the
# aid of format modifiers it is possible to change the minimum field width,
# the maximum field width and justification.
#
# The optional format modifier is placed between the percent sign and the
# conversion character.
#
# The first optional format modifier is the left justification flag which
# is just the minus (-) character. Then comes the optional minimum field
# width modifier. This is a decimal constant that represents the minimum
# number of characters to output. If the data item requires fewer
# characters, it is padded on either the left or the right until the
# minimum width is reached. The default is to pad on the left (right
# justify) but you can specify right padding with the left justification
# flag. The padding character is space. If the data item is larger than the
# minimum field width, the field is expanded to accommodate the data. The
# value is never truncated.
#
# This behavior can be changed using the maximum field width modifier which
# is designated by a period followed by a decimal constant. If the data
# item is longer than the maximum field, then the extra characters are
# removed from the end of the data item.
#
# Below are various format modifier examples for the category conversion
# specifier.
#
# %20c Left pad with spaces if the logger name is less than 20
# characters long
# %-20c Right pad with spaces if the logger name is less than 20
# characters long
# %.30c Truncates the logger name if it is longer than 30 characters
# %20.30c Left pad with spaces if the logger name is shorter than
# 20 characters. However, if the logger name is longer than
# 30 characters, then truncate the name.
# %-20.30c Right pad with spaces if the logger name is shorter than
# 20 characters. However, if the logger name is longer than
# 30 characters, then truncate the name.
#
# Below are examples of some conversion patterns.
#
# %.1l, [%d] %5l -- %c: %m\n
#
# This is how the Logger class in the Ruby standard library formats
# messages. The main difference will be in the date format (the Pattern
# Layout uses the ISO8601 date format). Set the :date_method on the
# Pattern Layout to be 'to_s' and then the date formats will agree.
#
class Pattern < ::Logging::Layout
# :stopdoc:
# default date format
ISO8601 = "%Y-%m-%dT%H:%M:%S".freeze
# call-seq:
# Pattern.create_date_format_methods( pl )
#
# This method will create the +date_format+ method in the given Pattern
# Layout _pl_ based on the configured date pattern and/or date method
# specified by the user.
#
def self.create_date_format_methods( pl )
code = "undef :format_date if method_defined? :format_date\n"
code << "def format_date( time )\n"
code << "time = apply_utc_offset(time)\n"
if pl.date_method.nil?
if pl.date_pattern =~ %r/%s/
code << "time.strftime('#{pl.date_pattern.gsub('%s','%6N')}')\n"
else
code << "time.strftime('#{pl.date_pattern}')\n"
end
else
code << "time.#{pl.date_method}\n"
end
code << "end\n"
::Logging.log_internal(0) {code}
pl._meta_eval(code, __FILE__, __LINE__)
end
# call-seq:
# Pattern.create_format_method( pl )
#
# This method will create the `format` method in the given Pattern
# Layout `pl` based on the configured format pattern specified by the
# user.
#
def self.create_format_method( pl )
builder = FormatMethodBuilder.new(pl)
code = builder.build_code
::Logging.log_internal(0) { code }
pl._meta_eval(code, __FILE__, __LINE__)
end
# :startdoc:
# call-seq:
# Pattern.new( opts )
#
# Creates a new Pattern layout using the following options.
#
# :pattern => "[%d] %-5l -- %c : %m\n"
# :date_pattern => "%Y-%m-%d %H:%M:%S"
# :date_method => "usec" or "to_s"
# :utc_offset => "-06:00" or -21600 or "UTC"
# :color_scheme => :default
#
# If used, :date_method will supersede :date_pattern.
#
# The :color_scheme is used to apply color formatting to the log messages.
# Individual tokens can be colorized witch the level token [%l] receiving
# distinct colors based on the level of the log event. The entire
# generated log message can also be colorized based on the level of the
# log event. See the ColorScheme documentation for more details.
#
def initialize( opts = {} )
super
@created_at = Time.now.freeze
@date_pattern = opts.fetch(:date_pattern, nil)
@date_method = opts.fetch(:date_method, nil)
@date_pattern = ISO8601 if @date_pattern.nil? && @date_method.nil?
@pattern = opts.fetch(:pattern,
"[%d] %-#{::Logging::MAX_LEVEL_LENGTH}l -- %c : %m\n")
cs_name = opts.fetch(:color_scheme, nil)
@color_scheme =
case cs_name
when false, nil; nil
when true; ::Logging::ColorScheme[:default]
else ::Logging::ColorScheme[cs_name] end
self.class.create_date_format_methods(self)
self.class.create_format_method(self)
end
attr_reader :pattern, :date_pattern, :date_method, :color_scheme
# call-seq:
# appender.pattern = "[%d] %-5l -- %c : %m\n"
#
# Set the message formatting pattern to be used by the layout.
#
def pattern=( var )
@pattern = var
Pattern.create_format_method(self)
end
# call-seq:
# appender.date_pattern = "%Y-%m-%d %H:%M:%S"
#
# Set the date formatting pattern to be used when outputting timestamps
# in the log messages.
#
def date_pattern=( var )
@date_pattern = var
Pattern.create_date_format_methods(self)
end
# call-seq:
# appender.date_method = 'to_s'
# appender.date_method = :usec
#
# Set the date method to be used when outputting timestamps in the log
# messages. If a date method is configured, the output of that method
# will be used in leu of the date pattern.
#
def date_method=( var )
@date_method = var
Pattern.create_date_format_methods(self)
end
# :stopdoc:
# Evaluates the given string of `code` if the singleton class of this
# Pattern Layout object.
#
# Returns this Pattern Layout instance.
def _meta_eval( code, file = nil, line = nil )
meta = class << self; self end
meta.class_eval code, file, line
self
end
# This class is used to build the `format` method for the Pattern layout. It
# parses the user defined pattern and emits Ruby source code (as a string)
# that can be `eval`d in the context of the Pattern layout instance.
class FormatMethodBuilder
# Matches the first directive encountered and the stuff around it.
#
# * $1 is the stuff before directive or "" if not applicable
# * $2 is the %#.# match within directive group
# * $3 is the directive letter
# * $4 is the precision specifier for the logger name
# * $5 is the stuff after the directive or "" if not applicable
DIRECTIVE_RGXP = %r/([^%]*)(?:(%-?\d*(?:\.\d+)?)([a-zA-Z%])(?:\{([^\}]+)\})?)?(.*)/m
# Arguments to sprintf keyed to directive letters
DIRECTIVE_TABLE = {
'c' => 'event.logger'.freeze,
'd' => 'format_date(event.time)'.freeze,
'F' => 'event.file'.freeze,
'l' => '::Logging::LNAMES[event.level]'.freeze,
'L' => 'event.line'.freeze,
'm' => 'format_obj(event.data)'.freeze,
'M' => 'event.method'.freeze,
'h' => "'#{Socket.gethostname}'".freeze,
'p' => 'Process.pid'.freeze,
'r' => 'Integer((event.time-@created_at)*1000).to_s'.freeze,
't' => 'Thread.current.object_id.to_s'.freeze,
'T' => 'Thread.current[:name]'.freeze,
'X' => :placeholder,
'x' => :placeholder,
'%' => :placeholder
}.freeze
# Human name aliases for directives - used for colorization of tokens
COLOR_ALIAS_TABLE = {
'c' => :logger,
'd' => :date,
'm' => :message,
'h' => :hostname,
'p' => :pid,
'r' => :time,
'T' => :thread,
't' => :thread_id,
'F' => :file,
'L' => :line,
'M' => :method,
'X' => :mdc,
'x' => :ndc
}.freeze
attr_reader :layout
attr_accessor :pattern
attr_reader :color_scheme
attr_reader :sprintf_args
attr_reader :format_string
attr_accessor :name_map_count
# Creates the format method builder and initializes some variables from
# the given Patter layout instance.
#
# pattern_layout - The Pattern Layout instance
#
def initialize( pattern_layout )
@layout = pattern_layout
@pattern = layout.pattern.dup
@color_scheme = layout.color_scheme
@sprintf_args = []
@format_string = '"'
@name_map_count = 0
end
# Returns `true` if the log messages should be colorized.
def colorize?
color_scheme && !color_scheme.lines?
end
# Returns `true` if the log messages should be colorized by line.
def colorize_lines?
color_scheme && color_scheme.lines?
end
# Returns `true` if the log levels have special colorization defined.
def colorize_levels?
color_scheme && color_scheme.levels?
end
# This method returns a String which can be `eval`d in the context of the
# Pattern layout. When it is `eval`d, a `format` method is defined in the
# Pattern layout.
#
# At the heart of the format method is `sprintf`. The conversion pattern
# specified in the Pattern layout is parsed and converted into a format
# string and corresponding arguments list. The format string and arguments
# are then processed by `sprintf` to format log events.
#
# Returns a Ruby code as a String.
def build_code
build_format_string
sprintf = "sprintf("
sprintf << format_string
sprintf << ', ' + sprintf_args.join(', ') unless sprintf_args.empty?
sprintf << ")"
if colorize_lines?
sprintf = "color_scheme.color(#{sprintf}, ::Logging::LNAMES[event.level])"
end
code = "undef :format if method_defined? :format\n"
code << "def format( event )\n#{sprintf}\nend\n"
end
# This method builds the format string used by `sprintf` to format log
# events. The conversion pattern given by the user is iteratively parsed
# by a regular expression into separate format directives. Each directive
# builds up the format string and the corresponding arguments list that
# will be formatted.
#
# The actual building of the format string is handled by separate
# directive specific methods. Those handlers also populate the arguments
# list passed to `sprintf`.
#
# Returns the format String.
def build_format_string
while true
match = DIRECTIVE_RGXP.match(pattern)
_, pre, format, directive, precision, post = *match
format_string << pre unless pre.empty?
case directive
when '%'; format_string << '%%'
when 'c'; handle_logger( format, directive, precision )
when 'l'; handle_level( format, directive, precision )
when 'X'; handle_mdc( format, directive, precision )
when 'x'; handle_ndc( format, directive, precision )
when *DIRECTIVE_TABLE.keys
handle_directives(format, directive, precision)
when nil; break
else
raise ArgumentError, "illegal format character - '#{directive}'"
end
break if post.empty?
self.pattern = post
end
format_string << '"'
end
# Add the logger name to the `format_string` and the `sprintf_args`. The
# `slice` argument is a little interesting - this is the number of logger
# name segments to keep. If we have a logger named "Foo::Bar::Baz" and our
# `slice` is 2, then "Bar::Baz" will appear in the generated log message.
# So the `slice` selects the last two parts of the logger name.
#
# format - format String
# directive - the directive character ('c')
# slice - the number of name segments to keep
#
# Returns nil
def handle_logger( format, directive, slice )
fmt = format + 's'
fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?
format_string << fmt
sprintf_args << DIRECTIVE_TABLE[directive].dup
if slice
numeric = Integer(slice) rescue nil
if numeric
raise ArgumentError, "logger name slice must be an integer greater than zero: #{numeric}" unless numeric > 0
sprintf_args.last <<
".split(::Logging::Repository::PATH_DELIMITER)" \
".last(#{slice}).join(::Logging::Repository::PATH_DELIMITER)"
else
format_string << "{#{slice}}"
end
end
nil
end
# Add the log event level to the `format_string` and the `sprintf_args`.
# The color scheme is taken into account when formatting the log event
# level.
#
# format - format String
# directive - the directive character ('l')
# precision - added back to the format string
#
# Returns nil
def handle_level( format, directive, precision )
if colorize_levels?
name_map = ::Logging::LNAMES.map { |name| color_scheme.color(("#{format}s" % name), name) }
var = "@name_map_#{name_map_count}"
layout.instance_variable_set(var.to_sym, name_map)
self.name_map_count += 1
format_string << '%s'
format_string << "{#{precision}}" if precision
sprintf_args << "#{var}[event.level]"
else
format_string << format + 's'
format_string << "{#{precision}}" if precision
sprintf_args << DIRECTIVE_TABLE[directive]
end
nil
end
# Add a Mapped Diagnostic Context to the `format_string` and the
# `sprintf_args`. Only one MDC value is added at a time, so this directive
# can appear multiple times using various keys.
#
# format - format String
# directive - the directive character ('X')
# key - which MDC value to add to the log message
#
# Returns nil
def handle_mdc( format, directive, key )
raise ArgumentError, "MDC must have a key reference" unless key
fmt = format + 's'
fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?
format_string << fmt
sprintf_args << "::Logging.mdc['#{key}']"
nil
end
# Add a Nested Diagnostic Context to the `format_string` and the
# `sprintf_args`. Since the NDC is an Array of values, the directive will
# appear only once in the conversion pattern. A `separator` is inserted
# between the values in generated log message.
#
# format - format String
# directive - the directive character ('x')
# separator - used to separate the values in the NDC array
#
# Returns nil
def handle_ndc( format, directive, separator )
fmt = format + 's'
fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?
format_string << fmt
separator = separator.to_s
separator = ' ' if separator.empty?
sprintf_args << "::Logging.ndc.context.join('#{separator}')"
nil
end
# Handles the rest of the directives; none of these need any special
# handling.
#
# format - format String
# directive - the directive character
# precision - added back to the format string
#
# Returns nil
def handle_directives( format, directive, precision )
fmt = format + 's'
fmt = color_scheme.color(fmt, COLOR_ALIAS_TABLE[directive]) if colorize?
format_string << fmt
format_string << "{#{precision}}" if precision
sprintf_args << DIRECTIVE_TABLE[directive]
nil
end
end
# :startdoc:
end
end