

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Difficulty: 8
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
Probably the single most useful thing we can do for error diagnosis is include a stack trace in the log. Say a user contacts tech support and reports that the website is hanging. You check the logs and find this:
2024-06-10T12:34:56Z ERROR Failed to connect to database
... great. Not super useful. But what if you had this instead?
2024-06-10T12:34:56Z ERROR Failed to connect to database
github.com/myorg/myapp/db.Connect
/src/bootdev/course-draft-learn-logging/examples/errors/stacktrace/main.go:25
github.com/myorg/myapp/handlers.GetUser
/src/bootdev/course-draft-learn-logging/examples/errors/stacktrace/main.go:40
github.com/myorg/myapp/server.ServeHTTP
/src/bootdev/course-draft-learn-logging/examples/errors/stacktrace/main.go:55
net/http.serverHandler.ServeHTTP
/usr/local/go/src/net/http/server.go:2887
net/http.(*conn).serve
/usr/local/go/src/net/http/server.go:1952
The full stack trace shows immediately where the error occurred, and what code paths led to it.
Go's standard logging libraries don't give us stack traces out of the box, but several third-party libraries do. One of the earliest and most popular is github.com/pkg/errors. Although this package is now archived and no longer actively maintained, many libraries still maintain compatibility with its API because it was so widely adopted.
There are two important parts of the process:
Wrapping an error with a stack trace is easy with WithStack:
import pkgerr "github.com/pkg/errors"
func Start(ctx context.Context) error {
err := db.Connect(ctx)
if err != nil {
// adds a stack trace to the
// err at the point `WithStack` is called
return pkgerr.WithStack(err)
}
return nil
}
I like to wrap errors at the boundary between my code and the code I don't control (standard library, third-party libraries, etc.). This way I just get stack traces "at the edges" of my code, and they're maximally useful for finding issues in my code, without being too noisy.
Printing the stack with the error is easy; a single formatting verb does the trick:
cause := errors.New("whoops")
err := pkgerr.WithStack(cause)
// adds the stack trace to the end of the error message
logger.Error(msg, "error", fmt.Sprintf("%+v", err))
Handlers in log/slog give us a ReplaceAttr callback that can centralize stack trace extraction logic:
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: replaceAttr,
}))
func replaceAttr(groups []string, a slog.Attr) slog.Attr {
if a.Key == "error" {
err, ok := a.Value.Any().(error)
if !ok {
return a
}
return slog.String("error", fmt.Sprintf("%+v", err))
}
return a
}
With this, if any structured log has an error attribute, it formats that error using %+v – which, for errors wrapped with WithStack, includes the full stack trace. If the error doesn't have a stack trace, it still prints the error message as usual.
Add stack traces to Linko's logged errors.
go get github.com/pkg/errors
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.