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.
If zap, charmlog, etc. don't provide conformance to the interface, that's not really on the Go team. It wouldn't be that hard to write your own adapter around your unidiomatic logger of choice if you're really stuck, though. This isn't an actual problem unless you think someone else owes you free labor for some reason.
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.
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.
> The key decision is thus between two patterns: using a global logger or using dependency injection. The former is extremely convenient but adds a hidden dependency that’s hard to test, while the latter is more verbose but makes dependencies explicit, resulting in highly testable and flexible code.
Curious how different people handle this. I personally pretty much always pass a logger into function, classes, structs (what have you) so it has the context I need it to. It's a tad more verbose I guess, but it's such a minor lift I've always found it worth it.
In any API service, it's better to handle via dependency injection IMO.
Instantiate all of your metadata once, and then send that logger down, so that anybody who uses that logger is guaranteed to have the right metadata... the time to add logging is not when you are debugging.
I'd say necessarily verbose. Without injection, it is not immediately apparent that something is dependent on something else (in this case a logger) with side effects, which ultimately harms understandability.
I expect by "more verbose" the author really meant "in need of more typing". I am not sure optimizing for less typing ever a good tradeoff. And if you can find good reason to optimize for less typing, you're not going to be choosing a structured language in the first place, so...
I really like structured logs and am pleased the Go team saw the benefits of bringing it into the standard library.
However, I feel like errors should be able to hold slog attributes. It makes for some very useful and easy error logging, especially when the logging takes place far up the execution chain from where the error happened.
This is easily possible with a custom error type and some log functions. I have published on GitHub my small and crude implementation that I use in a few hobby projects, MIT licensed, if anyone is interested. https://github.com/sveinnthorarins/sterlo
I felt the same way, and made my own package for adding slog attributes to an error for easier logging. It's usually all I need for my own custom errors.
I can understand why it's not in the stdlib though, it seems easy enough to run into key overwriting issues if a dependency returned custom errors with attributes.
I appreciate the built in support slog has for slog.GroupValue and the slog.LogValuer interface that enables everyone to build a solution best for their needs.
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?
To guarantee correctness, you must use the strongly-typed slog.Attr helpers. They make it impossible to create an unbalanced pair by catching errors at compile time
I take their point, but given the typical value of most logging lines, this does not seem worth the tax you pay in readability. This is a gripe I have with oTel too --- it really cruds your code up --- but with oTel you're getting long-term value that logging (which is still my go-to o11y) doesn't.
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(…).
Great article! super helpful for someone like me who recently started building services in Go.
I especially appreciated the section on slog.Attr and the !BADKEY issue, it’s one of those little things that can go unnoticed until logs break in prod.
peterldowns|5 months ago
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...
arccy|5 months ago
The blessed interface for logging backends is slog.Handler.
Applications can then wire that up with a handler they like, for example
zap: https://pkg.go.dev/go.uber.org/zap/exp/zapslog#Handler
charm https://github.com/charmbracelet/log?tab=readme-ov-file#slog...
aleksi|5 months ago
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.
trenchpilgrim|5 months ago
9rx|5 months ago
Uh... https://pkg.go.dev/golang.org/x/exp/slog#Handler
If zap, charmlog, etc. don't provide conformance to the interface, that's not really on the Go team. It wouldn't be that hard to write your own adapter around your unidiomatic logger of choice if you're really stuck, though. This isn't an actual problem unless you think someone else owes you free labor for some reason.
arcaen|5 months ago
ImJasonH|5 months ago
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)aleksi|5 months ago
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.
0x696C6961|5 months ago
imiric|5 months ago
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.
jjice|5 months ago
Curious how different people handle this. I personally pretty much always pass a logger into function, classes, structs (what have you) so it has the context I need it to. It's a tad more verbose I guess, but it's such a minor lift I've always found it worth it.
cfors|5 months ago
Instantiate all of your metadata once, and then send that logger down, so that anybody who uses that logger is guaranteed to have the right metadata... the time to add logging is not when you are debugging.
9rx|5 months ago
I'd say necessarily verbose. Without injection, it is not immediately apparent that something is dependent on something else (in this case a logger) with side effects, which ultimately harms understandability.
I expect by "more verbose" the author really meant "in need of more typing". I am not sure optimizing for less typing ever a good tradeoff. And if you can find good reason to optimize for less typing, you're not going to be choosing a structured language in the first place, so...
stackedinserter|5 months ago
sveinnthorarins|5 months ago
I really like structured logs and am pleased the Go team saw the benefits of bringing it into the standard library.
However, I feel like errors should be able to hold slog attributes. It makes for some very useful and easy error logging, especially when the logging takes place far up the execution chain from where the error happened.
This is easily possible with a custom error type and some log functions. I have published on GitHub my small and crude implementation that I use in a few hobby projects, MIT licensed, if anyone is interested. https://github.com/sveinnthorarins/sterlo
dlock17|5 months ago
https://github.com/Danlock/pkg/blob/main/errors/attr_test.go
I can understand why it's not in the stdlib though, it seems easy enough to run into key overwriting issues if a dependency returned custom errors with attributes.
I appreciate the built in support slog has for slog.GroupValue and the slog.LogValuer interface that enables everyone to build a solution best for their needs.
codeduck|5 months ago
lmz|5 months ago
tptacek|5 months ago
I take their point, but given the typical value of most logging lines, this does not seem worth the tax you pay in readability. This is a gripe I have with oTel too --- it really cruds your code up --- but with oTel you're getting long-term value that logging (which is still my go-to o11y) doesn't.
arccy|5 months ago
and even a slog bridge https://pkg.go.dev/go.opentelemetry.io/contrib/bridges/otels...
awesome_dude|5 months ago
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
roncesvalles|5 months ago
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.
catlifeonmars|5 months ago
It’s extremely unmagical in my opinion.
gdbsjjdn|5 months ago
unknown|5 months ago
[deleted]
vic1102|5 months ago
I especially appreciated the section on slog.Attr and the !BADKEY issue, it’s one of those little things that can go unnoticed until logs break in prod.
Thanks for putting this together!