Sign in

    moonlog

    Structured logging for MoonBit.

    log
    logging
    moonbit
    Download zip
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    2 days ago
    Downloads
    475

    No dependencies

    #moonlog

    Structured logging for MoonBit. Six levels, typed fields, filters by target and level, chains of steps, four layouts including ten JSON schemas, sinks behind a health machine, and alarms that walk their tiers.

    Quick start · The surface · Layouts · Slots · Documentation


    #Quick start

    let sink = @moonlog.Lines::new(fn(line) { println(line) })
    let hub = @moonlog.Hub::new(clock=fn() { 1_700_000_000_000_000_000L })
    let _ = hub.add(
    @moonlog.Slot::new(sink, name="console")
    .layout(@moonlog.Pattern::parse("{time:%H:%M:%S%.3f} {level:<5} {target}: {msg}{fields}")),
    )

    let log = hub.logger("mirror::sync")
    log.info("starting", fields=[("workers", @moonlog.Value::of_int(4))])
    log.warn("slow", fields=[("took_ms", @moonlog.Value::of_int(1200))])
    // 22:13:20.000 INFO mirror::sync: starting workers=4
    // 22:13:20.000 WARN mirror::sync: slow took_ms=1200

    moon add moonbitstack/moonlog

    #What it does not do

    It does not read a clock. Every instant arrives as nanoseconds since the epoch, from the clock a hub was built with. A logger that reads the time writes something different every run; here the instant is always one the application named, and a minute of sampling replays in a loop without waiting for the minute.

    It does not keep a global logger. A hub is a value and a logger is a value. A library takes a Logger as a parameter and writes through it; a library that was handed none writes nothing. Nothing in this package can be configured behind your back.

    It does not format messages. A message is a String, built at the call site with interpolation. A format language needs an escape for itself, a rule for a missing argument, and a way to be wrong at run time about both.

    It does not write anywhere by itself. A sink is one method — hand it an entry and say what went wrong. Lines::new(fn(line) { println(line) }) prints, Memory::new() keeps what a test wants to read, and a sink of your own reaches whatever you were going to write to anyway.

    #How a record travels

    flowchart LR L[Logger<br/>target + fields] --> H[Hub<br/>filter + chain] H --> F{slot filter} F -- dropped --> X(( )) F -- kept --> P[slot chain] P -- dropped --> X P -- kept --> Y[Layout<br/>pattern · logfmt · pretty · JSON] Y --> Q{queue?} Q -- direct --> S[Sink] Q -- buffered --> S S -- ok --> B[Breaker<br/>up] S -- failed --> B2[Breaker<br/>down] B2 --> A[Alarm<br/>tiers · * · fallback] A --> D[other sinks]

    A record is built with the hub's instant and sequence number, filtered once for everyone, run through the hub's chain, then offered to each slot in turn: its own filter, its own chain, its own layout, its own sink. A failure drives that slot's breaker, and the breaker decides whether an alarm is owed.

    #The surface

    #Levels

    LevelValueColourWritten as
    trace10cyanTRACE
    debug20magentaDEBUG
    info30greenINFO
    warn40yellowWARN
    error50redERROR
    fatal60white on redFATAL

    A level is a value, not an enumeration, so { name: "notice", value: 35, colour: Blue } works in filters, calls and layouts alike. Only value counts when comparing. Level::parse takes any capitalisation, warning, critical, and 0 to 255. Level::label(profile) writes it the way a profile's logger would — WARNING for Python, ERROR+4 for slog.

    #Values and records

    TypeWhat it carries
    ValueNull, Bool, Int, Float, Str, List, Map, Error(message, causes), Other(text)
    Callermodule path, function, file, line — every part optional
    Recordlevel, target, message, ordered fields, tags, caller, thread, span chain, instant, sequence number
    Identityapp, env, instance, host, pid, start, and the offset local time is written with

    Fields are (String, Value), in the order they were given; a later field of the same name replaces the earlier one where it stands. Record::set, ::unset, ::say, ::at, ::tag, ::stamped, ::located and ::on each answer a copy, so a step that changes a record cannot be seen by the slot next door.

    #Filters

    Comma-separated directives:

    DirectiveMeaning
    infothe default level
    app=debugtarget app and everything under it
    appeverything for app
    app=offnothing for app
    web:*=error* matches any run of characters
    info/sync(ed)?the message must also match the expression after /
    35a level by number

    A target matches itself and everything under it, split at ::, ., : or /, and the most specific directive wins: literal targets by length, then wildcards. Targets no directive matches are off unless a default level was given, and an empty spec lets error and above through.

    #Steps

    A Processor may change a record or drop it. The hub's chain runs first, then each slot's own.

    StepOptionsEffect
    Redactkey, path, pattern, maskmasks fields by name at any depth, at a dotted path, and matches in the message
    Sanitise—escapes line breaks, tabs and control characters as \n, \r, \t, \u{0007}
    Fieldskey-value pairssets fields on every record
    Keeptag, field, levelkeeps a record only when every condition holds
    Excludethe samedrops a record when every condition holds
    Samplefirst, every, perper level and message, keeps the first first in each per, then one in every
    Limitrate, bursta token bucket of rate a second holding up to burst
    Dedupewithindrops repeats within the window; the next one kept carries repeated

    The steps that need a moment are handed one: Processor::process(record, Env), where Env::at(nanoseconds) is the hub's instant. Nothing here reads a clock.

    #Layouts

    A Layout turns a record into one line. Scene says how: which profile's vocabulary, whether a terminal takes colour, and which identity is writing.

    LayoutOutput
    Patterna template, or a preset name
    Logfmttime=… level=warn target=app msg="…" key=value
    Prettya first line, then one aligned key = value per field
    JsonLineone object in a schema, with keys renamed, levels by name or number, fields nested

    PresetReproducesPattern
    serlogthe default{time} {level:<5} {target}: {msg}{fields}
    tracingtracing-subscriber{time:rfc3339} {level:>5} {spans}{target}: {msg}{fields}
    env_loggerenv_logger[{time:utc} {level:<5} {target}] {msg}
    pythonlogging.basicConfig{level}:{target}:{msg}
    logbackLogback, Log4j 2{time:%H:%M:%S%.3f} [{thread}] {level:<5} {target:~36} - {msg}
    slogGo's slog.TextHandlertime={time:%Y-%m-%dT%H:%M:%S%.3f%:z} level={level} msg={msg:q}{fields:q}
    pinopino-pretty[{time:%H:%M:%S%.3f}] {level} ({pid}): {msg}{fields}

    Every token but time may end with <N, >N or ^N to pad to N columns, then .M to keep at most M characters:

    TokenOptionsWrites
    timelocal, utc, rfc3339, unix, unix_ms, unix_ns, or strftimethe instant
    levellowerthe level, as the profile or the preset spells it
    target~N shortens leading segments until it fitswhere the record came from
    module, func, file, linefile:base keeps the namethe caller, ? when unknown
    thread, tid, taskwhere it ran
    pid, host, app, env, instancethe identity
    msg, fieldsq quotes the logfmt waythe message, and key=value per field
    tags, error, spanstags, the error field with its causes, the span chain
    elapsed, seqmilliseconds since the identity's start, the record's number

    SchemaKeys
    serlogtime level target msg file line fields app env instance
    pinolevel time pid hostname fields msg
    bunyanname hostname pid level fields msg time v
    zaplevel ts logger caller msg fields
    slogtime level msg fields
    structlogevent fields level logger timestamp
    logstash@timestamp @version message logger_name thread_name level level_value fields
    ecs@timestamp log.level message ecs.version log.logger log.origin.* process.pid host.hostname service.* fields
    gelfversion host short_message timestamp level _target fields, each prefixed _
    oteltimestamp severity_text severity_number body attributes resource

    #Sinks, slots, and what happens when one fails

    A Sink is one method: write(entry). An entry is the record and the line the slot's layout made of it. Lines, Memory and Discard come with the package; a sink of your own is a struct and three lines.

    A Slot is where everything else lives:

    SettingDefaultWhat it does
    name—how an alarm names it; * is refused
    kindcustomwhat the sink is, for a status page
    filter / atthe hub's filter alonewhat reaches this sink
    processnonesteps that run for this slot only
    layoutthe profile's patternhow entries are written
    colouredfalsewhether the line carries escapes
    queuedirectsize, batch, wait, full: Drop or Block
    retries0how many more times a failed write is tried
    probe5 sfirst wait before probing a failed sink, doubling to 5 min
    on_failIgnorePanic, Ignore, or Alarm(tiers)
    alarm_onlyfalsekeep it off the log route

    A slot's health is a machine with three states, and every transition is one write:

    stateDiagram-v2 [*] --> Up Up --> Down: write failed Down --> Down: record skipped Down --> Probing: probe due Probing --> Up: write went through Probing --> Down: write failed again

    Up writes. Down skips records until its next probe, counting them. Probing is one write deciding. A first failure opens an incident; a failure during a probe doubles the wait, up to five minutes, and reminds once the alarm's interval has passed; a success resolves it, and the wait starts over.

    hub.status() reports one Status per slot: its name and kind, its health, the records it skipped while down, the records its queue dropped, the records it suppressed because its own sink wrote them, when the current health began, and the latest error while it is not up.

    #Alarms

    An Alarm says which sinks to try, in which tiers. * stands for every sink on the log route. The walk leaves out the failing sink, leaves out any that are resting, never asks the same sink twice, and stops at the first tier where one sink took the record.

    let oncall = @moonlog.Tiers::from([["loki"], ["mail"], ["stderr"]])
    .with_fallback(@moonlog.Fallback::Ignore)
    .with_remind(1_800_000_000_000L) // half an hour

    let _ = hub.add(
    @moonlog.Slot::new(sink, name="loki").on_fail(@moonlog.OnFail::Alarm(oncall)),
    )

    An incident becomes a record: error when a sink failed, info when it works again, with sink, kind, phase, failures, skipped, dropped and since on it. Alarm records skip slot filters, because they are not a message from your program.

    #Profiles

    A profile changes four things at once, because those are the four a deployment already expects:

    ProfileConsoleLevelFilter variable
    serlogstdoutinfoSERLOG_FILTER
    tracingstdoutinfoRUST_LOG
    env_loggerstderrerrorRUST_LOG
    pythonstderrwarn—
    logbackstdoutdebug—
    slogstdoutinfo—
    pinostdoutinfo—

    #Examples

    ExampleWhat it shows
    01-toura hub, two slots, a filter, a logger with a bound field, and one record in six shapes
    02-formatsevery preset, every schema, renamed keys, a coloured line
    03-servicea service: redaction, sampling, a queue, a sink that is down, an alarm that falls back

    moon run src/examples/01-tour

    #Licence

    Apache-2.0. See LICENSE.

    Alarm

    pub(open) trait Alarm {
    fn route(Self, Incident) -> Array[Array[String]]
    fn fallback(Self) -> Fallback
    fn remind(Self) -> Int64
    }

    Chooses where an incident goes.

    The runtime walks the tiers, skips the failing sink and any that are resting, and stops at the first tier where one sink took the record — so an implementation only has to say which sinks to try.

    Desk

    pub(open) trait Desk {
    fn logs(Self) -> Array[String]
    fn resting(Self, String) -> Bool
    fn deliver(Self, String, Record) -> Result[Unit, String]
    }

    What the alarm walk needs from the sinks around it.

    Layout

    pub(open) trait Layout {
    fn render(Self, Record, Scene) -> String
    }

    How a record becomes one line.

    A layout knows nothing about where the line goes; a sink is handed the line its slot's layout made of the record.

    Processor

    pub(open) trait Processor {
    fn process(Self, Record, Env) -> Record?
    }

    A step in a processor chain: it may change the record, or drop it.

    A step that only decides returns the record it was given, untouched, so the other slots still see what was written. The first step that changes anything returns a copy.

    Sink

    pub(open) trait Sink {
    fn write(Self, Entry) -> Result[Unit, String]
    }

    Where entries go.

    Only write is required. Queues, retries, health and alarms belong to the slot the sink is registered in, so a sink delivers and says what went wrong and nothing more. Err is a failure worth telling someone about; a sink that cannot fail answers Ok.

    Refused

    pub(all) suberror Refused {
    Refused(at~ : Int, got~ : String, want~ : String)
    }

    A value the library will not take, and why.

    at is the offset in the input, counted in characters; got is what was there and want names what would have been taken. Parsing answers this rather than a default: a configuration file with a typo in it should say so, not run at a verbosity nobody asked for.

    Refused::to_text

    fn Refused::to_text(self : Refused) -> String

    The refusal as one line, for a terminal.

    Breaker

    pub struct Breaker {
    probe : Int64
    health : Health
    gap : Int64
    next : Int64
    told : Int64?
    since : Int64
    opened : Int64
    opened_at : Int64
    skipped : Int64
    error : String?
    }

    A slot's health: up, down, or one write testing whether it works again.

    It knows no I/O and reads no clock — every method takes the instant — which is what makes the machine checkable against vectors instead of against a stopwatch.

    Breaker::admit

    fn Breaker::admit(self : Breaker, now : Int64) -> Bool

    Whether a write may go ahead at now: turns a due Down into Probing, and counts a skip otherwise.

    Breaker::error

    fn Breaker::error(self : Breaker) -> String?

    The latest error.

    Breaker::failed_since

    fn Breaker::failed_since(self : Breaker) -> Int64

    When the current failure began, which is what an incident calls since; since itself moves when the sink recovers.

    Breaker::fault

    fn Breaker::fault(self : Breaker, now : Int64, error : String, remind : Int64, tell : Tell) -> Phase?

    A write failed; Opened or Reminder when that is to be told.

    Breaker::health

    fn Breaker::health(self : Breaker) -> Health

    Where the sink stands now: Up, Down, or Probing while a trial write is due.

    Breaker::new

    fn Breaker::new(probe : Int64, now : Int64) -> Breaker

    A healthy machine that first probes probe nanoseconds after a failure.

    Breaker::pass

    fn Breaker::pass(self : Breaker, now : Int64) -> Phase?

    A write went through; Resolved when the failure had been told.

    Breaker::resting

    fn Breaker::resting(self : Breaker, now : Int64) -> Bool

    Whether a sink is skipped at now without counting anything, for choosing alarm targets.

    Breaker::since

    fn Breaker::since(self : Breaker) -> Int64

    When the current health began.

    Breaker::skipped

    fn Breaker::skipped(self : Breaker) -> Int64

    Records skipped while down, over the slot's life.

    Breaker::skipped_now

    fn Breaker::skipped_now(self : Breaker) -> Int64

    Records skipped since the current failure began.

    Breaker::wait

    fn Breaker::wait(self : Breaker, now : Int64) -> Int64

    How long until the next probe.

    Caller

    pub(all) struct Caller {
    module_path : String?
    func : String?
    file : String?
    line : Int?
    } derive(Eq,
    Debug
    )

    Where in the source a record was made.

    Every part may be unknown; a layout writes ? for the ones that are, which is what a call site without debug information looks like.

    Caller::equal

    fn Caller::equal(Caller, Caller) -> Bool

    Caller::new

    fn Caller::new() -> Caller

    A caller with no part known; set the parts that are.

    Caller::not_equal

    fn Caller::not_equal(x : Caller, y : Caller) -> Bool

    Caller::to_repr

    Clock

    pub(all) enum Clock {
    Local
    Utc
    Rfc3339
    Unix
    UnixMs
    UnixNs
    Custom(String)
    } derive(Eq,
    Debug
    )

    How an instant is written.

    The library holds no clock, so every instant here arrives as nanoseconds since the epoch and every local time is the instant shifted by an offset that was handed in. A Custom spec is strftime, the subset below.

    Clock::equal

    fn Clock::equal(Clock, Clock) -> Bool

    Clock::not_equal

    fn Clock::not_equal(x : Clock, y : Clock) -> Bool

    Clock::parse

    fn Clock::parse(spec : StringView) -> Clock

    A clock spec, as a {time:…} hole spells it.

    Clock::to_repr

    Clock::write

    fn Clock::write(self : Clock, out : StringBuilder, time : Int64, offset : Int) -> Unit

    Writes time in this clock's form; offset is seconds east of UTC.

    Colour

    pub(all) enum Colour {
    Plain
    Red
    Green
    Yellow
    Blue
    Magenta
    Cyan
    White
    Grey
    Alarm
    } derive(Eq,
    Debug
    )

    A colour a terminal understands.

    Alarm is white on red, which is what a fatal line is worth. Plain writes no escape at all, so a value that never reached a terminal is unchanged.

    Colour::ansi

    fn Colour::ansi(self : Colour) -> String

    The escape that starts this colour, empty for Plain.

    Colour::equal

    fn Colour::equal(Colour, Colour) -> Bool

    Colour::name

    fn Colour::name(self : Colour) -> StringView

    The name used in configuration, filters and vector files.

    Colour::not_equal

    fn Colour::not_equal(x : Colour, y : Colour) -> Bool

    Colour::parse

    fn Colour::parse(name : StringView) -> Colour raise Refused

    A colour by name, gray included, which is how American spelling arrives.

    Colour::to_repr

    Dedupe

    pub struct Dedupe {
    within : Int64
    seen : Map[String, (Int64, Int)]
    }

    Drops a record that repeats one kept within the window; the next record kept carries repeated, the number dropped in between.
    impl Processor for Dedupe

    Dedupe::new

    fn Dedupe::new(within~ : Int64) -> Dedupe

    Drops repeats kept within within nanoseconds of each other.

    Dedupe::process

    fn Dedupe::process(self : Dedupe, record : Record, env : Env) -> Record?

    Discard

    pub struct Discard {
    }

    A sink that drops everything, for an application that configures logging off and a test that would rather not have output.
    impl Sink for Discard

    Discard::new

    fn Discard::new() -> Discard

    A sink that accepts every entry and keeps none.

    Discard::write

    fn Discard::write(_self : Discard, _entry : Entry) -> Result[Unit, String]

    Entry

    pub(all) struct Entry {
    record : Record
    line : String
    }

    What a sink is handed: the record, and the line its slot's layout made of it. The line is made once, whether or not the sink reads it.

    Env

    pub(all) struct Env {
    now : Int64
    }

    What a step may know about the moment it runs: the instant, in nanoseconds since the epoch, handed in by whoever owns the clock.

    Env::at

    fn Env::at(now : Int64) -> Env

    The environment at now, in nanoseconds since the epoch.

    Exclude

    pub struct Exclude {
    keep : Keep
    }

    Drops a record when every condition holds.

    Exclude::field

    fn Exclude::field(self : Exclude, key : String, value : String) -> Exclude

    Requires a field whose text is value.

    Exclude::level

    fn Exclude::level(self : Exclude, min : Level) -> Exclude

    Requires the level to be at least min.

    Exclude::new

    fn Exclude::new() -> Exclude

    No conditions yet; a step with none drops every record, so add the ones that pick what to drop.

    Exclude::process

    fn Exclude::process(self : Exclude, record : Record, _env : Env) -> Record?

    Exclude::tag

    fn Exclude::tag(self : Exclude, tag : String) -> Exclude

    Requires the record to carry tag.

    Failure

    pub(all) struct Failure {
    sink : String
    error : String
    at : Int64
    }

    One failed delivery, on the way to an alarm.

    Fallback

    pub(all) enum Fallback {
    Panic
    Ignore
    } derive(Eq,
    Debug
    )

    What an alarm does when no tier took the incident.

    Fallback::equal

    fn Fallback::equal(Fallback, Fallback) -> Bool

    Fallback::not_equal

    fn Fallback::not_equal(x : Fallback, y : Fallback) -> Bool

    Fallback::to_repr

    Fault

    pub(all) struct Fault {
    error : String
    phase : Phase?
    }

    What went wrong on the way to a sink, and whether the failure is one the alarm policy has been told about.

    Fields

    pub struct Fields {
    pairs : Array[(String, Value)]
    }

    Adds the same fields to every record.
    impl Processor for Fields

    Fields::new

    fn Fields::new() -> Fields

    No fields yet; add them with set.

    Fields::process

    fn Fields::process(self : Fields, record : Record, _env : Env) -> Record?

    Fields::set

    fn Fields::set(self : Fields, key : String, value : Value) -> Fields

    Adds key with value to every record, replacing a field of that name where it stands.

    Filter

    pub struct Filter {
    base : Level?
    rules : Array[Rule]
    pattern :
    Regex
    ?
    pattern_text : String
    }

    Which records pass, by target, level and optionally message.

    Comma-separated directives: a bare level for the default, app=debug for a target, app for everything a target writes, off for nothing, * inside a target, and a /regex at the end that the message must match. A target matches itself and everything under it, split at ::, ., : or /, and the most specific directive wins.

    Filter::admits

    fn Filter::admits(self : Filter, level : Level, target : String, msg : String) -> Bool

    Whether the message passes the /regex part, when there is one.

    Filter::allows

    fn Filter::allows(self : Filter, level : Level, target : String) -> Bool

    Whether a record at level from target passes, before its message is read.

    Filter::at

    fn Filter::at(min : Level) -> Filter

    A filter that lets min and above through for every target.

    Filter::has_pattern

    fn Filter::has_pattern(self : Filter) -> Bool

    Whether there is a /regex part.

    Filter::lowest

    fn Filter::lowest(self : Filter) -> Level?

    The lowest level any target lets through, or None when everything is off.

    Filter::off

    fn Filter::off() -> Filter

    A filter that lets nothing through.

    Filter::parse

    fn Filter::parse(spec : StringView) -> Filter raise Refused

    Reads the directives described on Filter; a malformed one is refused with its offset.

    Filter::to_text

    fn Filter::to_text(self : Filter) -> String

    The spec this filter reads back as.

    Full

    pub(all) enum Full {
    Drop
    Block
    } derive(Eq,
    Debug
    )

    What a full queue does with a record that arrives.

    Full::equal

    fn Full::equal(Full, Full) -> Bool

    Full::not_equal

    fn Full::not_equal(x : Full, y : Full) -> Bool

    Full::to_repr

    Health

    pub(all) enum Health {
    Up
    Down
    Probing
    } derive(Eq,
    Debug
    )

    Whether a sink is working.

    Health::equal

    fn Health::equal(Health, Health) -> Bool

    Health::name

    fn Health::name(self : Health) -> StringView

    The name used in configuration, status and the shared vectors.

    Health::not_equal

    fn Health::not_equal(x : Health, y : Health) -> Bool

    Health::to_repr

    Hub

    pub struct Hub {
    identity : Identity
    profile : Profile
    filter : Filter
    chain : Array[&Processor]
    slots : Array[Slot]
    layout : &Layout
    clock : () -> Int64
    seq : Int64
    inside : Bool
    writing : Int
    suppressed : Int64
    }

    The whole of a running logger: a global filter, a chain of steps every record goes through, a table of slots, and the identity and clock that the application handed in.

    There is no process-wide hub. An application builds one and passes the Logger values that write to it to the libraries it uses; a library that was handed none writes nothing.
    impl Desk for Hub

    Hub::add

    fn Hub::add(self : Hub, slot : Slot) -> String raise Refused

    Registers a slot and answers the name it took.

    A second slot with the same name is refused, and so is the name *, which would make an alarm tier ambiguous.

    Hub::deliver

    fn Hub::deliver(self : Hub, name : String, record : Record) -> Result[Unit, String]

    Hub::dispatch

    fn Hub::dispatch(self : Hub, record : Record) -> Unit

    Sends record everywhere it belongs: through the global chain, then to each slot's own filter and steps, then to its sink.

    A record made while a sink is already running is dropped and counted: a sink that logs into the hub it belongs to would otherwise never finish.

    Hub::enabled

    fn Hub::enabled(self : Hub, level : Level) -> Bool

    Whether a record at level could pass, before its target or message is known. Worth asking before building a message that costs something.

    Hub::filter

    fn Hub::filter(self : Hub) -> Filter

    The filter now in force.

    Hub::flush

    fn Hub::flush(self : Hub) -> Unit

    Writes what every queue holds, up to now.

    Hub::identity

    fn Hub::identity(self : Hub) -> Identity

    The identity records are stamped with.

    Hub::logger

    fn Hub::logger(self : Hub, target : String) -> Logger

    A logger writing to this hub under target, which is usually a module path.

    Hub::logs

    fn Hub::logs(self : Hub) -> Array[String]

    Hub::new

    fn Hub::new(identity? : Identity, profile? : Profile, clock? : () -> Int64, filter? : Filter) -> Hub

    A hub at profile, writing what the filter lets through.

    clock answers nanoseconds since the epoch. Without one every record carries 0, which is honest — this library never reads a clock — and every test says what instant it means.

    Hub::now

    fn Hub::now(self : Hub) -> Int64

    The instant the hub is working at, from the clock it was given.

    Hub::process

    fn Hub::process(self : Hub, step : &Processor) -> Unit

    Adds a step to the global chain, which runs before any slot sees the record.

    Hub::profile

    fn Hub::profile(self : Hub) -> Profile

    The profile in force.

    Hub::remove

    fn Hub::remove(self : Hub, name : String) -> Bool

    Takes a slot out by name, after writing what its queue holds.

    Hub::resting

    fn Hub::resting(self : Hub, name : String) -> Bool

    Hub::scene

    fn Hub::scene(self : Hub) -> Scene

    The scene a line is written in: this hub's vocabulary, this hub's identity.

    Hub::set_filter

    fn Hub::set_filter(self : Hub, filter : Filter) -> Unit

    Replaces the global filter; takes effect at once.

    Hub::set_level

    fn Hub::set_level(self : Hub, min : Level) -> Unit

    Lets through min and above for every target.

    Hub::slots

    fn Hub::slots(self : Hub) -> Array[Slot]

    Every registered slot, in the order they were added.

    Hub::status

    fn Hub::status(self : Hub) -> Array[Status]

    How every slot stands.

    Hub::suppressed

    fn Hub::suppressed(self : Hub) -> Int64

    Records made while a sink was running, dropped so a sink that logs cannot feed itself.

    Hub::write

    fn Hub::write(self : Hub, level : Level, target : String, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes a record at level from target.

    Identity

    pub(all) struct Identity {
    app : String
    env : String
    instance : String
    host : String
    pid : Int
    start : Int64
    offset : (Int64) -> Int
    }

    The process and the deployment it is one instance of.

    This is the only place where the library can learn who is writing: it is passed in, never probed, so a test writes the identity it wants to check. offset answers the seconds east of UTC at an instant, which is how a local timestamp is written without the library owning a time zone database.

    Identity::new

    fn Identity::new() -> Identity

    An identity that names nobody: the deployment a library has not been told about.

    Identity::of

    fn Identity::of(app~ : String, env? : String, instance? : String, host? : String, pid? : Int, start? : Int64, offset? : (Int64) -> Int) -> Identity

    An identity with the parts a deployment knows.

    Incident

    pub(all) struct Incident {
    sink : String
    kind : String
    phase : Phase
    error : String
    failures : Array[Failure]
    skipped : Int64
    dropped : Int64
    since : Int64
    at : Int64
    }

    A sink's failure, from the first error to its recovery.

    A phase is a fresh incident each time: Opened when the sink first failed, Reminder while it is still down, Resolved when it writes again. Each carries what is known at that moment, which is why the type is a value and not a handle.

    Incident::describe

    fn Incident::describe(self : Incident, target? : String) -> Record

    The record an incident becomes: error when something failed, info when something works again, with the failures on one field.

    Incident::new

    fn Incident::new(sink : String, kind : String, phase : Phase, error : String, now : Int64) -> Incident

    A fresh incident: sink, a sink of kind, failed with error at now, in phase.

    JsonLine

    pub struct JsonLine {
    schema : Schema
    keys : Array[(String, String)]
    levels : Levels?
    nest : String?
    }

    One record as a JSON object in a schema, with any key renamed.
    impl Layout for JsonLine

    JsonLine::encode

    fn JsonLine::encode(self : JsonLine, record : Record, scene : Scene) -> String

    Writes one record as a JSON object.

    JsonLine::levels

    fn JsonLine::levels(self : JsonLine, levels : Levels) -> JsonLine

    Writes levels by name or by number, whatever the schema does by default.

    JsonLine::nest

    fn JsonLine::nest(self : JsonLine, key : String) -> JsonLine

    Puts the fields under one key instead of at the top.

    JsonLine::new

    fn JsonLine::new(schema : Schema) -> JsonLine

    Lines in schema, with its own key names and level spelling.

    JsonLine::rename

    fn JsonLine::rename(self : JsonLine, key : String, to : String) -> JsonLine

    Writes key as to, such as msg as message.

    JsonLine::render

    fn JsonLine::render(self : JsonLine, record : Record, scene : Scene) -> String

    Keep

    pub struct Keep {
    tags : Array[String]
    equals : Array[(String, String)]
    level : Level?
    }

    Conditions on tags, fields and level; every one given must hold. As a step, keeps the record only when they do — the match of the specification.
    impl Processor for Keep

    Keep::field

    fn Keep::field(self : Keep, key : String, value : String) -> Keep

    Requires a field whose text is value.

    Keep::holds

    fn Keep::holds(self : Keep, record : Record) -> Bool

    Whether every condition holds for record.

    Keep::level

    fn Keep::level(self : Keep, min : Level) -> Keep

    Requires min and above.

    Keep::new

    fn Keep::new() -> Keep

    No conditions yet, so every record passes until one is added.

    Keep::process

    fn Keep::process(self : Keep, record : Record, _env : Env) -> Record?

    Keep::tag

    fn Keep::tag(self : Keep, tag : String) -> Keep

    Requires a tag.

    Level

    pub(all) struct Level {
    name : String
    value : Int
    colour : Colour
    }

    How severe a record is.

    A level is a value rather than an enumeration so an application can define its own — Level::{ name: "notice", value: 35, colour: Blue } — and have it work in filters, calls and layouts alike. Only value counts when levels are compared; name is what filters and labels read.
    impl Compare for Level
    impl Eq for Level
    impl Show for Level

    Level::at_least

    fn Level::at_least(self : Level, min : Level) -> Bool

    Whether this level passes a minimum.

    Level::colour

    fn Level::colour(self : Level) -> Colour

    Its colour in a terminal.

    Level::compare

    fn Level::compare(self : Level, other : Level) -> Int

    Level::equal

    fn Level::equal(self : Level, other : Level) -> Bool

    Level::label

    fn Level::label(self : Level, profile : Profile) -> String

    How a profile's logger writes this level.

    A pattern preset uses its own logger's vocabulary; every other layout uses the profile in force. Python's WARNING and CRITICAL, and slog's numeric names for the two levels it has no word for, are the only differences — which is the point of a profile.

    Level::logstash

    fn Level::logstash(self : Level) -> Int

    The number Logstash writes, 5000 to 40000.

    Level::name

    fn Level::name(self : Level) -> String

    Its lower-case name, as filters, labels and configuration files write it.

    Level::named

    fn Level::named(self : Level, name : String) -> Level

    The same level under another name, which is how a profile's vocabulary reaches a JSON document.

    Level::not_equal

    fn Level::not_equal(x : Level, y : Level) -> Bool

    Level::op_ge

    fn Level::op_ge(x : Level, y : Level) -> Bool

    Level::op_gt

    fn Level::op_gt(x : Level, y : Level) -> Bool

    Level::op_le

    fn Level::op_le(x : Level, y : Level) -> Bool

    Level::op_lt

    fn Level::op_lt(x : Level, y : Level) -> Bool

    Level::otel

    fn Level::otel(self : Level) -> Int

    The severity number of the OpenTelemetry log model.

    Level::output

    fn Level::output(self : Level, logger : &Logger) -> Unit

    Level::parse

    fn Level::parse(name : StringView) -> Level raise Refused

    A level by name or by number, in any capitalisation.

    warning is warn, critical is fatal, and 0 to 255 is a level by value, so a level defined by an application is still spelled the same way once it reaches a filter.

    Level::syslog

    fn Level::syslog(self : Level) -> Int

    The syslog severity, for GELF and anything else shaped like syslog.

    Level::to_string

    fn Level::to_string(self : Level) -> String

    Level::value

    fn Level::value(self : Level) -> Int

    Its number.

    Levels

    pub(all) enum Levels {
    Name
    Number
    } derive(Eq,
    Debug
    )

    How levels are written, when a schema is told to write them another way.

    Levels::equal

    fn Levels::equal(Levels, Levels) -> Bool

    Levels::not_equal

    fn Levels::not_equal(x : Levels, y : Levels) -> Bool

    Levels::parse

    fn Levels::parse(name : StringView) -> Levels raise Refused

    name or number: how a JSON line writes the level.

    Levels::to_repr

    Limit

    pub struct Limit {
    rate : Double
    burst : Double
    level : Double
    at : Int64
    started : Bool
    }

    A token bucket: rate records a second, with bursts of up to burst.
    impl Processor for Limit

    Limit::new

    fn Limit::new(rate~ : Double, burst~ : Double) -> Limit

    rate records a second on average, and up to burst at once; a burst below one is raised to one.

    Limit::process

    fn Limit::process(self : Limit, record : Record, env : Env) -> Record?

    Lines

    pub struct Lines {
    write : (String) -> Unit
    }

    A sink that writes each line through a function.

    This is the seam: Lines::new(fn(line) { println(line) }) prints, and Lines::new(fn(line) { held.push(line) }) is how a test reads what was written without a terminal in the way.
    impl Sink for Lines

    Lines::new

    fn Lines::new(write : (String) -> Unit) -> Lines

    A sink that hands each line to write.

    Lines::write

    fn Lines::write(self : Lines, entry : Entry) -> Result[Unit, String]

    Logfmt

    pub struct Logfmt {
    }

    One key=value line per record: time=… level=… target=… msg=… key=value.

    Values are quoted the logfmt way, so a message with a space in it cannot be mistaken for two fields. This is the shape a shell pipeline reads with cut and awk.
    impl Layout for Logfmt

    Logfmt::new

    fn Logfmt::new() -> Logfmt

    The layout; it has nothing to configure.

    Logfmt::render

    fn Logfmt::render(_self : Logfmt, record : Record, scene : Scene) -> String

    Logger

    pub struct Logger {
    hub : Hub
    target : String
    fields : Array[(String, Value)]
    }

    A target and the fields bound to it: what a module or a request holds and writes through.

    A logger is a value. It is passed in, it can be copied, and a library that was handed none writes nothing — there is no default one to inherit.

    Logger::bound

    fn Logger::bound(self : Logger, key : String, value : Value) -> Logger

    The same logger with key bound, so every line it writes carries it.

    Logger::debug

    fn Logger::debug(self : Logger, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes at debug.

    Logger::enabled

    fn Logger::enabled(self : Logger, level : Level) -> Bool

    Whether a message at level would be written.

    Worth asking before assembling a message that costs something: a logger cannot skip work its caller has already done.

    Logger::error

    fn Logger::error(self : Logger, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes at error.

    Logger::fatal

    fn Logger::fatal(self : Logger, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes at fatal. It does not abort: what to do after the last line is the caller's to decide, and a library that ends the process takes that decision away.

    Logger::info

    fn Logger::info(self : Logger, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes at info.

    Logger::log

    fn Logger::log(self : Logger, level : Level, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes a message, with fields if there are any.

    Logger::new

    fn Logger::new(hub : Hub, target : String) -> Logger

    A logger writing to hub under target.

    Logger::target

    fn Logger::target(self : Logger) -> String

    The target it writes under.

    Logger::trace

    fn Logger::trace(self : Logger, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes at trace.

    Logger::under

    fn Logger::under(self : Logger, target : String) -> Logger

    The same logger under another target, which is what a submodule is.

    Logger::warn

    fn Logger::warn(self : Logger, msg : String, fields? : Array[(String, Value)]) -> Unit

    Writes at warn.

    Memory

    pub struct Memory {
    size : Int
    entries : Array[Entry]
    }

    A sink that keeps the newest entries in memory, for a test or a status page.
    impl Sink for Memory

    Memory::clear

    fn Memory::clear(self : Memory) -> Unit

    Forgets everything it kept.

    Memory::find

    fn Memory::find(self : Memory, text : String) -> Array[Entry]

    The entries whose line contains text.

    Memory::lines

    fn Memory::lines(self : Memory) -> Array[String]

    The lines it kept, oldest first.

    Memory::new

    fn Memory::new(size? : Int) -> Memory

    Keeps the newest size entries; 0 keeps everything.

    Memory::records

    fn Memory::records(self : Memory) -> Array[Record]

    The records it kept, oldest first.

    Memory::write

    fn Memory::write(self : Memory, entry : Entry) -> Result[Unit, String]

    OnFail

    pub(all) enum OnFail {
    Panic
    Ignore
    Alarm(&Alarm)
    }

    What a slot does when its sink fails.

    Outcome

    pub(all) enum Outcome {
    Heard
    Unheard
    } derive(Eq,
    Debug
    )

    How an alarm walk ended.

    Outcome::equal

    fn Outcome::equal(Outcome, Outcome) -> Bool

    Outcome::not_equal

    fn Outcome::not_equal(x : Outcome, y : Outcome) -> Bool

    Outcome::to_repr

    Pattern

    pub struct Pattern {
    source : String
    pieces : Array[Piece]
    shows_error : Bool
    labels : Profile?
    }

    A line template: literal text with {token} or {token:options} holes, and {{ and }} for literal braces.

    Every token but time may end with <N, >N or ^N to pad to N columns, then .M to keep at most M characters. ^ puts the extra space on the right, which is what centred columns do in the loggers this reproduces.
    impl Layout for Pattern

    Pattern::parse

    fn Pattern::parse(text : StringView) -> Pattern raise Refused

    A pattern, or the name of a preset standing for one.

    Pattern::render

    fn Pattern::render(self : Pattern, record : Record, scene : Scene) -> String

    Pattern::source

    fn Pattern::source(self : Pattern) -> String

    The text it was parsed from.

    Phase

    pub(all) enum Phase {
    Opened
    Reminder
    Resolved
    } derive(Eq,
    Debug
    )

    Which report of an incident this is.

    Phase::equal

    fn Phase::equal(Phase, Phase) -> Bool

    Phase::name

    fn Phase::name(self : Phase) -> StringView

    The name used in a record's phase field and in the shared vectors.

    Phase::not_equal

    fn Phase::not_equal(x : Phase, y : Phase) -> Bool

    Phase::to_repr

    Piece

    type Piece

    One part of a pattern: a hole, or the text between two of them.

    Pretty

    pub struct Pretty {
    }

    A first line with the time, level, target and message, then one aligned key = value line per field. What a person reads in a terminal when the fields matter more than the bytes.
    impl Layout for Pretty

    Pretty::new

    fn Pretty::new() -> Pretty

    The layout; it has nothing to configure.

    Pretty::render

    fn Pretty::render(_self : Pretty, record : Record, scene : Scene) -> String

    Profile

    pub(all) enum Profile {
    Serlog
    Tracing
    EnvLogger
    Python
    Logback
    Slog
    Pino
    } derive(Eq,
    Debug
    )

    Another logger whose defaults an application can take on.

    A profile changes four things at once — the default line, the stream a console writes to, the level it starts at, and the variables a filter is read from — because those four are what a deployment already expects when it moves from one logger to another.

    Profile::env

    fn Profile::env(self : Profile) -> Array[String]

    The variables this profile reads a filter from, first one set wins.

    Profile::equal

    fn Profile::equal(Profile, Profile) -> Bool

    Profile::level

    fn Profile::level(self : Profile) -> Level

    The level this profile starts at.

    Profile::name

    fn Profile::name(self : Profile) -> StringView

    The name used in configuration files.

    Profile::not_equal

    fn Profile::not_equal(x : Profile, y : Profile) -> Bool

    Profile::parse

    fn Profile::parse(name : StringView) -> Profile raise Refused

    A profile by its lower-case name: serlog, tracing, env_logger, python, logback (or log4j), slog or pino.

    Profile::pattern

    fn Profile::pattern(self : Profile) -> StringView

    The pattern this profile's logger writes by default.

    Profile::stream

    fn Profile::stream(self : Profile) -> Stream

    Where this profile's console writes.

    Profile::to_repr

    Queue

    pub(all) struct Queue {
    size : Int
    batch : Int
    wait : Int64
    full : Full
    }

    A buffer in front of a sink, written in batches.

    Without a runtime to hand, the buffer is drained by the hub: when it holds batch records, when wait has passed since the first one, or at flush. That is the policy a background task would follow, with the clock still in the caller's hands.

    Queue::batch

    fn Queue::batch(self : Queue, batch : Int) -> Queue

    The same queue, written batch records at a time.

    Queue::full

    fn Queue::full(self : Queue, full : Full) -> Queue

    The same queue, with full deciding what an arriving record meets when it is full.

    Queue::new

    fn Queue::new(size~ : Int) -> Queue

    Holds size records, written batch_default at a time or wait_default after the first.

    Queue::wait

    fn Queue::wait(self : Queue, wait : Int64) -> Queue

    The same queue, flushed at most wait nanoseconds after its first record.

    Record

    pub(all) struct Record {
    level : Level
    target : String
    msg : String
    fields : Array[(String, Value)]
    tags : Array[String]
    caller : Caller
    thread : String?
    spans : String?
    time : Int64
    seq : Int64
    }

    One log event, as every sink sees it.

    There is no clock in here either: time is what was handed in, in nanoseconds since the epoch, and 0 means nobody said.

    Record::at

    fn Record::at(self : Record, level : Level) -> Record

    The same record at another level.

    Record::copy

    fn Record::copy(self : Record) -> Record

    A copy, with the arrays copied too, so an edit cannot be seen elsewhere.

    Record::field

    fn Record::field(self : Record, key : String) -> Value?

    The last field called key.

    Record::has

    fn Record::has(self : Record, key : String) -> Bool

    Whether any field is called key.

    Record::located

    fn Record::located(self : Record, file : String, line? : Int) -> Record

    The same record carrying a source file and line.

    Record::numbered

    fn Record::numbered(self : Record, seq : Int64) -> Record

    The same record numbered seq.

    Record::of

    fn Record::of(level : Level, target : String, msg : String) -> Record

    A record with everything but its level, target and message left empty.

    Record::on

    fn Record::on(self : Record, thread : String) -> Record

    The same record carrying a thread name.

    Record::say

    fn Record::say(self : Record, msg : String) -> Record

    The same record with another message.

    Record::set

    fn Record::set(self : Record, key : String, value : Value) -> Record

    The same record with key set: an earlier field of that name takes the new value where it stands, and a new name is appended, so the order a reader sees is scope, then logger, then the call.

    Record::sets

    fn Record::sets(self : Record, pairs : Array[(String, Value)]) -> Record

    The same record with every pair set in turn.

    Record::stamped

    fn Record::stamped(self : Record, time : Int64) -> Record

    The same record at an instant, in nanoseconds since the epoch.

    Record::tag

    fn Record::tag(self : Record, tag : String) -> Record

    The same record carrying one more tag.

    Record::tagged

    fn Record::tagged(self : Record, tag : String) -> Bool

    Whether the record carries tag.

    Record::unset

    fn Record::unset(self : Record, key : String) -> Record

    The same record with key gone.

    Redact

    pub struct Redact {
    keys : Array[String]
    paths : Array[Array[String]]
    patterns : Array[
    Regex
    ]
    mask : String
    }

    Masks fields by name at any depth or at a dotted path, and parts of the message that match a pattern.
    impl Processor for Redact

    Redact::key

    fn Redact::key(self : Redact, key : String) -> Redact

    Masks these names wherever they appear.

    Redact::mask

    fn Redact::mask(self : Redact, mask : String) -> Redact

    What a masked value becomes; *** unless set.

    Redact::new

    fn Redact::new() -> Redact

    A step that masks nothing yet.

    Redact::path

    fn Redact::path(self : Redact, path : String) -> Redact

    Masks the field at a dotted path, such as req.headers.authorization.

    Redact::pattern

    fn Redact::pattern(self : Redact, pattern : StringView) -> Redact raise Refused

    Replaces whatever matches in the message.

    Redact::process

    fn Redact::process(self : Redact, record : Record, _env : Env) -> Record?

    Report

    pub(all) struct Report {
    fault : Fault?
    phase : Phase?
    }

    What a delivery did: the failure it met, if any, and the phase an alarm is owed, if any. A recovery is a phase too, which is why this is not a Result.

    Rule

    type Rule

    One directive of a filter: a target and the level it lets through, or None for off.

    Sample

    pub struct Sample {
    first : Int
    every : Int
    per : Int64
    seen : Map[String, (Int64, Int)]
    }

    Keeps the first records of each level and message every period, then one in so many after that.
    impl Processor for Sample

    Sample::new

    fn Sample::new(first~ : Int, every? : Int, per~ : Int64) -> Sample

    Keeps the first first records per per nanoseconds, then one in every.

    Sample::process

    fn Sample::process(self : Sample, record : Record, env : Env) -> Record?

    Sanitise

    pub struct Sanitise {
    }

    Escapes line breaks, tabs and other control characters, so a message cannot forge a second line.

    Sanitise::new

    fn Sanitise::new() -> Sanitise

    The step; it has nothing to configure.

    Sanitise::process

    fn Sanitise::process(_self : Sanitise, record : Record, _env : Env) -> Record?

    Scene

    pub(all) struct Scene {
    profile : Profile
    colour : Bool
    identity : Identity
    }

    How a record is shown: whose vocabulary the level is written in, whether a terminal takes colour, and who is writing.

    Scene::at

    fn Scene::at(self : Scene, profile : Profile) -> Scene

    The same scene at another profile.

    Scene::coloured

    fn Scene::coloured(self : Scene, colour : Bool) -> Scene

    The same scene with colour turned on or off.

    Scene::new

    fn Scene::new(identity? : Identity, profile? : Profile, colour? : Bool) -> Scene

    A plain scene: no colour, the default vocabulary, an identity that names nobody.

    Schema

    pub(all) enum Schema {
    Serlog
    Pino
    Bunyan
    Zap
    Slog
    Structlog
    Logstash
    Ecs
    Gelf
    Otel
    } derive(Eq,
    Debug
    )

    A JSON shape other tools already read.

    Schema::equal

    fn Schema::equal(Schema, Schema) -> Bool

    Schema::name

    fn Schema::name(self : Schema) -> StringView

    The name used in configuration files and vector files.

    Schema::not_equal

    fn Schema::not_equal(x : Schema, y : Schema) -> Bool

    Schema::parse

    fn Schema::parse(name : StringView) -> Schema raise Refused

    A schema by name: serlog, pino, bunyan, zap, slog, structlog, logstash, ecs, gelf or otel.

    Schema::to_repr

    Slot

    pub struct Slot {
    name : String
    kind : String
    sink : &Sink
    filter : Filter?
    chain : Array[&Processor]
    layout : &Layout
    layout_given : Bool
    colour : Bool
    alarm_only : Bool
    on_fail : OnFail
    queue : Queue?
    retries : Int
    breaker : Breaker
    dropped : Int64
    suppressed : Int64
    buffered : Array[Record]
    buffered_at : Int64
    }

    One sink with the settings around it: name, filter, processors, layout, colour, queue, retries, health and the policy for failure.

    A slot is a value an application builds and hands to a hub. Nothing about it is process-wide, so two hubs can hold two slots writing the same file in two formats and neither knows about the other.

    Slot::alarm_only

    fn Slot::alarm_only(self : Slot) -> Slot

    Keeps it off the log route, for alarms only.

    Slot::at

    fn Slot::at(self : Slot, min : Level) -> Slot

    Takes only min and above.

    Slot::coloured

    fn Slot::coloured(self : Slot, colour : Bool) -> Slot

    Whether this slot's lines are coloured.

    Slot::failed_since

    fn Slot::failed_since(self : Slot) -> Int64

    When the current failure began, for the incident an alarm carries.

    Slot::filter

    fn Slot::filter(self : Slot, filter : Filter) -> Slot

    Takes only what filter lets through.

    Slot::kind

    fn Slot::kind(self : Slot, kind : String) -> Slot

    Names the kind of sink this is, for a status page: memory, file, loki, or whatever a sink of your own calls itself. custom until it is said.

    Slot::layout

    fn Slot::layout(self : Slot, layout : &Layout) -> Slot

    Lays entries out this way instead of the profile's pattern.

    Slot::name

    fn Slot::name(self : Slot) -> String

    The slot's name.

    Slot::new

    fn Slot::new(sink : &Sink, name~ : String) -> Slot

    A slot named name writing to sink, with the library's own pattern as its layout until one is given.

    Slot::on_fail

    fn Slot::on_fail(self : Slot, on_fail : OnFail) -> Slot

    What happens when the sink fails.

    Slot::probe

    fn Slot::probe(self : Slot, probe : Int64) -> Slot

    First wait before probing a failed sink; it doubles up to five minutes.

    Slot::process

    fn Slot::process(self : Slot, step : &Processor) -> Slot

    Adds a step that runs for this slot only.

    Slot::queue

    fn Slot::queue(self : Slot, queue : Queue) -> Slot

    Runs the sink behind queue.

    Slot::retries

    fn Slot::retries(self : Slot, retries : Int) -> Slot

    Tries a failed write this many more times, without pausing.

    Slot::status

    fn Slot::status(self : Slot) -> Status

    Where it stands.

    Status

    pub(all) struct Status {
    name : String
    kind : String
    health : Health
    skipped : Int64
    dropped : Int64
    suppressed : Int64
    since : Int64
    error : String?
    }

    A slot's state, for a status page.

    Stream

    pub(all) enum Stream {
    Stdout
    Stderr
    Auto
    } derive(Eq,
    Debug
    )

    Where a console writes.

    Stream::equal

    fn Stream::equal(Stream, Stream) -> Bool

    Stream::name

    fn Stream::name(self : Stream) -> StringView

    The name used in configuration files.

    Stream::not_equal

    fn Stream::not_equal(x : Stream, y : Stream) -> Bool

    Stream::parse

    fn Stream::parse(name : StringView) -> Stream raise Refused

    stdout, stderr or auto.

    Stream::to_repr

    Tell

    pub(all) enum Tell {
    Report
    Silent
    Folded
    } derive(Eq,
    Debug
    )

    How a failure is told.

    Tell::equal

    fn Tell::equal(Tell, Tell) -> Bool

    Tell::not_equal

    fn Tell::not_equal(x : Tell, y : Tell) -> Bool

    Tell::to_repr

    Tiers

    pub struct Tiers {
    tiers : Array[Array[String]]
    fallback : Fallback
    remind : Int64
    }

    Fixed tiers of sink names: [["loki"], ["oncall"], ["stderr"]].
    impl Alarm for Tiers

    Tiers::fallback

    fn Tiers::fallback(self : Tiers) -> Fallback

    Tiers::from

    fn Tiers::from(tiers : Array[Array[String]]) -> Tiers

    Tiers of names, tried in order.

    Tiers::new

    fn Tiers::new() -> Tiers

    The default alarm: standard error alone, one hour between reminders.

    Tiers::remind

    fn Tiers::remind(self : Tiers) -> Int64

    Tiers::route

    fn Tiers::route(self : Tiers, _incident : Incident) -> Array[Array[String]]

    Tiers::tiers

    fn Tiers::tiers(self : Tiers) -> Array[Array[String]]

    The tiers themselves.

    Tiers::with_fallback

    fn Tiers::with_fallback(self : Tiers, fallback : Fallback) -> Tiers

    The same tiers, with fallback deciding what happens when no tier took the incident.

    Tiers::with_remind

    fn Tiers::with_remind(self : Tiers, remind : Int64) -> Tiers

    How long a sink may stay down before it is reported again.

    Value

    pub(all) enum Value {
    Null
    Bool(Bool)
    Int(Int64)
    Float(Double)
    Str(String)
    List(Array[Value])
    Map(Array[(String, Value)])
    Error(String, Array[String])
    Other(String)
    } derive(
    Debug
    )

    A field's value, typed.

    Fields are carried as values rather than as text so a layout decides how they read: 7 and "7" are the same three characters on a line and different things in a JSON document.

    Value::encode

    fn Value::encode(self : Value) -> String

    The value as JSON.

    Value::of_bool

    fn Value::of_bool(b : Bool) -> Value

    A Bool.

    Value::of_error

    fn Value::of_error(message~ : String, causes? : Array[String]) -> Value

    An error with its chain of causes, the way error travels as a field.

    Value::of_float

    fn Value::of_float(x : Double) -> Value

    A Float.

    Value::of_int

    fn Value::of_int(n : Int) -> Value

    An Int, widened to 64 bits.

    Value::of_list

    fn Value::of_list(items : Array[Value]) -> Value

    A List of values.

    Value::of_long

    fn Value::of_long(n : Int64) -> Value

    An Int from a 64-bit integer.

    Value::of_map

    fn Value::of_map(pairs : Array[(String, Value)]) -> Value

    A Map, its pairs kept in the order given.

    Value::of_null

    fn Value::of_null() -> Value

    Null.

    Value::of_other

    fn Value::of_other(text : String) -> Value

    Anything else, shown as text.

    Value::of_str

    fn Value::of_str(s : StringView) -> Value

    A Str.

    Value::to_repr

    Value::to_text

    fn Value::to_text(self : Value) -> String

    The text form: bare strings, [a, b] for a list, {k=v} for a map, and an error with its causes after colons.

    Value::write_json

    fn Value::write_json(self : Value, out : StringBuilder) -> Unit

    Writes the value as JSON into out.

    batch_default

    let batch_default : Int

    How many records a queue writes at once when nothing says otherwise.

    Chosen here rather than taken from a case file: a thousand lines is one sensible write, and a queue that is told otherwise says so.

    built_in

    fn built_in() -> Array[Level]

    The six built-in levels, quietest first.

    debug

    let debug : Level

    Diagnostics for developers.

    error

    let error : Level

    A failure that needs attention.

    fatal

    let fatal : Level

    A failure the program cannot carry on from.

    info

    let info : Level

    Normal operation.

    longest_probe

    let longest_probe : Int64

    Five minutes: the longest a failed sink waits between probes.

    The cap the health cases stop at: the wait doubles up to this and no further, however long the outage lasts.

    probe_default

    let probe_default : Int64

    Five seconds: the first wait before a failed sink is probed again.

    The interval the health cases are written with, so a slot built with the defaults is the slot those cases describe.

    remind_default

    let remind_default : Int64

    One hour: how long an alarm stays quiet about a sink that is still down.

    The interval the alarm cases are written with, so a tier built with the defaults reports a long outage at the same moments they describe.

    reset

    let reset : String

    The escape that ends every colour.

    sound

    fn sound(alarm : &Alarm, incident : Incident, desk : &Desk) -> Outcome

    Delivers incident through alarm's tiers, adding every failed delivery to it.

    The failing sink is never asked. * stands for every sink on the log route. A sink already tried is not tried again, and a resting one is left out. One success ends the walk; when no tier takes it, nothing more happens here.

    trace

    let trace : Level

    Step-by-step detail.

    wait_default

    let wait_default : Int64

    One second: how long a queue waits for its batch to fill.

    Chosen here: long enough to gather a burst, short enough that a quiet service still sees its last line promptly.

    warn

    let warn : Level

    Something unexpected that the program survived.