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 ¶
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.
Types ¶
type Clock ¶
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 ¶
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 ¶
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 ¶
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.