

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Difficulty: 7
click for more info
Not enough gems
Cost: 6 gems
1: Logging Errors
incomplete
2: Stack Traces
incomplete
3: Slog Groups
incomplete
4: Handle Errors Once
incomplete
5: Adding Attributes
incomplete
6: Multiple Errors
incomplete
Back
ctrl+,
Next
ctrl+.
This lesson's interactive features are locked, please to keep using them
In the last lesson, we saw how to intercept and modify log key/value pairs with the ReplaceAttr callback. But we introduced a bit of a code smell at the same time: we're now emitting stack trace data in the same string as our error message.
{
"level": "ERROR",
"msg": "error validating password",
"error": "invalid stored credential format\ngithub.com/myorg/myapp/auth.validatePassword\n\t/src/auth.go:42\n..."
}
Aren't we supposed to be structuring our logs? Now it's cumbersome to search just the error message, or just the stack trace. And what if we have other error attributes we care about logging in some applications? Error codes and other metadata are common.
What we really want is something like this:
{
"level": "ERROR",
"msg": "error validating password",
"error": {
"message": "invalid stored credential format",
"stack_trace": "github.com/myorg/myapp/auth.validatePassword\n\t/src/auth.go:42\n...",
"error_code": 12345,
"error_subcode": 3.14159
}
}
log/slog gives us a tool for this exact problem: groups.
slog.Group and slog.GroupAttrsThe slog.Group function creates a group attribute for use in log calls:
logger.Info("user logged in",
slog.Group("user",
slog.String("name", "frodo"),
slog.String("role", "ringbearer"),
),
)
This produces nested output in JSON:
{
"level": "INFO",
"msg": "user logged in",
"user": { "name": "frodo", "role": "ringbearer" }
}
And dotted keys in text format:
level=INFO msg="user logged in" user.name=frodo user.role=ringbearer
There's also slog.GroupAttrs, which does the same thing but takes slog.Attr values instead of any. This is useful inside replaceAttr, where you're already working with slog.Attr values:
return slog.GroupAttrs("error",
slog.Attr{Key: "message", Value: slog.StringValue("something went wrong")},
slog.Attr{Key: "stack_trace", Value: slog.StringValue("...")},
)
Split error message and stack trace into separate fields.
type stackTracer interface {
error
StackTrace() pkgerr.StackTrace
}
if stackErr, ok := errors.AsType[stackTracer](err); ok {
return slog.GroupAttrs("error", slog.Attr{
Key: "message",
Value: slog.StringValue(stackErr.Error()),
}, slog.Attr{
Key: "stack_trace",
Value: slog.StringValue(fmt.Sprintf("%+v", stackErr.StackTrace())),
})
}
Restart your server with LINKO_LOG_FILE=linko.access.log set:
LINKO_LOG_FILE=linko.access.log go run .
Run and submit the CLI tests from the root of the Linko repo.