Logging in Go with Slog: A Practitioner's Guide
11–20 of 59 posts
Re: Logging in Go with Slog: A Practitioner's Guide
#12My biggest gripe with slog is that there is no clear guidance on supported types of attributes. One could argue that supported types are the ones provided by Attr "construct" functions (like slog.String, slog.Duration, etc), but it is not enough. For example, there is no function for int32 – does it mean it is not supported? Then there is slog.Any and some support in some handlers for error and fmt.Stringer interface…
All values are supported.
req := expvar.NewInt("requests")
req.Add(1)
attr := slog.Any("requests", req)
slog.New(slog.NewTextHandler(os.Stderr, nil)).Info("text", attr)
slog.New(slog.NewJSONHandler(os.Stderr, nil)).Info("json", attr)
This code produces time=2025-09-12T13:15:42.125+02:00 level=INFO msg=text requests=1
{"time":"2025-09-12T13:15:42.125555+02:00","level":"INFO","msg":"json","requests":{}}
So the code that uses slog but does not know what handler will be used can't rely on it lazily calling the `String() string` method: half of the standard handlers do that, half don't.Re: Logging in Go with Slog: A Practitioner's Guide
#13Earlier quoted context omitted.
All values are supported.
Well, is fmt.Stringer supported? The result might surprise you: req := expvar.NewInt("requests") req.Add(1) attr := slog.Any("requests", req) slog.New(slog.NewTextHandler(os.Stderr, nil)).Info("text", attr) slog.New(slog.NewJSONHandler(os.Stderr, nil)).Info("json", attr) This code produces time=2025-09-12T13:15:42.125+02:00 level=INFO msg=text requests=1 {"time":"2025-09-12T13:15:42.125555+02:00","level":"INFO","msg"…
type StringerValue struct {
fmt.Stringer
}
func (v StringerValue) LogValue() slog.Value {
return slog.StringValue(v.String())
}
Usage example: slog.Any("requests", StringerValue{req})
There might be a case for making the expvar types implement `slog.LogValuer` directly.Re: Logging in Go with Slog: A Practitioner's Guide
#14Re: Logging in Go with Slog: A Practitioner's Guide
#15I have a gripe with slog - it uses magic for config What I mean is, if you configure slog in (say) your main package, then, by magic, that config is used by any call to slog within your application. There's no "Oh you are using this instance of slog that has been configured to have this behaviour" - it's "Oh slog got configured so that's the config you have been given" I've never tried to see if I can split configs u…
It’s extremely unmagical in my opinion.
Re: Logging in Go with Slog: A Practitioner's Guide
#16Earlier quoted context omitted.
Well, is fmt.Stringer supported? The result might surprise you: req := expvar.NewInt("requests") req.Add(1) attr := slog.Any("requests", req) slog.New(slog.NewTextHandler(os.Stderr, nil)).Info("text", attr) slog.New(slog.NewJSONHandler(os.Stderr, nil)).Info("json", attr) This code produces time=2025-09-12T13:15:42.125+02:00 level=INFO msg=text requests=1 {"time":"2025-09-12T13:15:42.125555+02:00","level":"INFO","msg"…
If you need more control, you can create a wrapper type that implements `slog.LogValuer` type StringerValue struct { fmt.Stringer } func (v StringerValue) LogValue() slog.Value { return slog.StringValue(v.String()) } Usage example: slog.Any("requests", StringerValue{req}) There might be a case for making the expvar types implement `slog.LogValuer` directly.
And I know that I can create a wrapper for unsupported types. My problem is exactly that – I don't know what types are supported. Is error supported, for example? Should I create a wrapper for it? And, as a handler author, should I support it directly or not?
Re: Logging in Go with Slog: A Practitioner's Guide
#17Earlier quoted context omitted.
If you need more control, you can create a wrapper type that implements `slog.LogValuer` type StringerValue struct { fmt.Stringer } func (v StringerValue) LogValue() slog.Value { return slog.StringValue(v.String()) } Usage example: slog.Any("requests", StringerValue{req}) There might be a case for making the expvar types implement `slog.LogValuer` directly.
So clearly not all values are supported. And I know that I can create a wrapper for unsupported types. My problem is exactly that – I don't know what types are supported. Is error supported, for example? Should I create a wrapper for it? And, as a handler author, should I support it directly or not?
Re: Logging in Go with Slog: A Practitioner's Guide
#18I have a gripe with slog - it uses magic for config What I mean is, if you configure slog in (say) your main package, then, by magic, that config is used by any call to slog within your application. There's no "Oh you are using this instance of slog that has been configured to have this behaviour" - it's "Oh slog got configured so that's the config you have been given" I've never tried to see if I can split configs u…
Re: Logging in Go with Slog: A Practitioner's Guide
#19Earlier quoted context omitted.
All values are supported.
Well, is fmt.Stringer supported? The result might surprise you: req := expvar.NewInt("requests") req.Add(1) attr := slog.Any("requests", req) slog.New(slog.NewTextHandler(os.Stderr, nil)).Info("text", attr) slog.New(slog.NewJSONHandler(os.Stderr, nil)).Info("json", attr) This code produces time=2025-09-12T13:15:42.125+02:00 level=INFO msg=text requests=1 {"time":"2025-09-12T13:15:42.125555+02:00","level":"INFO","msg"…
The output of data is handled by the handler. Such behaviour is clearly outlined in the documentation by the JSONHandler. I wouldn't expect a JSONHandler to use Stringer. I'd expect it to use the existing JSON interfaces, which it does.
I'd expect the Text handler to use TextMarshaller. Which it does. Or Stringer, which it does implicitly via fmt.Sprintf.
Re: Logging in Go with Slog: A Practitioner's Guide
#20The thing that gets me about slog is that the output key for the slog JSON handler is msg, but that's not compatible with Googles own GCP Stackdriver logging. Since that key is a constant I now need to use an attribute replacer to change it from msg to message (or whatever it is stackdriver wants). Good work Google.
It's a slog handler that formats everything the way GCP wants, including with trace contexts, etc.
We've had this in production for months, and it's been pretty great.
You can add this at your main.go
import _ "github.com/chainguard-dev/clog/gcp/init"
(the rest of the library is about attaching a logger to a context.Context, but you don't need to use that to use the GCP logger)