iyi

Standard library, 0.18.0

std/log

Levelled logging with named sources.

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
  end

with_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)
    ...
  end

close 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.

std/logiyi doc src/std/log.iyi
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
end

Backendclass

std/logiyi doc src/std/log.iyi
pub abstract class Backend
  abstract def close : Nil
  abstract def write(entry : Entry) : Nil
end

DefaultFormatterclass

std/logiyi doc src/std/log.iyi
pub class DefaultFormatter < Formatter
  def format(entry : Entry) : String
end

Entrystruct

std/logiyi doc src/std/log.iyi
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
end

Formatterclass

std/logiyi doc src/std/log.iyi
pub abstract class Formatter
  def format(entry : Entry, io : IO) : Nil
  abstract def format(entry : Entry) : String
end

IOBackendclass

std/logiyi doc src/std/log.iyi
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
end

Logclass

The root logger and the calls that go straight to it.

std/logiyi doc src/std/log.iyi
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
end

Loggerclass

std/logiyi doc src/std/log.iyi
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
end

MemoryBackendclass

std/logiyi doc src/std/log.iyi
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
end

Metadataclass

Keys and values, the newest first: immutable, so a task's context can sit in the table every thread reads.

std/logiyi doc src/std/log.iyi
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
end

Severityenum

std/logiyi doc src/std/log.iyi
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