on master help sign in
Document the structured logging API and guttered multiline output

The docs described the pre-logCtx API (base.logInfo : a -> IO {}, any value logged as a message). Update to the actual implementation: - base.debug/info/warn/error : (<>logCtx) => Text -> IO {}; logInfo et al. are deprecated aliases (base.md, stdlib.md, knot.md, README.md) - (<>logCtx) context records: merged per-callsite, innermost wins, caller passes only the Text message - multiline context field renders as a |-guttered block on TTY, \n-escaped inline in JSON (base.md, stdlib.md, AGENTS.md) - JSON ctx fields merge later-wins over level/msg/timestamp - runtime events (HTTP serve loop, migrations, watcher panics) emit through the same logger so JSON stderr stays one record per line; todo reports / GC dump / CLI usage stay raw (AGENTS.md) docs/logging-proposal.md is the historical design doc and is left as-is.

commitd0ba637253583b303775556b9a1c437548dc0185 authoriko <mail@iko.soy> dateTue 11 Aug 2026, 05:12 (2 months ago) parent6b9d60f
5 files changed+71−24
AGENTS.md+2−2 README.md+4−3 base.md+41−6 knot.md+4−4 stdlib.md+20−9
AGENTS.md+2−2
@@ -65,10 +65,10 @@ let (module, parse_diags) = parser.parse_module();
6565
6666### Runtime (`crates/knot-runtime/`)
6767
68−Compiled as a `staticlib` (with `rlib` for workspace dependency resolution). All functions use `extern "C"` ABI, called by Cranelift-generated code via symbol references. `lib.rs` is the main runtime; `log.rs` implements the leveled logger used by `logInfo`/`logWarn`/`logError`/`logDebug` (TTY-colored on stderr or one JSON record per line; `--debug` arg scanned by `debug_enabled()` gates `logDebug`); `tui.rs` is the `db` subcommand's terminal-UI database explorer (parses `_knot_schema`, paginates rows, drills into individual records).
68+Compiled as a `staticlib` (with `rlib` for workspace dependency resolution). All functions use `extern "C"` ABI, called by Cranelift-generated code via symbol references. `lib.rs` is the main runtime; `log.rs` implements the leveled, structured logger behind `base.debug`/`info`/`warn`/`error` (and the deprecated `logInfo`/`logWarn`/`logError`/`logDebug` aliases): TTY-colored on stderr or one JSON record per line, with `(<>logCtx)` context fields merged in (a multiline field renders as a `│`-guttered block on a TTY, `\n`-escaped inline in JSON). The runtime's own events — HTTP serve-loop errors, source migrations, watcher panics — emit through the same logger so JSON-mode stderr stays one record per line; `knot_todo` reports, the GC stats dump, and CLI/usage errors stay raw stderr. `--debug` arg scanned by `debug_enabled()` gates `debug`. `tui.rs` is the `db` subcommand's terminal-UI database explorer (parses `_knot_schema`, paginates rows, drills into individual records).
6969
7070Compiled programs expose a common CLI without any user wiring:
71−- `--debug` — enable `logDebug` output
71+- `--debug` — enable `base.debug` output
7272- `--help` — print usage including any compile-time overrides exposed via `knot_override_check_help` (the compiler emits this list at codegen time)
7373- `--http-max-body-bytes=N` — cap HTTP request and response bodies (`K`/`M`/`G` suffixes, default `16M`); applies to both `listen` and `fetch`
7474- `--<name>=<value>` — override a compile-time constant declared in the source (same flags accepted both at `knot build` and at run time)
README.md+4−3
@@ -690,8 +690,9 @@ enforce referential integrity and uniqueness at write time. See
690690`examples/constraints.knot`.
691691
692692**Other goodies.** Bytes and hex encoding, BLAKE3 hashing, UUIDv7,
693−JSON encode/decode, file I/O, leveled logging (`logInfo`/`logWarn`/...),
694−crypto (`generateKeyPair`/`encrypt`/`sign`/`verify`). See
693+JSON encode/decode, file I/O, structured leveled logging
694+(`base.info`/`base.warn`/… with `logCtx` context records), crypto
695+(`generateKeyPair`/`encrypt`/`sign`/`verify`). See
695696`examples/bytes.knot`, `examples/hash.knot`, `examples/uuid.knot`,
696697`examples/json.knot`, `examples/crypto.knot`, `examples/log_test.knot`.
697698
@@ -700,7 +701,7 @@ any required-argument constants declared in the source:
700701
701702```sh
702703./my_program # run main
703−./my_program --debug # turn on logDebug output
704+./my_program --debug # turn on base.debug output
704705./my_program --help # print usage + any required CLI arguments
705706./my_program --http-max-body-bytes=32M # cap HTTP body size (K/M/G suffixes)
706707./my_program --port=8080 # supply a required CLI argument
base.md+41−6
@@ -319,19 +319,54 @@ base.readLine : IO Text
319319
320320Read a line from stdin.
321321
322−### `base.logInfo` / `base.logWarn` / `base.logError` / `base.logDebug`
322+### `base.debug` / `base.info` / `base.warn` / `base.error`
323323
324324```
325−base.logInfo : a -> IO {}
326−base.logWarn : a -> IO {}
327−base.logError : a -> IO {}
328−base.logDebug : a -> IO {}
325+base.debug : (<>logCtx) => Text -> IO {}
326+base.info : (<>logCtx) => Text -> IO {}
327+base.warn : (<>logCtx) => Text -> IO {}
328+base.error : (<>logCtx) => Text -> IO {}
329329```
330330
331331Leveled logging to **stderr** (kept separate from `println`'s stdout). Colored
332−on a TTY, one JSON line per record otherwise. `logDebug` is silent unless the
332+on a TTY, one JSON line per record otherwise. `debug` is silent unless the
333333program is run with `--debug`.
334334
335+The `(<>logCtx)` constraint merges every `logCtx` record in scope at the call
336+site (innermost scope wins) and attaches the fields as structured context — the
337+caller passes only the message:
338+
339+```knot
340+do
341+ base.info "starting"
342+ with {logCtx {availableMb 64}}
343+ (base.warn "low memory")
344+ yield {}
345+```
346+
347+On a TTY a single-line context field renders inline (`availableMb=64`); a
348+field whose value contains a newline is lifted out of the splat and shown as a
349+guttered block beneath the line:
350+
351+```
352+WARN low memory availableMb=64
353+ERROR failed to compile module availableMb=64
354+ source
355+ │ fn main =
356+ │ let x = 1 in
357+ │ x
358+```
359+
360+In JSON mode context fields merge into the emitted record (`{"level","msg",…
361+ctx,"timestamp"}`); a field named `level`, `msg`, or `timestamp` replaces the
362+default (later-wins, like record `unify`). Newlines stay `\n`-escaped so each
363+record is one line.
364+
365+The runtime's own events (HTTP serve-loop errors, migrations, watcher panics)
366+emit through the same machinery, so in JSON mode every stderr line is a
367+well-formed record. `base.logInfo`/`logWarn`/`logError`/`logDebug` are
368+deprecated aliases kept for source compatibility.
369+
335370### `base.show`
336371
337372```
knot.md+4−4
@@ -948,10 +948,10 @@ result type (see [base.md](base.md#morphs-basemorph)).
948948| `println` | `a -> IO {}` | Print with newline |
949949| `print` | `a -> IO {}` | Print without newline |
950950| `readLine` | `IO Text` | Read stdin line |
951−| `logInfo` | `a -> IO {}` | Leveled log to stderr (INFO) |
952−| `logWarn` | `a -> IO {}` | Leveled log to stderr (WARN) |
953−| `logError` | `a -> IO {}` | Leveled log to stderr (ERROR) |
954−| `logDebug` | `a -> IO {}` | DEBUG; only emits when run with `--debug` |
951+| `debug` | `(<>logCtx) => Text -> IO {}` | Leveled log to stderr (DEBUG); only emits when run with `--debug` |
952+| `info` | `(<>logCtx) => Text -> IO {}` | Leveled log to stderr (INFO) |
953+| `warn` | `(<>logCtx) => Text -> IO {}` | Leveled log to stderr (WARN) |
954+| `error` | `(<>logCtx) => Text -> IO {}` | Leveled log to stderr (ERROR) |
955955| `readFile` | `Text -> IO Text` | Read file |
956956| `writeFile` | `Text -> Text -> IO {}` | Write file (path, content) |
957957| `appendFile` | `Text -> Text -> IO {}` | Append to file |
stdlib.md+20−9
@@ -604,29 +604,40 @@ print : a -> IO {}
604604
605605Print a value to stdout without a trailing newline.
606606
607−### `logInfo` / `logWarn` / `logError` / `logDebug`
607+### `debug` / `info` / `warn` / `error`
608608
609609```
610−logInfo : a -> IO {}
611−logWarn : a -> IO {}
612−logError : a -> IO {}
613−logDebug : a -> IO {}
610+debug : (<>logCtx) => Text -> IO {}
611+info : (<>logCtx) => Text -> IO {}
612+warn : (<>logCtx) => Text -> IO {}
613+error : (<>logCtx) => Text -> IO {}
614614```
615615
616616Leveled logging to stderr (so output does not mix with `println` on stdout).
617617When stderr is a TTY, output is colored; otherwise each record is written as
618−one JSON line for log aggregators. `logDebug` only emits when the program is
618+one JSON line for log aggregators. `debug` only emits when the program is
619619launched with `--debug` — debug records are dropped silently otherwise. (Every
620620compiled Knot program accepts `--debug` automatically; see
621621[Runtime CLI](#runtime-cli).)
622622
623+Each logger carries a `(<>logCtx)` fold constraint: the compiler merges every
624+`logCtx` record in scope at the call site (innermost scope wins) and attaches
625+the fields as structured context, so the caller passes only the message.
626+
623627```knot
624628do
625− base.logInfo "starting"
626− base.logWarn {event "low memory" availableMb 64}
629+ base.info "starting"
630+ with {logCtx {availableMb 64}}
631+ (base.warn "low memory")
627632 yield {}
628633```
629634
635+A context field whose value contains a newline renders on a TTY as a guttered
636+block beneath the log line (single-line fields stay inline as `k=v`); in JSON
637+mode the value stays `\n`-escaped on the single emitted line.
638+
639+`logInfo`/`logWarn`/`logError`/`logDebug` are deprecated aliases.
640+
630641### `show`
631642
632643```
@@ -1309,7 +1320,7 @@ subcommands without any user wiring:
13091320
13101321| Argument | Description |
13111322|----------|-------------|
1312−| `--debug` | Enable `logDebug` output; without this flag, `logDebug` calls are dropped silently. |
1323+| `--debug` | Enable `base.debug` output; without this flag, `base.debug` calls are dropped silently. |
13131324| `--help` | Print usage including any compile-time overrides exposed by the program. |
13141325| `--http-max-body-bytes=N` | Cap HTTP request and response bodies. Suffixes: `K`, `M`, `G`. Default `16M`. Applies to both `listen` and `fetch`. |
13151326| `--<name>=<value>` | Override a compile-time constant. The compiler exposes top-level constants annotated for override; see the program's own `--help`. The same flags may be set at build time via `knot build … --<name>=<value>`. |