Live data from Hacker News

Logging in Go with Slog: A Practitioner's Guide

dash0.com

11–20 of 59 posts

Re: Logging in Go with Slog: A Practitioner's Guide

#11
The 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.

Re: Logging in Go with Slog: A Practitioner's Guide

#12
post #4

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

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":"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

#13
post #12

Earlier 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"…

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.

Re: Logging in Go with Slog: A Practitioner's Guide

#15

I 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…

I’m not sure I understand what you mean by “magic for config”. You create and configure a logger using slog.New(…). You can use the default logger instead, slog.Default(), which is just a global and has a default config. You can also set the default logger using slog.SetDefault(…).

It’s extremely unmagical in my opinion.

Re: Logging in Go with Slog: A Practitioner's Guide

#16
post #12

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

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

#17
post #16

Earlier 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?

Not sure what your definition of "supported" is, but I'm afraid you're going to have to bite the bullet and ... gasp ... read the documentation https://pkg.go.dev/log/slog

Re: Logging in Go with Slog: A Practitioner's Guide

#18

I 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…

The "magic" is just global state? I agree that I try to avoid global state in my code but it's hardly Spring Boot levels of auto wiring bullshit.

Re: Logging in Go with Slog: A Practitioner's Guide

#19
post #12

Earlier 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"…

That seems to work as expected?

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

#20
post #11

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

We had the same annoyance, and wrote https://pkg.go.dev/github.com/chainguard-dev/clog/gcp to bridge the gap.

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)
Post reply on HN