log

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package log is the one structured event line every part writes BESIDE the one human line it already writes. SPEC-LOGS.md Part 2 fixes the shape: one JSON object per state change, one line, through Go's own log/slog with slog.NewJSONHandler. The stdout line stays the SPEC.md event, unchanged; this line is the same event with the fields a LogQL query needs, so a person can ask "why is this card hung" without an ssh and a grep.

The fields are fixed, all fifteen of them, and every object carries all fifteen: an id that is not this event's scope is the empty string or zero rather than an omitted key, so `| json` never guesses and a query on `card=""` means exactly "not this scope". ts, level, source, event and msg are never absent; the rest are "" or 0 when they do not apply.

The two fields that come from outside the event are injected: the clock that fills ts (never time.Now() read directly, so a test is deterministic) and the source of the run's guid (never /proc read directly in a test).

Nothing here is a second logging system. The handler is slog's, the escape is internal's oneline, and the line is additive: a reader that only knows the stdout event still works.

slog.go is the shape a long-running verb holds.

SPEC-LOGS.md Part 2 fixes one shape for every mechanical part: one JSON object per state change, one line. A verb that takes one unit of work and exits -- `nova-pulse launch`, `nova-merge batch` -- builds a Line, writes it and is gone. A long-running verb is different: it changes state many times over one run -- the harvest loop, the pull worker, the bridge that republishes the bus -- and the labels that name the run do not change from the first event to the last.

What does not change is what this file holds: the host the run is on (the spec's `bench` field), the verb's own name, and the tool it belongs to. What changes is the event: the label a LogQL query selects on and the one human sentence. Event renders one spec Line through slog's JSON handler, so a line from a long-running verb is the same object -- the same fifteen fields, escaped through oneline and redacted the same way -- as one written by New and Write. Nothing here is a second logging system.

Index

Constants

View Source
const Redacted = "[redacted]"

Redacted is what replaces a secret value. It is a fixed string so a query can find the lines where something was redacted, and a test can assert the mark is there.

Variables

This section is empty.

Functions

func ProcessGUID

func ProcessGUID() string

ProcessGUID is the production guid: the kernel's boot id, this process's pid and the process's start time. A process keeps the same guid for its whole run, so its lines group, and two runs that share a pid after a reboot still differ by the boot id. A test never calls this; it injects a fixed GUIDSource instead.

func Redact

func Redact(s string) string

Redact returns s with every secret-shaped value replaced by Redacted. It is safe to call on any string, including one already redacted: the mark itself matches no rule.

Types

type Clock

type Clock func() time.Time

Clock is the writer's clock. Production passes time.Now; a test passes a fixed func so ts is deterministic.

type GUIDSource

type GUIDSource func() string

GUIDSource is the source of this process run's guid. Production passes ProcessGUID; a test passes a fixed func so it never reads /proc.

type Line

type Line struct {
	TS     string // ts: UTC RFC3339Nano from the writer's clock
	Level  string // level: DEBUG, INFO, WARN or ERROR
	Source string // source: the tool that wrote the event
	Bench  string // bench: the machine, by its fleet name
	Verb   string // verb: the verb within the tool
	Job    string // job: the work item's job id
	Card   string // card: the work item's card id
	PR     int    // pr: the work item's pull request number
	Run    string // run: the work item's run id
	Slot   string // slot: the work item's slot
	GUID   string // guid: one per process run
	Event  string // event: start, refuse, retry, done, or the part's own noun
	Msg    string // msg: one human sentence, escaped through oneline by Write
	DurMS  int64  // dur_ms: milliseconds from start to this event
	Err    string // err: the error's text, escaped through oneline by Write
}

Line is one structured event: the fixed field list of SPEC-LOGS.md Part 2. It is a value, built by New and filled by the verb, and Write renders it as one JSON line.

func New

func New(clock Clock, guid GUIDSource, source string) Line

New fills the two fields that come from outside the event -- ts from the injected clock and guid from the injected source -- and defaults the level to INFO, the level of a state change that is not a warning or an error. The verb fills the rest.

func (Line) Write

func (l Line) Write(w io.Writer) error

Write emits the event as exactly one JSON object with the spec's fifteen fields. The message and the error go through internal/oneline first, so a newline in either cannot add a second line and the object is one line whatever the sentence holds. The writer is injected: stderr in production (a unit's stderr is the systemd journal), a buffer in a test. The handler is slog's JSON handler; the handler's own time, level and msg keys are replaced by the spec's ts, level and msg fields so the object is the spec's object and not slog's.

Every field whose content comes from outside this program -- the ids, the message and the error -- passes through Redact on the way out, so SPEC-LOGS.md Part 2's one hard rule ("a secret VALUE is never logged") is enforced by the emitter and not by review. The fixed vocabulary the program writes itself -- ts, level, source, event, guid -- is never touched, so a redaction can never rename an event out from under a query.

type LongVerb

type LongVerb struct {
	W      io.Writer // nil writes nothing
	Clock  Clock     // nil defaults to time.Now
	GUID   GUIDSource
	Source string // the tool: nova-pulse, nova-swarm, nova-work
	Host   string // the machine, by its fleet name -- the spec's bench field
	Verb   string // the verb within the tool: harvest, claim, launch
}

LongVerb is one long-running verb's structured sink for the length of its run.

W nil writes nothing, which is how a caller that named no sink keeps its exact stdout and stderr: the line is additive and never a replacement. Clock and GUID are injected -- the same two the spec injects everywhere -- so a test is deterministic and never reads time.Now or /proc. A nil Clock or GUID defaults, so production wiring is the sink and the labels and nothing else.

func NewLongVerb

func NewLongVerb(w io.Writer, source, host, verb string, clock Clock, guid GUIDSource) *LongVerb

NewLongVerb is the production wiring: the writer the verb chose, the tool it is, the host it runs on, its own verb name, and the injected clock and guid. A nil writer is a quiet verb, so a run that named no --log keeps its exact stdout and stderr.

func (*LongVerb) Event

func (v *LongVerb) Event(label, msg string)

Event writes one state change as exactly one JSON line. label is the state change a LogQL query selects on -- start, done, refuse, retry, or the part's own noun (delete, keep, fetch, claim, ack) -- and msg is the one human sentence. The run's host, verb and source ride on every line, so `{source="nova-pulse", verb="harvest", bench="space"}` returns exactly one run's log.

The line goes through Line.Write, so it carries the spec's fixed fifteen fields whether or not the caller filled them, redacts every field that came from outside the program, and cannot be split by a newline in msg.

A line that cannot be written is never a reason to stop the work: the state change already happened, and the record of it is the loop or the lane, not this line.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL