Emit slog attributes from every handler (closes #19)
check / check (push) Successful in 36s
check / check (pull_request) Successful in 35s

Handle never read the record's attributes, and WithAttrs and WithGroup
returned the receiver unchanged. The JSON and webhook handlers also marshaled
slog.Record itself, whose attributes are unexported.

Each handler now carries a handlerAttrs value, copied rather than mutated, so
sibling loggers cannot leak attributes into each other. JSON and webhook
output nest groups as objects; the console appends key=value pairs with dotted
group keys, quoted as slog.NewTextHandler quotes them. Logged values are never
written to. In JSON the record's own fields win a key collision, and a
repeated key keeps its last value unless both are groups, which merge; the
README documents both.

Deviation: a key containing invalid UTF-8 stays unquoted, unlike the stdlib:
#24.

Model: opus-5-5 (rebase, this message)
This commit is contained in:
2026-09-28 10:34:46 +00:00
parent 5d4b078304
commit f22461fe25
6 changed files with 401 additions and 26 deletions
+44
View File
@@ -18,6 +18,13 @@ Released v1.0.0 2024-06-14. Works as intended. No known bugs.
- if output is a tty, outputs pretty color logs
- if output is not a tty, outputs json
- supports delivering each log message via a webhook
- emits every `slog` attribute: those passed to a log call, those
accumulated with `WithAttrs`, and those qualified by `WithGroup`.
`slog.Group` values nest, and `slog.LogValuer` values are resolved. In
json output attributes are object fields (groups become nested objects);
in console output they are appended as `key=value` pairs, with grouped
keys written as `group.key=value`. See
[Attribute output](#attribute-output) for the details worth knowing
## Planned Features
@@ -60,6 +67,43 @@ func main() {
}
```
## Attribute output
Attributes reach every handler: the ones passed to the log call, the ones
accumulated with `WithAttrs`, and the ones qualified by the groups open at
the time they were attached. `slog.Group` values nest, and
`slog.LogValuer` values are resolved to the value they stand for. A few
behaviours are worth knowing before you rely on them.
**Your values are never modified.** Whatever you log is read and rendered,
never written to. A map or a slice you pass to `slog.Any` comes back from
the logger exactly as you handed it over, even when a group later uses the
same key.
**Durations are nanoseconds in json, and readable on the console.** The
json and webhook payloads emit a `slog.Duration` as a number of
nanoseconds, matching `slog.NewJSONHandler`, so a consumer can compare and
aggregate the field without parsing it. The console line emits the same
duration as `3s`, matching `slog.NewTextHandler`, because a person reads
it.
**The json payload is an object, with the consequences an object has.**
The record's own fields are named `Time`, `Level`, `Message` and `PC`, and
they own those names: an attribute keyed after one of them is dropped from
the json and webhook output. A key logged more than once keeps its last
value there for the same reason, with one exception: when both are
`slog.Group` values sharing a key, the two groups are merged into one
object holding the members of both, rather than the second replacing the
first. Neither the collision nor the merge applies to the console output,
which is a line of text: every pair appears, in order. If you need a field
called `message`, pick a key that does not collide - the collision is
silent.
**Empty things follow the `slog.Handler` contract.** An empty `Attr` is
ignored, an empty group is elided along with its key, a group with an
empty key is inlined into its parent, and `WithGroup("")` returns the
handler unchanged.
## License
[WTFPL](./LICENSE)