1# The event stream23Atlas narrates itself through one leveled, structured event stream. There is4no wrapper library: Go's standard `log/slog` is the whole mechanism, and5`internal/logging` is only the level policy, the attribute vocabulary, and the6CLI wiring written down in one place.78The stream has two audiences that want the same thing — a person watching a9pipeline run, and an agent diagnosing why it failed. Both are served by one10line per meaningful unit of work, carrying domain facts under documented keys11rather than prose.1213## Levels, used with intent1415| Level | What belongs here | Default |16|---|---|---|17| `debug` | Internal mechanics: a cache consulted, a plan derived, a retry. Anything whose volume scales with the data. | off |18| `info` | One line per **meaningful unit of work**: a capture skipped, a pyramid derived, a bundle installed, a build gated. If a person would say it aloud when asked "what happened?", it is Info. | on |19| `warn` | Something tolerated that a human should eventually see: a held merge pin, a bundle skipped during a scan, an under-claiming enricher that stayed silent. Never used for "this failed". | on |20| `error` | An operation failed. The error value rides as an attribute; the message says what was being attempted. | on |2122Two rules about volume. Info is per *unit of work*, not per item: "packed232,048 locations" is one Info line, not 2,048. And Warn is not a softer Error —24if the run continues correctly, it is Warn; if something a caller asked for25did not happen, it is Error.2627## Where the stream goes2829Every CLI run writes the event stream to the terminal:3031- **stderr, human-readable text, by default.** Piped stdout stays clean for32 product output — reports, JSON, anything a caller parses. A command that33 prints a report to stdout and its narration to stderr composes; one that34 mixes them does not.35- **`--log-json`** switches to a machine-readable stream, for the workbench,36 for CI, and for anything that wants to filter by attribute.37- **`--log-level`** opens up `debug` (or closes down to `warn`/`error`).38 Absent the flag, **`ATLAS_LOG`** is consulted; absent both, the level is39 `info`. A flag always beats the environment: an explicit instruction beats40 an ambient one. An unrecognised level name is refused, not silently41 ignored.4243The workbench's streamed pipeline output is this same stream, rendered as44rows. Nothing special is emitted for it.4546`fmt.Print*` and `log.Print*` outside a command's product-output path are lint47violations (issue #5 §9). Narration goes through the stream.4849## The shared attribute vocabulary5051Log events speak about the system the way bundles speak about themselves. The52key set is small, documented, and used consistently, so grep, the workbench,53and an agent can correlate events without per-package archaeology.5455| Key | Type | Meaning |56|---|---|---|57| `op` | string | The unit of work: `crawl`, `translate`, `tiles`, `compose`, `enrich`, `measure`, `install`, `scan`. The first thing to group by. |58| `volume` | string (slug) | Which `.atlas` volume the event is about. |59| `world` | string (slug) | Which ground within that volume. |60| `lens` | string | Which picture of that ground. |61| `stamp` | string | The content fingerprint of the build in question, short form (12 hex). What ties an event to a file on disk. |62| `source` | string (slug) | The capture source a generate-lane event is about. Only the generate lane and merge ledgers speak it. |63| `enricher` | string | The enricher an enrich-lane event is about. |64| `dur` | duration | How long the unit of work took. |65| `path` | string | A filesystem path the event is about. The one key that names the machine rather than the domain; it is here because a person reading a failure needs to know which file. |6667**Keys name domain facts about the work, never code organization.** Which68component emitted an event is the logger's name or source, not a vocabulary69entry — `slog.Logger.With` and `AddSource` already answer that question.7071The vocabulary is semi-standardized, in the `semconv` spirit: a package with a72fact none of these keys names may add its own, and a key that earns its way73into several packages is promoted here. `internal/logging` exports a74constructor per key (`logging.Volume(slug)`, `logging.Dur(d)`) so a key is75never a bare string literal at a call site, where it could be misspelled into76invisibility.7778## Usage7980```go81opts := logging.Options{}82opts.Bind(flag.CommandLine)83flag.Parse()8485logger, err := logging.Setup(opts) // stderr, and slog's process default86if err != nil {87 // an unusable --log-level is a usage error, not a warning88}8990logger.Info("bundle installed",91 logging.Op("install"),92 logging.Volume("bend-or"),93 logging.Stamp("ec3fe8c21cfe"),94 logging.Dur(elapsed))95```9697`Setup` is called once, explicitly, from a command's `main`. There is no98`init`: a library importing this package gets nothing it did not ask for, and99`format/` does not import it at all.100101## The browser half102103`render/log.ts` mirrors this: a thin leveled module, console-backed,104structured fields as an object payload, the level gate read once from105`?atlas-log=` or `localStorage`. Reading it once is deliberate — a level that106changed mid-run would make two captures of one run disagree. The browser107console becomes the same kind of stream, so any headless driver that captures108the console gets structured context for free. An109ESLint rule forbids bare `console.*` outside that module and names this110contract when it fires. The level names and the attribute vocabulary above are111shared; the analysis lane has no logger of its own, because a pure112transformation of its declared inputs has nothing to narrate.