lib/lumberjack/template.rb
# frozen_string_literal: true
module Lumberjack
# A template converts entries to strings. Templates can contain the following place holders to
# reference log entry values:
#
# * :time
# * :severity
# * :progname
# * :tags
# * :message
#
# Any other words prefixed with a colon will be substituted with the value of the tag with that name.
# If your tag name contains characters other than alpha numerics and the underscore, you must surround it
# with curly brackets: `:{http.request-id}`.
class Template
TEMPLATE_ARGUMENT_ORDER = %w[:time :severity :progname :pid :message :tags].freeze
MILLISECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%3N"
MICROSECOND_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N"
PLACEHOLDER_PATTERN = /:(([a-z0-9_]+)|({[^}]+}))/i.freeze
# Create a new template from the markup. The +first_line+ argument is used to format only the first
# line of a message. Additional lines will be added to the message unformatted. If you wish to format
# the additional lines, use the :additional_lines options to specify a template. Note that you'll need
# to provide the line separator character in this template if you want to keep the message on multiple lines.
#
# The time will be formatted as YYYY-MM-DDTHH:MM:SSS.SSS by default. If you wish to change the format, you
# can specify the :time_format option which can be either a time format template as documented in
# +Time#strftime+ or the values +:milliseconds+ or +:microseconds+ to use the standard format with the
# specified precision.
#
# Messages will have white space stripped from both ends.
#
# @param [String] first_line The template to use to format the first line of a message.
# @param [Hash] options The options for the template.
def initialize(first_line, options = {})
@first_line_template, @first_line_tags = compile(first_line)
additional_lines = options[:additional_lines] || "#{Lumberjack::LINE_SEPARATOR}:message"
@additional_line_template, @additional_line_tags = compile(additional_lines)
# Formatting the time is relatively expensive, so only do it if it will be used
@template_include_time = first_line.include?(":time") || additional_lines.include?(":time")
self.datetime_format = (options[:time_format] || :milliseconds)
end
# Set the format used to format the time.
#
# @param [String] format The format to use to format the time.
def datetime_format=(format)
if format == :milliseconds
format = MILLISECOND_TIME_FORMAT
elsif format == :microseconds
format = MICROSECOND_TIME_FORMAT
end
@time_formatter = Formatter::DateTimeFormatter.new(format)
end
# Get the format used to format the time.
#
# @return [String]
def datetime_format
@time_formatter.format
end
# Convert an entry into a string using the template.
#
# @param [Lumberjack::LogEntry] entry The entry to convert to a string.
# @return [String] The entry converted to a string.
def call(entry)
return entry unless entry.is_a?(LogEntry)
first_line = entry.message.to_s
additional_lines = nil
if first_line.include?(Lumberjack::LINE_SEPARATOR)
additional_lines = first_line.split(Lumberjack::LINE_SEPARATOR)
first_line = additional_lines.shift
end
formatted_time = @time_formatter.call(entry.time) if @template_include_time
format_args = [formatted_time, entry.severity_label, entry.progname, entry.pid, first_line]
tag_arguments = tag_args(entry.tags, @first_line_tags)
message = (@first_line_template % (format_args + tag_arguments))
message.rstrip! if message.end_with?(" ")
if additional_lines && !additional_lines.empty?
tag_arguments = tag_args(entry.tags, @additional_line_tags) unless @additional_line_tags == @first_line_tags
additional_lines.each do |line|
format_args[format_args.size - 1] = line
line_message = (@additional_line_template % (format_args + tag_arguments)).rstrip
line_message.rstrip! if line_message.end_with?(" ")
message << line_message
end
end
message
end
private
def tag_args(tags, tag_vars)
return [nil] * (tag_vars.size + 1) if tags.nil? || tags.size == 0
tags_string = +""
tags.each do |name, value|
unless value.nil? || tag_vars.include?(name)
value = value.to_s
value = value.gsub(Lumberjack::LINE_SEPARATOR, " ") if value.include?(Lumberjack::LINE_SEPARATOR)
tags_string << "[#{name}:#{value}] "
end
end
args = [tags_string.chop]
tag_vars.each do |name|
args << tags[name]
end
args
end
# Compile the template string into a value that can be used with sprintf.
def compile(template) # :nodoc:
tag_vars = []
template = template.gsub(PLACEHOLDER_PATTERN) do |match|
var_name = match.sub("{", "").sub("}", "")
position = TEMPLATE_ARGUMENT_ORDER.index(var_name)
if position
"%#{position + 1}$s"
else
tag_vars << var_name[1, var_name.length]
"%#{TEMPLATE_ARGUMENT_ORDER.size + tag_vars.size}$s"
end
end
[template, tag_vars]
end
end
end