It's fine for application logging but I have two gripes with slog:
1) If you're writing a library that can be used by many different applications and want to emit logs, you'll still need to write a generic log interface with adapters for slog, zap, charmlog, etc. That the golang team refuses to bless a single interface for everyone to settle on both makes sense given their ideological standpoint on shipping interfaces and also causes endless mild annoyance and code duplication.
2) I believe it's still impossible to see the correct callsite in test logs when using slog as the logger. For more information, see https://github.com/neilotoole/slogt?tab=readme-ov-file#defic.... It's possible I'm out of date here — please correct me if this is wrong, it's actually a much larger annoyance for me and one of the reasons I still use uber/zap or charmbracelet/log.
Overall, especially given that it performs worse than uber/zap and everyone has basically standardized on that and it provides essentially the same interface, I recommend using uber/zap instead.
EDIT: just to expand further, take a look at the recommended method of wrapping helper methods that call logs. Compare to the `t.Helper()` approach. And some previous discussion. Frustrating!
1) The idea is that your library should accept the slog logger and use it. The caller would create a logger with a handler that defines how log messages are handled. But there are problems with supported types; see my other comments.
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.
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 interfaces. The end result is a bit of a mess.
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.
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.
My problem with that is that it makes it impossible to use slog logger safely without knowing what handler is being used. Which kind of defeats the purpose of defining the common structured logging interface.
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
Not sure I understand your sarcasm. I read the documentation, source code, handler writing guide, and issues in the Go repository multiple times over two years, and I use slog extensively. Go is my primary language since r60. I think I know how to read Go docs.
Now, please point me to the place in the documentation that says if I can or can't use a value implementing the error interface as an attribute value, and will the handler or something else would call the `Error() string` method.
My definition of "supported" is simple – I could pass a supported value to the logger and get a reasonable representation from any handler. In my example, the JSON handler does not provide it for the fmt.Stringer.
I'm surprised this isn't a standard base pattern in languages, to be honest. Apache's commons-logging library was a standard part of enterprise java placements for many years, and only started to go away when Log4J came along.
Log4j is one of the possible backends for commons logging (and was basically the reason for it - choosing between log4j and the built-in java logging). I think you mean SLF4J?
I may be remembering it wrong, but I think log4j only became a commons logging backend several years after it became mainstream; before that I remember the two being entirely different and no interchangeable. It's a long time ago!
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 up, and I don't have a usecase, it just strikes me as magic is all
There's a default logger that's used when you call package-level functions (as opposed to methods on an instance of slog.Logger). The default logger is probably what you configured in your main package.
In my opinion this is perfectly idiomatic Go. Sometimes the package itself hosts one global instance. If you think that's "magic" then you must think all of Go is magic. It helps to think of a package in Go as equivalent to a single Java class. Splitting up a Go package's code into multiple files is purely cosmetic.
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(…).
I'm a big fan of slog, and this is a great overview.
The fact it is so flexible and composable, while still maintaining a simple API is just great design. I wasn't aware of the performance overhead compared to something like zerolog, but this shouldn't be a concern for most applications.
It's fine for application logging but I have two gripes with slog:
1) If you're writing a library that can be used by many different applications and want to emit logs, you'll still need to write a generic log interface with adapters for slog, zap, charmlog, etc. That the golang team refuses to bless a single interface for everyone to settle on both makes sense given their ideological standpoint on shipping interfaces and also causes endless mild annoyance and code duplication.
2) I believe it's still impossible to see the correct callsite in test logs when using slog as the logger. For more information, see https://github.com/neilotoole/slogt?tab=readme-ov-file#defic.... It's possible I'm out of date here — please correct me if this is wrong, it's actually a much larger annoyance for me and one of the reasons I still use uber/zap or charmbracelet/log.
Overall, especially given that it performs worse than uber/zap and everyone has basically standardized on that and it provides essentially the same interface, I recommend using uber/zap instead.
EDIT: just to expand further, take a look at the recommended method of wrapping helper methods that call logs. Compare to the `t.Helper()` approach. And some previous discussion. Frustrating!
- https://pkg.go.dev/log/slog#example-package-Wrapping
- https://github.com/golang/go/issues/59145#issuecomment-14770...
1) The idea is that your library should accept the slog logger and use it. The caller would create a logger with a handler that defines how log messages are handled. But there are problems with supported types; see my other comments.
2) It is improved in 1.25. See https://github.com/golang/go/issues/59928 and https://pkg.go.dev/testing#T.Output. Now it is possible to update slogt to provide correct callsite – the stack depth should be the same.
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
(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)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 interfaces. The end result is a bit of a mess.
All values are supported.
Well, is fmt.Stringer supported? The result might surprise you:
This code produces 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.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.
My problem with that is that it makes it impossible to use slog logger safely without knowing what handler is being used. Which kind of defeats the purpose of defining the common structured logging interface.
If you need more control, you can create a wrapper type that implements `slog.LogValuer`
Usage example: 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
Not sure I understand your sarcasm. I read the documentation, source code, handler writing guide, and issues in the Go repository multiple times over two years, and I use slog extensively. Go is my primary language since r60. I think I know how to read Go docs.
Now, please point me to the place in the documentation that says if I can or can't use a value implementing the error interface as an attribute value, and will the handler or something else would call the `Error() string` method.
My definition of "supported" is simple – I could pass a supported value to the logger and get a reasonable representation from any handler. In my example, the JSON handler does not provide it for the fmt.Stringer.
I'm surprised this isn't a standard base pattern in languages, to be honest. Apache's commons-logging library was a standard part of enterprise java placements for many years, and only started to go away when Log4J came along.
Log4j is one of the possible backends for commons logging (and was basically the reason for it - choosing between log4j and the built-in java logging). I think you mean SLF4J?
I may be remembering it wrong, but I think log4j only became a commons logging backend several years after it became mainstream; before that I remember the two being entirely different and no interchangeable. It's a long time ago!
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 up, and I don't have a usecase, it just strikes me as magic is all
There's a default logger that's used when you call package-level functions (as opposed to methods on an instance of slog.Logger). The default logger is probably what you configured in your main package.
In my opinion this is perfectly idiomatic Go. Sometimes the package itself hosts one global instance. If you think that's "magic" then you must think all of Go is magic. It helps to think of a package in Go as equivalent to a single Java class. Splitting up a Go package's code into multiple files is purely cosmetic.
If you need log output in tests, you should use an instance of a logger.
More teams should be validating their logging and should be leveraging structured logging and getting valuable logging insights/events.
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.
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.
I'm a big fan of slog, and this is a great overview.
The fact it is so flexible and composable, while still maintaining a simple API is just great design. I wasn't aware of the performance overhead compared to something like zerolog, but this shouldn't be a concern for most applications.