Today’s topic is probably the most interesting, subtle, and complicated of the three HandlerOptions fields: ReplaceAttr.
type HandlerOptions
type HandlerOptions struct { … // ReplaceAttr is called to rewrite each non-group attribute before it is logged. // The attribute's value has been resolved (see [Value.Resolve]). // If ReplaceAttr returns a zero Attr, the attribute is discarded. // // The built-in attributes with keys "time", "level", "source", and "msg" // are passed to this function, except that time is omitted // if zero, and source is omitted if AddSource is false. // // The first argument is a list of currently open groups that contain the // Attr. It must not be retained or modified. ReplaceAttr is never called // for Group attributes, only their contents. For example, the attribute // list // // Int("a", 1), Group("g", Int("b", 2)), Int("c", 3) // // results in consecutive calls to ReplaceAttr with the following arguments: // // nil, Int("a", 1) // []string{"g"}, Int("b", 2) // nil, Int("c", 3) // // ReplaceAttr can be used to change the default keys of the built-in // attributes, convert types (for example, to replace a `time.Time` with the // integer seconds since the Unix epoch), sanitize personal information, or // remove attributes from the output. ReplaceAttr func(groups []string, a Attr) Attr }
ReplaceAttr gives you the ability to replace, or remove, log attributes before they’re propagated to the log.
Today I’ll share one common use-case for this: Testing logs.
I’ve already talked about how to omit logs when testing, but if you’re ever testing logs themselves, you’ll want some way to test your logs without the noise of an ever-changing timestamp. ReplaceAttr gives you one simple way to do that:
Simply rewrite every timestamp to a static value!
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
// Remove the time attribute
return slog.Attr{}
}
// Pass all others through unaltered
return a
},
}))
With your log handler configured this way, you can do exact string matches in your tests:
const want = "level=INFO msg=Message foo=bar"
if got := buf.String(); want != got {
t.Error("Unexpected log output.\nWant: %s\n Got: %s\n", want, got)
}