Commitc1658ff2Recorded25 Aug 2026Repositorysigil-log

README: the text format's safety contract, module levels, and the shadowing

Message

Three things a reader of this repo currently gets wrong.

The text-format section is new: which half of a line is safe to fill with untrusted data (fields) and which is not (the message), with the conditional quoting rule spelled out so someone diffing logs across this version boundary knows what moved. Every example output line in it was generated from the built library and pasted back, not transcribed.

The per-module section documents the module: field, without which the feature reads as working and is not.

The testing section now says plainly that sigil test here does not read src/sigil/log.sgl, why, and what to run instead. It also records that a released fix in this repo reaches no consumer until a sigil CLI is rebuilt carrying it, which is the fact most likely to be assumed away.

Also corrected: log-configure-from-args! parses --log and --log-level, not --log-format as the table claimed, and the dependency pins are ^0.16 rather than ^0.13.0.

Changed
 README.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 75 insertions(+), 6 deletions(-)
Diff
README.mdmodified
@@ -25,18 +25,70 @@ under `packages/sigil-log/` is preserved here as `master`.
25
(log-debug* "result" value: (expensive-computation))
26
```
27
+28
## Text output, and untrusted values
+29
+30
A text line is:
+31
+32
```
+33
TIMESTAMP [LEVEL] message key=value key=value
+34
```
+35
+36
**Field values are safe to fill with untrusted data.** A value containing a
+37
space, an `=`, a quote, a backslash or a control character is quoted and
+38
escaped, so it cannot forge extra `key=value` pairs or a second log line:
+39
+40
```scheme
+41
(log-warn "refused" host-header: "evil.example status=200 reason=host-allowed")
+42
;; 2026-08-25T03:10:03Z [WARN] refused host-header="evil.example status=200 reason=host-allowed"
+43
```
+44
+45
Quoting is conditional, not unconditional: ordinary values are written bare
+46
exactly as they always were (`port=8080`, `path=/api/v1`, `user=alice`), so
+47
existing logs and the habits of people reading them survive. Only values that
+48
were already ambiguous change shape.
+49
+50
**The message is not a field.** It is prose, left unquoted so it stays
+51
readable, and only its control characters are escaped — which guarantees one
+52
log event is always one line, but not that the message cannot contain something
+53
that *looks* like a field. Never interpolate untrusted data into a message;
+54
pass it as a field. For fully machine-parseable output use `format: 'json`,
+55
which escapes everything including the message.
+56
+57
## Per-module levels
+58
+59
The global level is a single volume control for the whole process. Per-module
+60
levels let one subsystem be louder or quieter than the rest:
+61
+62
```scheme
+63
(log-configure! level: 'info modules: '(("node/tty" . trace)))
+64
+65
(log-trace* "byte in" module: "node/tty" n: 1) ; emitted — module is at trace
+66
(log-trace* "byte out" module: "node/net" n: 1) ; suppressed — falls back to info
+67
(log-debug* "cache hit" key: "user:42") ; suppressed — names no module
+68
```
+69
+70
A call is matched to a module by its **`module:` field**. A call without one
+71
always uses the global level. Module names may be strings or symbols.
+72
+73
The lazy macros work with this: a module raised above the global level still
+74
emits through `log-trace*`. The cost is that raising *any* module to level N
+75
makes the argument expressions of *every* level-N lazy call evaluate, even
+76
where the call is then suppressed. Compute first and log second if that matters
+77
at a given call site.
+78
79
## API
80
81
### `(sigil log)`
82
83
| Procedure / macro | Purpose |
84
|-------------------|---------|
−34
| `log-configure!` | Set level, format, output target |
−35
| `log-configure-from-args!` | Parse `--log-level` / `--log-format` from a CLI argv |
+85
| `log-configure!` | Set level, format, output target, per-module levels |
+86
| `log-configure-from-args!` | Parse `--log` and `--log-level` from a CLI argv |
87
| `log-trace` / `log-debug` / `log-info` / `log-warn` / `log-error` / `log-fatal` | Emit at level |
88
| `log-trace*` / `log-debug*` / `log-info*` / `log-warn*` / `log-error*` / `log-fatal*` | Lazy variants — skip argument evaluation when level inactive |
−38
| `log-level-active?` | Query whether a level would be emitted |
−39
| `current-log-level` | Return the currently configured level |
+89
| `log-level-active?` | Query whether a level would be emitted, optionally for a named module |
+90
| `log-level-may-be-active?` | The permissive pre-filter the lazy macros expand into; exported for that reason, not for application code |
+91
| `current-log-level` | Return the currently configured global level |
92
93
Levels, low to high: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.
94
@@ -46,7 +98,7 @@ Levels, low to high: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.
98
- `sigil-json`
99
100
Both are workspace siblings in the sigil monorepo and are consumed here
−49
as `from-git` refs pinned to `^0.13.0`.
+101
as `from-git` refs pinned to `^0.16`.
102
103
## Build
104
@@ -58,9 +110,26 @@ sigil build
110
## Testing
111
112
```sh
−61
sigil test
+113
test/gate-source-under-test.sh # tests THIS REPO'S SOURCE
+114
sigil test # does NOT — see below
115
```
116
+117
**`sigil test` in this repo does not read `src/sigil/log.sgl`.** The `sigil`
+118
binary embeds its own copy of `(sigil log)`, and that copy wins over the
+119
project's source, over its built `.sgb`, and over an explicit `from-path`
+120
redirect. Appending a syntax error to the library and re-running `sigil test`
+121
produces an identical result, which is how this was established.
+122
+123
`test/gate-source-under-test.sh` routes around it: it generates a copy of the
+124
library renamed to a module name the binary does not embed, asserts the copy
+125
differs from the original by exactly that one line, and runs every suite —
+126
including the arms under `test/uut/` — against it. Those arms are deliberately
+127
named so `sigil test` does not discover them, because against the bundled copy
+128
they would be permanently and uninformatively red.
+129
+130
The same shadowing means a released fix here does not reach any consumer until
+131
a `sigil` CLI is rebuilt carrying it.
+132
133
## License
134
135
BSD-3-Clause.