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 |
Fields with nothing to report are omitted. An error from New logs as err.msg
and err.kind=errors.basic; one from Newf has no kind and logs err.msg alone.
kind is the identity, not the outermost wrapper¶
errors.KindOf skips this package's own annotation layers (hint, detail,
attributes, message and 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, such as 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: