class Log # The program name used for log entries # # Defaults to the executable name class_property progname = File.basename(PROGRAM_NAME) # The current process PID protected class_getter pid : String = Process.pid.to_s # Base interface to convert log entries and write them to an `IO` module Formatter # Writes a `Log::Entry` through an `IO` abstract def format(entry : Log::Entry, io : IO) # Creates an instance of a `Log::Formatter` that calls # the specified `Proc` for every entry def self.new(&proc : (Log::Entry, IO) ->) : self ProcFormatter.new proc end end # :nodoc: private struct ProcFormatter include Formatter def initialize(@proc : (Log::Entry, IO) ->) end def format(entry : Log::Entry, io : IO) : Nil @proc.call(entry, io) end end # Base implementation of `Log::Formatter` to convert # log entries into text representation # # This can be used to create efficient formatters: # ``` # require "log" # # struct MyFormat < Log::StaticFormatter # def run # string "- " # severity # string ": " # message # end # end # # Log.setup(:info, Log::IOBackend.new(formatter: MyFormat)) # Log.info { "Hello" } # => - INFO: Hello # Log.error { "Oh, no!" } # => - ERROR: Oh, no! # ``` # # There is also a helper macro to generate these formatters. Here's # an example that generates the same result: # ``` # Log.define_formatter MyFormat, "- #{severity}: #{message}" # ``` abstract struct StaticFormatter extend Formatter def initialize(@entry : Log::Entry, @io : IO) end # Write the entry timestamp in RFC3339 format def timestamp : Nil @entry.timestamp.to_rfc3339(@io, fraction_digits: 6) end # Write a fixed string def string(str : _) : Nil @io << str end # Write the message of the entry def message : Nil @io << @entry.message end # Write the severity # # This writes the severity in uppercase and left padded # with enough space so all the severities fit def severity : Nil @entry.severity.label.rjust(@io, 6) end # Write the source for non-root entries # # It doesn't write any output for entries generated from the root logger. # Parameters `before` and `after` can be provided to be written around # the value. # ``` # Log.define_formatter TestFormatter, "#{source(before: '[', after: "] ")}#{message}" # Log.setup(:info, Log::IOBackend.new(formatter: TestFormatter)) # Log.for("foo.bar").info { "Hello" } # => - [foo.bar] Hello # ``` def source(*, before : _ = nil, after : _ = nil) : Nil if @entry.source.size > 0 @io << before << @entry.source << after end end # Write all the values from the entry data # # It doesn't write any output if the entry data is empty. # Parameters `before` and `after` can be provided to be written around # the value. def data(*, before : _ = nil, after : _ = nil) : Nil unless @entry.data.empty? @io << before << @entry.data << after end end # Write all the values from the context # # It doesn't write any output if the context is empty. # Parameters `before` and `after` can be provided to be written around # the value. def context(*, before : _ = nil, after : _ = nil) : Nil unless @entry.context.empty? @io << before << @entry.context << after end end # Write the exception, including backtrace # # It doesn't write any output unless there is an exception in the entry. # Parameters `before` and `after` can be provided to be written around # the value. `before` defaults to `'\n'` so the exception is written # on a separate line def exception(*, before : _ = '\n', after : _ = nil) : Nil if ex = @entry.exception @io << before ex.inspect_with_backtrace(@io) @io << after end end # Write the program name. See `Log.progname`. def progname : Nil @io << Log.progname end # Write the current process identifier def pid(*, before = '#', after = nil) @io << before << Log.pid << after end # Write the `Log::Entry` to the `IO` using this pattern def self.format(entry : Log::Entry, io : IO) : Nil new(entry, io).run end # Subclasses must implement this method to define the output pattern abstract def run end # Generate subclasses of `Log::StaticFormatter` from a string with interpolations # # Example: # ``` # Log.define_formatter MyFormat, "- #{severity}: #{message}" # ``` # See `Log::StaticFormatter` for the available methods that can # be called within the interpolations. macro define_formatter(name, pattern) struct {{name}} < ::Log::StaticFormatter def run {% for part in pattern.expressions %} {% if part.is_a?(StringLiteral) %} string {{ part }} {% else %} {{ part }} {% end %} {% end %} end end end end # Default short format # # It writes log entries with the following format: # ``` # 2020-05-07T17:40:07.994508000Z INFO - my.source: Initializing everything # ``` # # When the entries have context data it's also written to the output: # ``` # 2020-05-07T17:40:07.994508000Z INFO - my.source: Initializing everything -- {"data" => 123} # ``` # # Exceptions are written in a separate line: # ``` # 2020-05-07T17:40:07.994508000Z ERROR - my.source: Something failed # Oh, no (Exception) # from ... # ``` Log.define_formatter Log::ShortFormat, "#{timestamp} #{severity} - #{source(after: ": ")}#{message}" \ "#{data(before: " -- ")}#{context(before: " -- ")}#{exception}"