≡ moonlog 0.1.1

API reference

Everything the library exports. The root package is grouped by what a name does rather than alphabetically: a reader looking for a name usually knows which part of the pipeline it belongs to. The six subpackages follow, each one a section of its own. Times are nanoseconds since the epoch and durations the same; a text that is not a String is a StringView. Every value type derives Eq and Debug, so equal, not_equal and to_repr exist wherever a value does; they are not listed one by one. This page is generated from the interfaces.

Levels

typeColourPlain · Red · Green · Yellow · Blue · Magenta · Cyan · White · Grey · Alarm

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.

fnColour::ansi(Self) -> String

The escape that starts this colour, empty for Plain.

fnColour::name(Self) -> StringView

The name used in configuration, filters and vector files.

fnColour::parse(StringView) -> Self raise Refused

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

typeLevelname : String, value : Int32, 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. Implements Compare, Eq, Show.

fnLevel::at_least(Self, Self) -> Bool

Whether this level passes a minimum.

fnLevel::colour(Self) -> Colour

Its colour in a terminal.

fnLevel::label(Self, 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.

fnLevel::logstash(Self) -> Int32

The number Logstash writes, 5000 to 40000.

fnLevel::name(Self) -> String

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

fnLevel::named(Self, String) -> Self

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

fnLevel::otel(Self) -> Int32

The severity number of the OpenTelemetry log model.

fnLevel::parse(StringView) -> Self 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.

fnLevel::syslog(Self) -> Int32

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

fnLevel::value(Self) -> Int32

Its number.

fnbuilt_in() -> Array[Level]

The six built-in levels, quietest first.

letdebugLevel

Diagnostics for developers.

leterrorLevel

A failure that needs attention.

letfatalLevel

A failure the program cannot carry on from.

letinfoLevel

Normal operation.

letresetString

The escape that ends every colour.

lettraceLevel

Step-by-step detail.

letwarnLevel

Something unexpected that the program survived.

Values and records

typeCallermodule_path : String?, func : String?, file : String?, line : Int32?

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.

fnCaller::new() -> Self

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

typeIdentityapp : String, env : String, instance : String, host : String, pid : Int32, start : Int64, offset : (Int64) -> Int32

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.

fnIdentity::new() -> Self

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

fnIdentity::of(app~ : String, env? : String, instance? : String, host? : String, pid? : Int32, start? : Int64, offset? : (Int64) -> Int32) -> Self

An identity with the parts a deployment knows.

typeRecordmut level : Level, target : String, mut msg : String, mut fields : Array[(String, Value)], tags : Array[String], mut caller : Caller, mut thread : String?, mut spans : String?, mut time : Int64, mut 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.

fnRecord::at(Self, Level) -> Self

The same record at another level.

fnRecord::copy(Self) -> Self

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

fnRecord::field(Self, String) -> Value?

The last field called key.

fnRecord::has(Self, String) -> Bool

Whether any field is called key.

fnRecord::located(Self, String, line? : Int32) -> Self

The same record carrying a source file and line.

fnRecord::numbered(Self, Int64) -> Self

The same record numbered seq.

fnRecord::of(Level, String, String) -> Self

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

fnRecord::on(Self, String) -> Self

The same record carrying a thread name.

fnRecord::say(Self, String) -> Self

The same record with another message.

fnRecord::set(Self, String, Value) -> Self

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.

fnRecord::sets(Self, Array[(String, Value)]) -> Self

The same record with every pair set in turn.

fnRecord::stamped(Self, Int64) -> Self

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

fnRecord::tag(Self, String) -> Self

The same record carrying one more tag.

fnRecord::tagged(Self, String) -> Bool

Whether the record carries tag.

fnRecord::unset(Self, String) -> Self

The same record with key gone.

typeValueNull · Bool(Bool) · Int(Int64) · Float(Double) · Str(String) · List(Array[Value]) · Map(Array[(String, Value)]) · Error(String, Array[String]) · Other(String)

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.

fnValue::encode(Self) -> String

The value as JSON.

fnValue::of_bool(Bool) -> Self

A Bool.

fnValue::of_error(message~ : String, causes? : Array[String]) -> Self

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

fnValue::of_float(Double) -> Self

A Float.

fnValue::of_int(Int32) -> Self

An Int, widened to 64 bits.

fnValue::of_list(Array[Self]) -> Self

A List of values.

fnValue::of_long(Int64) -> Self

An Int from a 64-bit integer.

fnValue::of_map(Array[(String, Self)]) -> Self

A Map, its pairs kept in the order given.

fnValue::of_null() -> Self

Null.

fnValue::of_other(String) -> Self

Anything else, shown as text.

fnValue::of_str(StringView) -> Self

A Str.

fnValue::to_text(Self) -> String

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

fnValue::write_json(Self, StringBuilder) -> Unit

Writes the value as JSON into out.

Errors

typeRefusedRefused(at~ : Int32, 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.

fnRefused::to_text(Self) -> String

The refusal as one line, for a terminal.

Filters

typeFiltermut base : Level?, rules : Array[Rule], mut pattern : @string.Regex?, mut 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.

fnFilter::admits(Self, Level, String, String) -> Bool

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

fnFilter::allows(Self, Level, String) -> Bool

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

fnFilter::at(Level) -> Self

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

fnFilter::has_pattern(Self) -> Bool

Whether there is a /regex part.

fnFilter::lowest(Self) -> Level?

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

fnFilter::off() -> Self

A filter that lets nothing through.

fnFilter::parse(StringView) -> Self raise Refused

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

fnFilter::to_text(Self) -> String

The spec this filter reads back as.

Steps

typeDedupewithin : Int64, seen : Map[String, (Int64, Int32)]

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

fnDedupe::new(within~ : Int64) -> Self

Drops repeats kept within within nanoseconds of each other.

fnDedupe::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

typeEnvnow : 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.

fnEnv::at(Int64) -> Self

The environment at now, in nanoseconds since the epoch.

typeExcludekeep : Keep

Drops a record when every condition holds. Implements Processor.

fnExclude::field(Self, String, String) -> Self

Requires a field whose text is value.

fnExclude::level(Self, Level) -> Self

Requires the level to be at least min.

fnExclude::new() -> Self

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

fnExclude::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

fnExclude::tag(Self, String) -> Self

Requires the record to carry tag.

typeFieldspairs : Array[(String, Value)]

Adds the same fields to every record. Implements Processor.

fnFields::new() -> Self

No fields yet; add them with set.

fnFields::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

fnFields::set(Self, String, Value) -> Self

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

typeKeeptags : Array[String], equals : Array[(String, String)], mut 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. Implements Processor.

fnKeep::field(Self, String, String) -> Self

Requires a field whose text is value.

fnKeep::holds(Self, Record) -> Bool

Whether every condition holds for record.

fnKeep::level(Self, Level) -> Self

Requires min and above.

fnKeep::new() -> Self

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

fnKeep::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

fnKeep::tag(Self, String) -> Self

Requires a tag.

typeLimitrate : Double, burst : Double, mut level : Double, mut at : Int64, mut started : Bool

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

fnLimit::new(rate~ : Double, burst~ : Double) -> Self

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

fnLimit::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

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

typeRedactkeys : Array[String], paths : Array[Array[String]], patterns : Array[@string.Regex], mut mask : String

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

fnRedact::key(Self, String) -> Self

Masks these names wherever they appear.

fnRedact::mask(Self, String) -> Self

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

fnRedact::new() -> Self

A step that masks nothing yet.

fnRedact::path(Self, String) -> Self

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

fnRedact::pattern(Self, StringView) -> Self raise Refused

Replaces whatever matches in the message.

fnRedact::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

typeSamplefirst : Int32, every : Int32, per : Int64, seen : Map[String, (Int64, Int32)]

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

fnSample::new(first~ : Int32, every? : Int32, per~ : Int64) -> Self

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

fnSample::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

typeSanitise

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

fnSanitise::new() -> Self

The step; it has nothing to configure.

fnSanitise::process(Self, Record, Env) -> Record?

As a Processor: The record, changed or as it was, or None to drop it.

Layouts

typeClockLocal · Utc · Rfc3339 · Unix · UnixMs · UnixNs · Custom(String)

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.

fnClock::parse(StringView) -> Self

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

fnClock::write(Self, StringBuilder, Int64, Int32) -> Unit

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

typeJsonLineschema : Schema, keys : Array[(String, String)], mut levels : Levels?, mut nest : String?

One record as a JSON object in a schema, with any key renamed. Implements Layout.

fnJsonLine::encode(Self, Record, Scene) -> String

Writes one record as a JSON object.

fnJsonLine::levels(Self, Levels) -> Self

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

fnJsonLine::nest(Self, String) -> Self

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

fnJsonLine::new(Schema) -> Self

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

fnJsonLine::rename(Self, String, String) -> Self

Writes key as to, such as msg as message.

fnJsonLine::render(Self, Record, Scene) -> String

As a Layout: The record as one line of text, without the line break.

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

typeLevelsName · Number

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

fnLevels::parse(StringView) -> Self raise Refused

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

typeLogfmt

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. Implements Layout.

fnLogfmt::new() -> Self

The layout; it has nothing to configure.

fnLogfmt::render(Self, Record, Scene) -> String

As a Layout: The record as one line of text, without the line break.

typePatternsource : 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. Implements Layout.

fnPattern::parse(StringView) -> Self raise Refused

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

fnPattern::render(Self, Record, Scene) -> String

As a Layout: The record as one line of text, without the line break.

fnPattern::source(Self) -> String

The text it was parsed from.

typePretty

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. Implements Layout.

fnPretty::new() -> Self

The layout; it has nothing to configure.

fnPretty::render(Self, Record, Scene) -> String

As a Layout: The record as one line of text, without the line break.

typeSchemaSerlog · Pino · Bunyan · Zap · Slog · Structlog · Logstash · Ecs · Gelf · Otel

A JSON shape other tools already read.

fnSchema::name(Self) -> StringView

The name used in configuration files and vector files.

fnSchema::parse(StringView) -> Self raise Refused

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

Sinks

typeDiscard

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

fnDiscard::new() -> Self

A sink that accepts every entry and keeps none.

fnDiscard::write(Self, Entry) -> Result[Unit, String]

As a Sink: Writes one entry; Err says why it could not.

typeEntryrecord : 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.

typeLineswrite : (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. Implements Sink.

fnLines::new((String) -> Unit) -> Self

A sink that hands each line to write.

fnLines::write(Self, Entry) -> Result[Unit, String]

As a Sink: Writes one entry; Err says why it could not.

typeMemorysize : Int32, entries : Array[Entry]

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

fnMemory::clear(Self) -> Unit

Forgets everything it kept.

fnMemory::find(Self, String) -> Array[Entry]

The entries whose line contains text.

fnMemory::lines(Self) -> Array[String]

The lines it kept, oldest first.

fnMemory::new(size? : Int32) -> Self

Keeps the newest size entries; 0 keeps everything.

fnMemory::records(Self) -> Array[Record]

The records it kept, oldest first.

fnMemory::write(Self, Entry) -> Result[Unit, String]

As a Sink: Writes one entry; Err says why it could not.

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

Slots and health

typeBreakerprobe : Int64, mut health : Health, mut gap : Int64, mut next : Int64, mut told : Int64?, mut since : Int64, mut opened : Int64, mut opened_at : Int64, mut skipped : Int64, mut 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.

fnBreaker::admit(Self, Int64) -> Bool

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

fnBreaker::error(Self) -> String?

The latest error.

fnBreaker::failed_since(Self) -> Int64

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

fnBreaker::fault(Self, Int64, String, Int64, Tell) -> Phase?

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

fnBreaker::health(Self) -> Health

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

fnBreaker::new(Int64, Int64) -> Self

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

fnBreaker::pass(Self, Int64) -> Phase?

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

fnBreaker::resting(Self, Int64) -> Bool

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

fnBreaker::since(Self) -> Int64

When the current health began.

fnBreaker::skipped(Self) -> Int64

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

fnBreaker::skipped_now(Self) -> Int64

Records skipped since the current failure began.

fnBreaker::wait(Self, Int64) -> Int64

How long until the next probe.

typeFaulterror : 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.

typeFullDrop · Block

What a full queue does with a record that arrives.

typeHealthUp · Down · Probing

Whether a sink is working.

fnHealth::name(Self) -> StringView

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

typeOnFailPanic · Ignore · Alarm(&Alarm)

What a slot does when its sink fails.

typeQueuesize : Int32, batch : Int32, 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.

fnQueue::batch(Self, Int32) -> Self

The same queue, written batch records at a time.

fnQueue::full(Self, Full) -> Self

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

fnQueue::new(size~ : Int32) -> Self

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

fnQueue::wait(Self, Int64) -> Self

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

typeReportfault : 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.

typeSlotname : String, mut kind : String, sink : &Sink, mut filter : Filter?, chain : Array[&Processor], mut layout : &Layout, mut layout_given : Bool, mut colour : Bool, mut alarm_only : Bool, mut on_fail : OnFail, mut queue : Queue?, mut retries : Int32, mut breaker : Breaker, mut dropped : Int64, mut suppressed : Int64, buffered : Array[Record], mut 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.

fnSlot::alarm_only(Self) -> Self

Keeps it off the log route, for alarms only.

fnSlot::at(Self, Level) -> Self

Takes only min and above.

fnSlot::coloured(Self, Bool) -> Self

Whether this slot's lines are coloured.

fnSlot::failed_since(Self) -> Int64

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

fnSlot::filter(Self, Filter) -> Self

Takes only what filter lets through.

fnSlot::kind(Self, String) -> Self

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.

fnSlot::layout(Self, &Layout) -> Self

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

fnSlot::name(Self) -> String

The slot's name.

fnSlot::new(&Sink, name~ : String) -> Self

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

fnSlot::on_fail(Self, OnFail) -> Self

What happens when the sink fails.

fnSlot::probe(Self, Int64) -> Self

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

fnSlot::process(Self, &Processor) -> Self

Adds a step that runs for this slot only.

fnSlot::queue(Self, Queue) -> Self

Runs the sink behind queue.

fnSlot::retries(Self, Int32) -> Self

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

fnSlot::status(Self) -> Status

Where it stands.

typeStatusname : String, kind : String, health : Health, skipped : Int64, dropped : Int64, suppressed : Int64, since : Int64, error : String?

A slot's state, for a status page.

typeTellReport · Silent · Folded

How a failure is told.

letbatch_defaultInt32

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.

letlongest_probeInt64

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.

letprobe_defaultInt64

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.

letwait_defaultInt64

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.

Alarms

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

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

typeFailuresink : String, error : String, at : Int64

One failed delivery, on the way to an alarm.

typeFallbackPanic · Ignore

What an alarm does when no tier took the incident.

typeIncidentsink : String, kind : String, phase : Phase, error : String, failures : Array[Failure], mut skipped : Int64, mut dropped : Int64, mut since : Int64, mut 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.

fnIncident::describe(Self, target? : String) -> Record

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

fnIncident::new(String, String, Phase, String, Int64) -> Self

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

typeOutcomeHeard · Unheard

How an alarm walk ended.

typePhaseOpened · Reminder · Resolved

Which report of an incident this is.

fnPhase::name(Self) -> StringView

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

typeTierstiers : Array[Array[String]], fallback : Fallback, remind : Int64

Fixed tiers of sink names: [["loki"], ["oncall"], ["stderr"]]. Implements Alarm.

fnTiers::fallback(Self) -> Fallback

As a Alarm: What happens when no tier took the incident.

fnTiers::from(Array[Array[String]]) -> Self

Tiers of names, tried in order.

fnTiers::new() -> Self

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

fnTiers::remind(Self) -> Int64

As a Alarm: How long a sink may stay down before it is reported again, in nanoseconds.

fnTiers::route(Self, Incident) -> Array[Array[String]]

As a Alarm: The tiers of sink names to try for this incident, in order.

fnTiers::tiers(Self) -> Array[Array[String]]

The tiers themselves.

fnTiers::with_fallback(Self, Fallback) -> Self

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

fnTiers::with_remind(Self, Int64) -> Self

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

letremind_defaultInt64

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.

fnsound(&Alarm, Incident, &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.

Hub and logger

typeHubidentity : Identity, profile : Profile, mut filter : Filter, chain : Array[&Processor], slots : Array[Slot], layout : &Layout, clock : () -> Int64, mut seq : Int64, mut inside : Bool, mut writing : Int32, mut 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. Implements Desk.

fnHub::add(Self, 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.

fnHub::deliver(Self, String, Record) -> Result[Unit, String]

As a Desk: Writes record to the named sink at once, bypassing its queue. Err is what the sink said, or that no sink has that name.

fnHub::dispatch(Self, 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.

fnHub::enabled(Self, 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.

fnHub::filter(Self) -> Filter

The filter now in force.

fnHub::flush(Self) -> Unit

Writes what every queue holds, up to now.

fnHub::identity(Self) -> Identity

The identity records are stamped with.

fnHub::logger(Self, String) -> Logger

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

fnHub::logs(Self) -> Array[String]

As a Desk: The sinks on the log route, which * stands for.

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

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.

fnHub::now(Self) -> Int64

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

fnHub::process(Self, &Processor) -> Unit

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

fnHub::profile(Self) -> Profile

The profile in force.

fnHub::remove(Self, String) -> Bool

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

fnHub::resting(Self, String) -> Bool

As a Desk: Whether a sink is down and not yet due for a probe.

fnHub::scene(Self) -> Scene

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

fnHub::set_filter(Self, Filter) -> Unit

Replaces the global filter; takes effect at once.

fnHub::set_level(Self, Level) -> Unit

Lets through min and above for every target.

fnHub::slots(Self) -> Array[Slot]

Every registered slot, in the order they were added.

fnHub::status(Self) -> Array[Status]

How every slot stands.

fnHub::suppressed(Self) -> Int64

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

fnHub::write(Self, Level, String, String, fields? : Array[(String, Value)]) -> Unit

Writes a record at level from target.

typeLoggerhub : 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.

fnLogger::bound(Self, String, Value) -> Self

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

fnLogger::debug(Self, String, fields? : Array[(String, Value)]) -> Unit

Writes at debug.

fnLogger::enabled(Self, 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.

fnLogger::error(Self, String, fields? : Array[(String, Value)]) -> Unit

Writes at error.

fnLogger::fatal(Self, 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.

fnLogger::info(Self, String, fields? : Array[(String, Value)]) -> Unit

Writes at info.

fnLogger::log(Self, Level, String, fields? : Array[(String, Value)]) -> Unit

Writes a message, with fields if there are any.

fnLogger::new(Hub, String) -> Self

A logger writing to hub under target.

fnLogger::target(Self) -> String

The target it writes under.

fnLogger::trace(Self, String, fields? : Array[(String, Value)]) -> Unit

Writes at trace.

fnLogger::under(Self, String) -> Self

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

fnLogger::warn(Self, String, fields? : Array[(String, Value)]) -> Unit

Writes at warn.

typeSceneprofile : 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.

fnScene::at(Self, Profile) -> Self

The same scene at another profile.

fnScene::coloured(Self, Bool) -> Self

The same scene with colour turned on or off.

fnScene::new(identity? : Identity, profile? : Profile, colour? : Bool) -> Self

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

Profiles

typeProfileSerlog · Tracing · EnvLogger · Python · Logback · Slog · Pino

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.

fnProfile::env(Self) -> Array[String]

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

fnProfile::level(Self) -> Level

The level this profile starts at.

fnProfile::name(Self) -> StringView

The name used in configuration files.

fnProfile::parse(StringView) -> Self raise Refused

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

fnProfile::pattern(Self) -> StringView

The pattern this profile's logger writes by default.

fnProfile::stream(Self) -> Stream

Where this profile's console writes.

typeStreamStdout · Stderr · Auto

Where a console writes.

fnStream::name(Self) -> StringView

The name used in configuration files.

fnStream::parse(StringView) -> Self raise Refused

stdout, stderr or auto.

moonlog/stdio

A console on standard output, standard error, or both.

typeConsolestream : @moonlog.Stream, colour : Bool, from : @moonlog.Level

A sink that writes each entry as one line on the console. Auto sends a record to standard error when its level reaches from and to standard output otherwise, which is what a pipeline wants: the log stream can be redirected while failures still reach the terminal. Whether the line carries colour is settled once, when the console is built, and the slot has to be told the same answer: Console::slot does both. Implements Sink.

fnConsole::coloured(Self) -> Bool

Whether this console's lines carry escapes.

fnConsole::from(Self) -> @moonlog.Level

The level at which Auto turns to standard error.

fnConsole::new(stream? : @moonlog.Stream, colour? : Bool, from? : @moonlog.Level) -> Self

A console on stream. colour decides whether the slot writes escapes: leave it out and the console asks the environment and the terminal (see decide). from is the level at which Auto starts using standard error; every level from error up unless it is given.

fnConsole::slot(Self, name? : String) -> @moonlog.Slot

A slot around this console, already coloured the way the console decided.

fnConsole::stream(Self) -> @moonlog.Stream

The stream this console was built for.

fnConsole::target(Self, @moonlog.Level) -> @moonlog.Stream

The stream a record at level is written to.

fnConsole::write(Self, @moonlog.Entry) -> Result[Unit, String]
fndecide(Bool, Bool, Bool?) -> Bool

Whether colour is wanted, given what the terminal said and what the environment said. FORCE_COLOR is the last word — someone who set it meant it — then NO_COLOR, then the terminal.

fnforce_color() -> Bool?

FORCE_COLOR, when it says something: 0 and false turn colour off, any other value turns it on, and an unset variable says nothing.

fnno_color() -> Bool

Whether NO_COLOR is set to anything but the empty string, which is what the convention at no-color.org asks for.

fnterminal(@moonlog.Stream) -> Bool

Whether a slot writing to stream should carry escapes. Auto asks for both streams, so a run whose log is piped into a file gets plain lines even though its failures still reach the terminal.

moonlog/file

A file that rotates itself.

typeFilepath : String, append : Bool, perms : Int32, mkdir : Bool, rotation : Rotation, limits : Limits, compress : Bool, buffer : Int32, clock : () -> Int64, mut handle : Int32?, mut written : Int64, mut opened : Int64, held : @buffer.Buffer

A sink that appends each entry to a file, rotating it when it grows too large or too old. The file is opened on the first write, so a sink that cannot open its file is a failing slot rather than a failure at startup: the health machine puts the sink down, the alarm says why, and a probe retries. reopen picks up a file that an outside tool moved away. Segments are named <file>.<20261101T090000>, or .gz too when compress is on, and are pruned by Limits after every cut. Names are UTC, so a directory listing reads as a history. Implements Sink.

fnFile::close(Self) -> Unit

Writes what the buffer holds, then gives the file back to the system. A sink that is buffered should be closed, or its last lines are lost.

fnFile::flush(Self) -> Result[Unit, String]

Writes what the buffer holds.

fnFile::new(path~ : String, append? : Bool, perms? : Int32, mkdir? : Bool, rotation? : Rotation, limits? : Limits, compress? : Bool, buffer? : Int32, clock? : () -> Int64) -> Self

A file sink on path. append keeps what is there (the default) and false starts the file empty. perms is the mode a new file gets, in the usual octal form. mkdir creates the parent directories. buffer gathers that many bytes before writing, which trades a lost tail on a crash for fewer calls; 0 writes every line as it arrives. clock answers nanoseconds since the epoch. A file sink needs one for segment names and time-based rotation, so it reads the system clock unless it is given another.

fnFile::path(Self) -> String

The file this sink writes.

fnFile::reopen(Self) -> Result[Unit, String]

Closes the file and opens it again, which is what an outside rotate wants: the old descriptor keeps writing to a file that was moved, this one writes to the path again.

fnFile::rotate(Self) -> Result[Unit, String]

Cuts the file now, whether or not a limit was reached: the current file becomes a segment and a fresh one is started.

fnFile::segments(Self) -> Array[String]

The segments of this file, oldest first. A segment is a file this sink cut and left behind: what a status page lists, and what keep, max_age and max_total count.

fnFile::slot(Self, name? : String) -> @moonlog.Slot

A slot around this sink.

fnFile::write(Self, @moonlog.Entry) -> Result[Unit, String]
fnFile::written(Self) -> Int64

How many bytes the current file holds.

typeLimitskeep : Int32, max_age : Int64, max_total : Int64

What becomes of the files rotation leaves behind. All three are ceilings: zero keeps everything. max_age and max_total are only applied to segments, never to the file being written.

fnLimits::keep(Self, Int32) -> Self

The same limits, keeping at most keep segments.

fnLimits::max_age(Self, Int64) -> Self

The same limits, dropping segments older than max_age nanoseconds.

fnLimits::max_total(Self, Int64) -> Self

The same limits, dropping the oldest segments until the rest fit in max_total bytes.

fnLimits::new(keep? : Int32, max_age? : Int64, max_total? : Int64) -> Self

Keep every segment, for ever.

typeRotationsize : Int64, every : Int64

When a file is rotated. size is a limit in bytes and every a span in nanoseconds; zero turns either one off. A file that has been written to rotates before the line that would cross the limit, so a line never straddles two files.

fnRotation::every(Self, Int64) -> Self

The same rotation, with a time limit.

fnRotation::new(size? : Int64, every? : Int64) -> Self

No rotation: the file grows until something else deals with it.

fnRotation::size(Self, Int64) -> Self

The same rotation, with a size limit.

moonlog/loki

Entries pushed to Loki.

typeLimitscapacity : Int32, batch : Int32, timeout : Int32

How much a Loki sink holds while it waits for its next push, and how much goes in one request. capacity is a ceiling on entries waiting to be sent; when the queue is full the oldest goes, because a shipper that stops the program it logs for is worse than one that loses the start of an outage. timeout is in seconds and applies to one request.

fnLimits::batch(Self, Int32) -> Self

The same limits, pushng a different number of entries at a time.

fnLimits::capacity(Self, Int32) -> Self

The same limits, holding a different number of entries.

fnLimits::new(capacity? : Int32, batch? : Int32, timeout? : Int32) -> Self

Eight thousand entries, which is a few seconds of a busy service, and one request's worth of them.

fnLimits::timeout(Self, Int32) -> Self

The same limits, waiting a different number of seconds for an answer.

typeLokiurl : String, labels : Array[(String, String)], tenant : String?, auth : (String, String)?, limits : Limits, clock : () -> Int64, held : Array[Pending], mut sent : Int64, mut rejected : Int64, mut dropped : Int64, mut failures : Int64

A sink that ships entries to Loki. write only queues: a log call must not wait for a network. push does the sending, from an async context the application already runs, so the retry that a busy Loki asks for lives in the caller's loop — and the slot's own queue, retries and health machine still govern how the sink is treated when a push keeps failing. Each level gets a stream of its own, so a query can select on it without parsing a line, and the entry's fields travel as structured metadata. Implements Sink.

fnLoki::body(Self) -> String

The push body the pending entries would make, without taking them. What a check before a deployment prints, and what a test reads instead of standing up a server.

fnLoki::dropped(Self) -> Int64

How many were dropped because the queue was full.

fnLoki::encode(Self, Array[Pending]) -> String

The push body: one stream per level, each with its own labels.

fnLoki::failures(Self) -> Int64

How many pushes came back with something to retry.

fnLoki::new(url~ : String, labels? : Array[(String, String)], tenant? : String, auth? : (String, String), limits? : Limits, clock? : () -> Int64) -> Self

A sink pushing to url, which is the Loki base address: /loki/api/v1/push is appended unless the address already ends with it. labels are the stream's own, such as the job and the host; the level is added per stream. auth is a user and password for basic authentication, and tenant is sent as X-Scope-OrgID for a multi-tenant Loki.

fnLoki::pending(Self) -> Int32

How many entries are waiting.

fnLoki::rejected(Self) -> Int64

How many Loki refused for good.

fnLoki::sent(Self) -> Int64

How many entries Loki has taken.

fnLoki::slot(Self, name? : String) -> @moonlog.Slot

A slot around this sink.

fnLoki::url(Self) -> String

The address entries are pushed to.

fnLoki::write(Self, @moonlog.Entry) -> Result[Unit, String]
typeReportsent : Int32, rejected : Int32, retry : String?

What one push did. A batch either lands, is refused for good, or is worth trying again: Loki answers 400 for an entry it will never take — a timestamp outside the tenant's window is the usual one — and 429 or 5xx when it is merely busy. Sending the first kind again would spin for ever, so those entries are dropped and counted, and only the second kind comes back to the queue.

fnReport::idle() -> Self

Nothing was pending, so nothing was tried.

fnReport::pushed(Self) -> Bool

Whether the push asked to be repeated.

typeVerdictAccepted · Refused · Retry

What to do with a batch that came back as status.

fnVerdict::of(Int32) -> Self

How a status is read: 2xx landed, 400 and 404 will never land, everything else — 408, 429, 5xx — is worth another try.

moonlog/mail

Records mailed as one letter.

typeMailhost : String, port : Int32, security : Security, tls : @tls.Client?, auth : (String, String)?, from : String, to : Array[String], subject : String, level : @moonlog.Level, window : Int64, at_most : Int32, capacity : Int32, clock : () -> Int64, held : Array[Held], mut sent : Int64, mut dropped : Int64, mut failures : Int64

A sink that mails the records a deployment cannot afford to lose. It is the mail handler a service already knows: a level of its own, a window in which a burst becomes one mail, and a ceiling on how many records one mail carries. write only queues — a log call never waits for SMTP — and send does the talking, from the application's own async loop. When the server is down the mail stays queued, and the slot's retries, health machine and alarm take over from there. Implements Sink.

fnMail::at_most(Self) -> Int32

How many records one mail carries.

fnMail::draft(Self) -> @moonmail.Message raise @moonmail.Refused

The mail the pending records would become, without sending it. What a check before a deployment prints, and what a test reads instead of standing up a server.

fnMail::dropped(Self) -> Int64

How many records were dropped because the queue was full.

fnMail::due(Self, Int64) -> Bool

Whether a batch is ready at now: the oldest record has waited out the window, or there are enough of them to fill a mail.

fnMail::failures(Self) -> Int64

How many sends failed and left their records queued.

fnMail::html_for(Self, Array[Held]) -> String

The HTML body: the same lines in a table, with the fields that came with them, so an inbox shows the same thing a console does.

fnMail::letter(Self, Array[Held]) -> @moonmail.Message raise @moonmail.Refused

The mail a batch becomes, ready to encode.

fnMail::level(Self) -> @moonlog.Level

The floor this sink mails at.

fnMail::new(host~ : String, from~ : String, to~ : Array[String], port? : Int32, security? : Security, tls? : @tls.Client, auth? : (String, String), subject? : String, level? : @moonlog.Level, window? : Int64, at_most? : Int32, capacity? : Int32, clock? : () -> Int64) -> Self

A sink mailing through host. from is the envelope sender and to the recipients, both as addresses. subject is a template: {count} (a number), {records} (the number and the noun that goes with it), {level} and {target} are filled in from the batch. level is the floor this sink is interested in — it mails what a slot would already have let through, but a sink built beside a console slot gets the whole stream, and this is what keeps a queued warning out of the on-call inbox. security decides how the connection is protected; StartTls and Tls need tls, a client with its own trust store, because which roots to trust is the application's decision and not this sink's.

fnMail::pending(Self) -> Int32

How many records are waiting.

fnMail::sent(Self) -> Int64

How many records have been mailed.

fnMail::slot(Self, name? : String) -> @moonlog.Slot

A slot around this sink.

fnMail::subject_for(Self, Array[Held]) -> String

The subject a batch is mailed under.

fnMail::text_for(Self, Array[Held]) -> String

The plain text body: one line per record, as the slots wrote them.

fnMail::window(Self) -> Int64

How long a batch is gathered.

fnMail::write(Self, @moonlog.Entry) -> Result[Unit, String]
typeSecurityClear · StartTls · Tls

How the connection to the mail server is protected.

fnSecurity::port(Self) -> Int32

The port a security setting usually means: 25 for the clear, 587 for submission with STARTTLS, 465 for implicit TLS. The submission ports are what a relay in front of a service listens on when it is not the local one.

letat_most_defaultInt32

How many records one mail carries; a burst larger than this is sent in several mails rather than one too big to read.

letwindow_defaultInt64

How long a batch of records is gathered before it is mailed.

moonlog/config

A document read, checked and written back.

typeAlarmtiers : Array[Array[String]], remind : Int64, fallback : @moonlog.Fallback

An alarm as a document writes it: which sinks to try, in which tiers, how often to remind, and what to do when every tier is spent.

fnAlarm::tiers(Self) -> @moonlog.Tiers

The alarm as the hub wants it.

typeConfigmut profile : @moonlog.Profile, mut filter : String, mut slots : Array[Slot], mut alarm : Alarm?

A logging configuration, read from a document and checked against the settings the shipped sinks read. Reading a document does not build anything: it says what the deployment asked for, and the application — or moonlog/global — turns that into sinks. That split is what lets the same document be checked, dumped and reloaded without a file system in the way.

fnConfig::alarm(Self) -> Alarm?

The alarm the document describes, if any.

fnConfig::dump(Self, format? : Format) -> String

Writes the configuration as a document, which is what dump is for: a deployment can be read back and checked without the file that produced it.

fnConfig::filter(Self) -> String

The filter the document asks for, as written.

fnConfig::load(path~ : String) -> Self raise Refused

Reads a configuration file, taking the format from its extension.

fnConfig::new() -> Self

What a configuration with no document says: the default profile, the default filter, and nothing to write to.

fnConfig::parse(StringView, format? : Format, lookup? : (String) -> String?) -> Self raise Refused

Reads a document. ${NAME} in any string is replaced by the environment variable of that name; a name the environment does not have is refused, because a configuration that silently logs to an empty URL is worse than one that will not start.

fnConfig::profile(Self) -> @moonlog.Profile

The profile the document asks for.

fnConfig::save(Self, path~ : String) -> Unit raise Refused

Writes the configuration to path in the format its extension asks for.

fnConfig::schema() -> Json

The JSON Schema a document of this shape answers to. It is built from the same tables the parser checks against, so the schema an editor or a pipeline validates with cannot drift from the parser: a key the schema allows is a key the parser reads, and a value of the wrong shape is refused in both places. Publish it next to a deployment for the editor to pick up; moonlog/config never needs it to read a file.

fnConfig::slot(Self, String) -> Slot?

The slot called name, if there is one.

fnConfig::slots(Self) -> Array[Slot]

The slots the document describes, in document order.

fnConfig::to_json(Self) -> Json

The document this configuration is written as.

fnConfig::watch(path~ : String, format? : Format, lookup? : (String) -> String?) -> Watcher raise Refused

Starts watching path.

typeFormatYaml · Json · Toml

The three shapes a configuration file is written in. All three read into the same tree, so the same document means the same configuration whichever one it is written in.

fnFormat::name(Self) -> StringView

The name this format is written as.

fnFormat::of(StringView) -> Self raise Refused

The format a file name asks for, by its extension.

fnFormat::parse(StringView) -> Self raise Refused

A format by name.

fnFormat::read(Self, StringView, String) -> Json raise Refused

Reads a document into the tree every reader in this family produces.

fnFormat::write(Self, Json) -> String

Writes the tree back out in this format.

typeQueuesize : Int32, batch : Int32, wait : Int64, full : @moonlog.Full

A queue as a document writes it.

typeRefusedRefused(at~ : String, got~ : String, want~ : String)

What went wrong in a configuration file, said in the terms a reader can act on: where in the document, what was there, and what belongs there. at is a key path such as slots.file.max_size, or empty for the document as a whole.

fnRefused::to_text(Self) -> String

A refusal with the key path it happened at.

typeSlotname : String, kind : String, filter : String?, layout : String?, colour : Bool?, retries : Int32?, probe : Int64?, queue : Queue?, settings : Map[String, Json]

One sink as the document describes it. settings holds the keys that belong to the kind; text, flag, size and the rest read them back with the shape the parser already checked.

fnSlot::count(Self, String) -> Int32?

A setting as a whole number.

fnSlot::flag(Self, String) -> Bool?

A setting as a yes or no.

fnSlot::keys(Self) -> Array[String]

The keys this slot's kind reads, which is what a deployment file is checked against.

fnSlot::pairs(Self, String) -> Array[(String, String)]?

A setting as a table of strings, in document order where the format keeps one.

fnSlot::size(Self, String) -> Int64?

A setting as a byte count, 10mb and a bare number alike.

fnSlot::span(Self, String) -> Int64?

A setting as a number of nanoseconds.

fnSlot::text(Self, String) -> String?

A setting as a string. The parser has already checked that anything that is there has the shape the key asks for, so a None here only means the document left the key out.

fnSlot::texts(Self, String) -> Array[String]?

A setting as a list of strings.

typeWatcherpath : String, mut seen : String, format : Format, lookup : (String) -> String?

A configuration file watched for changes. There is no background task here: poll reads the file and answers it when the bytes differ from the last document it saw, which keeps the reload on the same thread as everything else and makes it something a test can step through. Comparing content — rather than the modification time — means a rewrite is never missed because two edits landed inside one timestamp tick, which is the usual way a fast test (or a coarse filesystem clock) fools an mtime check. A configuration document is small, so reading it on each poll is cheap; parsing happens only when the content actually moved.

fnWatcher::path(Self) -> String

The file being watched.

fnWatcher::poll(Self) -> Result[Config?, String]

The newest configuration, when the file has changed since the last answer. Ok(None) means the bytes are unchanged. An Err means the file changed and the new one will not do — the caller keeps what it has and decides whether to say so.

fnnanos_as_span(Int64) -> String

A span in the shortest form that reads back as the same number of nanoseconds.

fnparse_size(String, StringView) -> Int64 raise Refused

How many bytes a size names: 512, 4kb, 10MiB, 1.5gb. A bare number is bytes. k, m, g and t are powers of a thousand, ki, mi, gi and ti powers of 1024, with or without a b.

fnparse_span(String, StringView) -> Int64 raise Refused

How many nanoseconds a span names: 200ms, 5s, 1m30s, 2h. A span always says its unit — a bare number is refused, because a configuration that means seconds in one program and nanoseconds in another is a configuration that is wrong silently.

letremind_defaultInt64

One second, the reminder an alarm gets when the document does not say.

moonlog/global

The process-wide hub.