std/log
import std/log to use it. Source: src/std/log.iyi at v0.18.0.
Log.info "started" writes an Entry — source, severity, message and the UTC instant it was made — through the root logger's backend. Log.for("db.pool") answers the logger for that source: a child of db, which is a child of the root, and the same object every time the name is asked for. A logger with no level or backend of its own uses its parent's, so Log.setup(level, backend) reconfigures the root in place and every logger already handed out follows it; a child given its own level= or backend= keeps that until it is set back to nil.
Severities in order: Trace, Debug, Info, Notice, Warn, Error, Fatal, and None to silence a logger. An entry below the logger's level is dropped before its message is built, so log.debug { expensive } costs nothing when Debug is off. The default level is Info.
Backends: IOBackend writes each entry to an IO through a Formatter (DefaultFormatter: 2024-02-29T15:30:45.123456Z INFO - source: message, and ... INFO - message at the root, the source and its : left off); MemoryBackend keeps the entries for a test to read back. A Backend of your own is a class with write(entry) and close.
Logging is synchronous: the entry is written before the call returns, in the calling thread, with no queue between.
**Data.** An entry carries keys and values besides its message, from three places, each over the one before:
Log.with_context(request_id: id) do # every entry in this task
db = Log.for("db").bind(pool: "main") # every entry of a logger
db.info("query", rows: 3) # one entry
endwith_context holds for the block, in the task that runs it and in the tasks it starts, which find their starter's context while it is open - where the other library's new fiber starts with none. DefaultFormatter writes the data after the message, ... - db: query -- request_id=7 pool=main rows=3, a value with a space, quote or = in it quoted.
Log.setup_from_env sets the root's level from LOG_LEVEL.
AsyncBackendclass
Entries written by a thread of their own, so a call that logs does not wait for the write: the entry is formatted where it is logged and queued, and a writer thread writes what has queued up since its last write, in order, in one write, to *io*'s descriptor.
AsyncBackend.open do |backend| # closed, and drained, at the end
Log.setup(Severity::Info, backend)
...
endclose waits for every queued entry to be written and the thread to end; an entry written after it is refused. Entries queued when a program ends without close are lost, which the block form rules out. The backend is the thread that made it's, as every object not Share is.
A thread needs the collector and a target with threads: under -Dgc_none or -Dgc_boehm, or on another target, new refuses.
pub class AsyncBackend < Backend
def close : Nil
def formatter : Formatter
def formatter=(value : Formatter) : Formatter
def initialize(io : IO = STDOUT, formatter : Formatter = DefaultFormatter.new)
# A backend for the block, closed and drained when the block ends.
def self.open(io : IO = STDOUT, formatter : Formatter = DefaultFormatter.new, &block : (AsyncBackend -> R)) : R forall R
def write(entry : Entry) : Nil
endBackendclass
pub abstract class Backend
abstract def close : Nil
abstract def write(entry : Entry) : Nil
endDefaultFormatterclass
pub class DefaultFormatter < Formatter
def format(entry : Entry) : String
endEntrystruct
pub struct Entry
# The task's context, the logger's bound data and the call's own, in
# that order, a later key's value over an earlier's.
def data : Hash(String, String)
def initialize(source : String, severity : Severity, message : String, timestamp : Time = Time.utc, data : Hash(String, String) = {} of String => String)
def message : String
def severity : Severity
def source : String
def timestamp : Time
endFormatterclass
pub abstract class Formatter
def format(entry : Entry, io : IO) : Nil
abstract def format(entry : Entry) : String
endIOBackendclass
pub class IOBackend < Backend
def close : Nil
def formatter : Formatter
def formatter=(value : Formatter) : Formatter
def initialize(io : IO, formatter : Formatter = DefaultFormatter.new)
def io : IO
def write(entry : Entry) : Nil
endLogclass
The root logger and the calls that go straight to it.
pub class Log
def self.backend : Backend | ::Nil
def self.backend=(backend : Backend | ::Nil) : Nil
# The data this task's open contexts give an entry, the outermost first.
def self.context : Hash(String, String)
def self.debug(msg : String, **data) : Nil
def self.debug(**data, &b : (-> String)) : Nil
def self.error(msg : String, **data) : Nil
def self.error(**data, &b : (-> String)) : Nil
def self.fatal(msg : String, **data) : Nil
def self.fatal(**data, &b : (-> String)) : Nil
def self.for(source : String) : Logger
def self.info(msg : String, **data) : Nil
def self.info(**data, &b : (-> String)) : Nil
def self.level : Severity
def self.level=(level : Severity) : Nil
def self.notice(msg : String, **data) : Nil
def self.notice(**data, &b : (-> String)) : Nil
def self.root : Logger
# Reconfigures the root in place, so loggers obtained earlier follow.
def self.setup(level : Severity = Severity::Info, backend : Backend = IOBackend.new(STDOUT)) : Logger
# `setup`, the level read from *variable* - a severity's name, any case
# - or *default_level* when it is unset or empty. A name that is no
# severity is refused, the severities named.
def self.setup_from_env(default_level : Severity = Severity::Info, backend : Backend = IOBackend.new(STDOUT), variable : String = "LOG_LEVEL") : Logger
def self.trace(msg : String, **data) : Nil
def self.trace(**data, &b : (-> String)) : Nil
def self.warn(msg : String, **data) : Nil
def self.warn(**data, &b : (-> String)) : Nil
# Runs the block with *data* on every entry logged in this task and in
# the tasks it starts, over the context already open, and answers the
# block's answer. The context before it is back when the block ends,
# panic or not.
def self.with_context(**data, &block : (-> R)) : R forall R
endLoggerclass
pub class Logger
# This logger's own backend, or the nearest ancestor's; nil silences.
def backend : Backend | ::Nil
def backend=(b : Backend | ::Nil) : Nil
# A logger for the same source, its level and backend this one's, whose
# entries all carry *data*: a request's id, a connection's peer.
def bind(**data) : Logger
# The data `bind` gave this logger and its parents, the root's first.
protected def bound_into(found : Hash(String, String)) : Nil
def debug(message : String, **data) : Nil
def debug(**data, &b : (-> String)) : Nil
def debug? : Bool
def error(message : String, **data) : Nil
def error(**data, &b : (-> String)) : Nil
def error? : Bool
def fatal(message : String, **data) : Nil
def fatal(**data, &b : (-> String)) : Nil
def fatal? : Bool
# The logger for *child_name* under this one, created on first ask;
# `"a.b"` is the child `b` of the child `a`.
def for(child_name : String) : Logger
protected def get_or_create_child(name : String) : Logger
def info(message : String, **data) : Nil
def info(**data, &b : (-> String)) : Nil
def info? : Bool
def initialize(source : String, level : Severity | ::Nil = nil, backend : Backend | ::Nil = nil, parent : Logger | ::Nil = nil, bound : Metadata | ::Nil = nil)
# This logger's own level, or the nearest ancestor's; Info at a root
# never given one.
def level : Severity
# nil hands the decision back to the parent.
def level=(lvl : Severity | ::Nil) : Nil
# None is a level and not a severity: an entry logged at None was
# written by a logger set to None, the one level meant to write nothing.
def log(severity : Severity, message : String, **data) : Nil
def log(severity : Severity, **data, &block : (-> String)) : Nil
def notice(message : String, **data) : Nil
def notice(**data, &b : (-> String)) : Nil
def notice? : Bool
def own_backend : Backend | ::Nil
def own_level : Severity | ::Nil
def source : String
def trace(message : String, **data) : Nil
def trace(**data, &b : (-> String)) : Nil
def trace? : Bool
def warn(message : String, **data) : Nil
def warn(**data, &b : (-> String)) : Nil
def warn? : Bool
endMemoryBackendclass
pub class MemoryBackend < Backend
def clear : Nil
def close : Nil
def entries : Array(Entry)
def formatter : Formatter
def formatter=(value : Formatter) : Formatter
def initialize(formatter : Formatter = DefaultFormatter.new)
# The entries as the formatter would have written them.
def output : Array(String)
def write(entry : Entry) : Nil
endMetadataclass
Keys and values, the newest first: immutable, so a task's context can sit in the table every thread reads.
pub class Metadata
def initialize(key : String, value : String, rest : Metadata | ::Nil)
def key : String
# Into *into*, the oldest first, so a later key's value replaces an
# earlier one's.
def merge_into(into : Hash(String, String)) : Nil
def rest : Metadata | ::Nil
def to_h : Hash(String, String)
def value : String
endSeverityenum
pub enum Severity : Int32
Trace = 0
Debug = 1
Info = 2
Notice = 3
Warn = 4
Error = 5
Fatal = 6
None = 7
def <(other : Severity) : Bool
def <=(other : Severity) : Bool
def >(other : Severity) : Bool
def >=(other : Severity) : Bool
def label : String
def self.parse(name : String) : Severity
# The severity named by *name*, case-insensitively, or nil: what a
# `LOG_LEVEL` variable holds.
def self.parse?(name : String) : Severity | ::Nil
def to_s : String
end
Levelled logging with named sources.