Logging in Go with Slog: a Practitioner's Guide
Posted4 months agoActive4 months ago
dash0.comTechstory
calmmixed
Debate
60/100
Go Programming LanguageLoggingStructured Logging
Key topics
Go Programming Language
Logging
Structured Logging
The article discusses the new slog logging package in Go, and the discussion revolves around its design, features, and potential issues, with some users praising its flexibility and others criticizing its limitations.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
4d
Peak period
25
96-108h
Avg / period
7
Comment distribution49 data points
Loading chart...
Based on 49 loaded comments
Key moments
- 01Story posted
Sep 8, 2025 at 9:03 AM EDT
4 months ago
Step 01 - 02First comment
Sep 12, 2025 at 5:14 AM EDT
4d after posting
Step 02 - 03Peak activity
25 comments in 96-108h
Hottest window of the conversation
Step 03 - 04Latest activity
Sep 16, 2025 at 1:53 PM EDT
4 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45167739Type: storyLast synced: 11/20/2025, 5:23:56 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
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.
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?
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.
> Values are formatted as with an encoding/json.Encoder with SetEscapeHTML(false), with two exceptions.
> First, an Attr whose Value is of type error is formatted as a string, by calling its Error method. Only errors in Attrs receive this special treatment, not errors embedded in structs, slices, maps or other data structures that are processed by the encoding/json package.
So the json handler more or less works as if you called json.Marshal, which sounds pretty reasonable.
for a reasonable substitute subset, use the core language types, and implement LogValuer for anything complex.
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.
Does it, though? Why would the log producer care about how the log entires are formatted? Only the log consumer cares about that.
As with everything in life, there are tradeoffs to that approach, of course, and it might be hard to grasp if you come from languages which different idioms that prioritize producer over consumer, but if you look closely everything about Go is designed to prioritize the needs of the consumer over the needs of the producer. That does seem to confuse a lot of people, interestingly. I expect because it isn't idiomatic to prioritize the consumer in a lot of other languages and people get caught up in trying to write code in those other languages using Go syntax instead of actually learning Go.
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
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.
More teams should be validating their logging and should be leveraging structured logging and getting valuable logging insights/events.
It’s extremely unmagical in my opinion.
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)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...
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.
2) TIL about `T.Output`, thank you, that's great to know about. Still annoying and would be nice if the slog package showed an example of logging from tests with correct callsites. Golang gets so many things right about testing, so the fact that logging in tests is difficult really stands out and bothers me.
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.
For an example from one of my own libraries, see
https://github.com/peterldowns/pgmigrate/blob/d3ecf8e4e8af87...
There is: https://pkg.go.dev/golang.org/x/exp/slog#Handler
If, say, zap was conformant, you'd slog.New(zap.NewHandler()) or whatever and away you go. It seems the only problem here is that the logging packages you want to use are not following the blessed, idiomatic path.
> For an example from one of my own libraries
There are a lot of problem with that approach at scale. That might not matter for your pet projects, but slog also has to serve those who are pushing computers to their limits. Your idea didn't escape anyone.
I don't, really. If performance is of utmost concern, you're not going to accept the overhead of passing the logger through an interface anyway, so that's moot. A library concerned about performance as a top priority has to pick one and only one.
But if a library has decided that flexibility is more important than raw efficiency, then the interface is already defined.
The only 'problem' I can see is if `zaplogger` hasn't implemented the interface. But there isn't much the Go team can do about implementations not playing nicely.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...
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.
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...
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 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
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.
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.
and even a slog bridge https://pkg.go.dev/go.opentelemetry.io/contrib/bridges/otels...
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!
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.
3 more comments available on Hacker News