We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Stack Traces

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.

Pkg Errors Package

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:

  1. Creating errors with stack traces
  2. Extracting stack traces from errors, so they can be logged

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.

Assignment

Add stack traces to Linko's logged errors.

  1. 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.