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

Slog Groups

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

The 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("...")},
)

Assignment

Split error message and stack trace into separate fields.

  1. type stackTracer interface {
    	error
    	StackTrace() pkgerr.StackTrace
    }
    
  2. 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.