Log an error¶
Errors implement slog.LogValuer, so passing one to a handler emits a group of
attributes rather than a flattened string.
level=ERROR msg="release lookup failed" \
err.msg="looking up codeberg: no provider registered" \
err.kind=forge.provider_not_found \
err.hint="[Register one with forge.Register(), or check the spelling.]" \
err.source_type=codeberg
Nothing is needed to switch this on. It happens because the error implements the
interface slog already looks for.
What is in the group¶
| Key | From |
|---|---|
msg |
err.Error() |
kind |
the error's identity — see below |
hint |
Hints(err), when there are any |
detail |
Details(err), when there are any |
| anything else | whatever you attached with WithAttrs |
Empty groups are omitted, so a plain error logs as err.msg and err.kind and
nothing more.
kind is the identity, not the outermost wrapper¶
errors.KindOf skips this package's own annotation layers — hint, detail,
attributes, message, stack — and reports the first kind that says what the error
is:
err := errors.WithAttrs(
errors.WithHint(errors.WithStack(ErrProviderNotFound), "…"),
slog.String("source_type", "codeberg"),
)
errors.KindOf(err) // "forge.provider_not_found", not "errors.attrs"
Reporting the outermost kind would name whichever annotation happened to be
applied last, which describes the plumbing rather than the failure. This is also
what go/observability will report as OpenTelemetry's exception.type.
Attach the things you will search on¶
import "log/slog"
return errors.WithAttrs(err,
slog.String("source_type", sourceType),
slog.String("host", host),
slog.Int("attempt", attempt),
)
They arrive as first-class attributes — err.host=codeberg.org — not inside a
message you have to pattern-match later.
slog.Attr is used rather than a type of our own because both destinations want
exactly that shape: a log record and a trace span attribute are both key/value.
Choosing anything else would mean translating at each end.
The stack is not in the group¶
Deliberately. It is large, it is rarely what a log line is for, and including it by default makes every error log unreadable.
A handler that wants it asks:
if stack := errors.StackOf(err); stack != nil {
logger.Debug("failure stack", "stack", stack.String())
}
StackOf returns the outermost stack — the one closest to where the error
surfaced. That is also the route go/observability uses to fill OTEL's
exception.stacktrace.
Verbose output for a human¶
For a CLI printing to a terminal rather than a log pipeline, %+v renders
everything in one block, most actionable first: