Logging best practices with log/slog

535 views
Skip to first unread message

cpu...@gmail.com

unread,
Mar 28, 2025, 8:58:51 AM3/28/25
to golang-nuts
I'm in the process of converting our application (evcc.io) from using spf13/jwalterweatherman for logging to log/slog. One current pain point is passing loggers around the application and deriving child loggers. With using slog there are various options for doing that:

Passing loggers:
  • pass parent logger as parameter
  • pass a context and retrieve the parent logger from context value

Passing log attributes/groups to child loggers:
  • assign them to loggers and pass them via the logger (see before- either as parameter or context value)
  • assign them to context and retrieve them from context value for assigning to the child logger
Not sure those questions are clear enough, but let's try: are there experiences what works "best" in larger applications?

Cheers,
Andi

Andrew Harris

unread,
Mar 29, 2025, 5:45:45 PM3/29/25
to golang-nuts
The question of how to pass loggers around was discussed at significant length in GitHub, and drew a lot of strong opinions. More so than other standard library stuff, any particular piece of the slog API is maybe less an eager endorsement of best practices, but more about matching what exists in other popular logging packages. Particularly context/logger usage was sort of a bright line division, I think a fair summary is that it's not an option for some developers, and for others it's admittedly a trade-off that makes sense because contexts are already plumbed through everything.

The jww package begins to address the question by using a configurable global logger, like slog.Default. Before moving away from that - and it might be enough, really, it's greatly simplifying - I think it might be worth working out what kinds of structure is already latent in the application. Personally I've come to prefer implementing the LogValuer interface and global loggers, except when there are really easy/obvious places to use child loggers. But, there are lots of opinions and lots of real-world examples of different approaches working out as well.

Mike Schinkel

unread,
Mar 30, 2025, 12:36:00 AM3/30/25
to Andrew Harris, cpu...@gmail.com, GoLang Nuts Mailing List
> On Mar 28, 2025, at 8:58 AM, cpu...@gmail.com <cpu...@gmail.com> wrote:
>
> I'm in the process of converting our application (evcc.io) from using spf13/jwalterweatherman for logging to log/slog.

This is a very serendipitously timed discussion (for me) since yesterday and today I have been working on how to configure logging for a new project I am working on.

The solution I have is built on top of slog and leverages slog.Default() and then uses a package variable `logger` defined in every package I write. I add a `logger.go` file to every one of my packages and then add this line of code which uses a small package I write with a `Logger` type simply to embed slog.Log so I could add my own methods and to preprocess calls to *slog.Logger if I find that to be needed down the road.

var logger = logging.NewPackageLogger("<package_name>") // UPDATE this with your package name

I don't love this approach because it requires adding an updated version of this file to every package I write but it feels the most workable solution I have tried to date, and this is about my 5th iteration of trying to find a workable solution for logging. (BTW, sure would be nice if Go had a way to capture the name of the current package using a well-known constant like PACKAGE_NAME, but I digress.)

Here is the current state of my logging package along with a few examples of using it:

https://gist.github.com/mikeschinkel/3b1fb9ac4bbc200c95d0fc31692959ce

I have not yet run this approach in production so I do not know if I will discover it to be unworkable. I decided to mention it here to solicit input from anyone who might be able to find a reason this approach might not be workable.

> On Mar 29, 2025, at 5:44 PM, Andrew Harris <harris...@gmail.com> wrote:
>
> The question of how to pass loggers around was discussed at significant length in GitHub, and drew a lot of strong opinions.

Can you link that discussion, please, for those of us who may have not seen the discussion nor be able to find the specific discussion via search?

> More so than other standard library stuff, any particular piece of the slog API is maybe less an eager endorsement of best practices, but more about matching what exists in other popular logging packages.

Yes.

While I do appreciate slog for being a big improvement over log, it still does not solve the inconsistency problem across 3rd party packages used by a Go app. And the fact there are a lot of strong conflicting opinions ensures we won't see a convergence to a defacto-standard approach to logging unless and until there emerges some new innovation for logging from the Go team.

-Mike

P.S. Maybe Go could add a way for a package to register some sort of middleware hook so that a developer wanting to orchestrate the use of logging across all packages use could do so like we can currently handle HTTP requests and responses? OTOH, I almost feel like logging needs special handling by the compiler given how opinions regarding best practices diverge to such an extent... 🤔


Andrew Harris

unread,
Mar 30, 2025, 3:45:59 AM3/30/25
to golang-nuts
> Can you link that discussion, please, for those of us who may have not seen the discussion nor be able to find the specific discussion via search?

https://github.com/golang/go/issues/56345 (super-thread)
https://github.com/golang/go/issues/58243 (what to do about context)

There's more scattered in other closed issues. I remain totally impressed by Jonathan Amsterdam's willingness to have kept up with things ... particularly with contexts the API evolved a bit in ways that were difficult to work out before lots of public commentary.


> it still does not solve the inconsistency problem across 3rd party packages used by a Go app

FWIW I think pkg.go.dev "Imported by" numbers show adoption of slog has been pretty good - I am going by memory but slog is up to 40k importers, and I'm fairly certain that logrus, zap, and zerolog have grown by maybe a few thousand since slog was included in the standard library. In theory custom wrapping of slog Handlers is flexible enough to integrate diverging opinions between dependencies of a project. I don't know how frequently that is leveraged in solutions.

Mike Schinkel

unread,
Apr 1, 2025, 3:30:34 AM4/1/25
to Andrew Harris, GoLang Nuts Mailing List
On Mar 30, 2025, at 3:45 AM, Andrew Harris <harris...@gmail.com> wrote:

> Can you link that discussion, please, for those of us who may have not seen the discussion nor be able to find the specific discussion via search?

https://github.com/golang/go/issues/56345 (super-thread)
https://github.com/golang/go/issues/58243 (what to do about context)

Thank you for those links.

> it still does not solve the inconsistency problem across 3rd party packages used by a Go app

FWIW I think "Imported by" numbers show adoption of slog has been pretty good - I am going by memory but slog is up to 40k importers

Given that log has been "imported by" 1,270k times — or +30 times more than slog — and I'd say we are a very long way from slog being defacto-standard, unfortunately. 

In theory custom wrapping of slog Handlers is flexible enough to integrate diverging opinions between dependencies of a project. I don't know how frequently that is leveraged in solutions.

Depending on how slog is implemented in a project, yes that is true. And I do see slog was a great improvement over log. 

OTOH slog was a necessary but still not sufficient step for developers to gain full control over logging in their apps. Additionally it would be helpful if Go made it possible to wrest control of logging from a package used by an app and give that control back to the app developer. A good place to start would be for all those packages that use log. 

-Mike

P.S. One simple approach, at least for log, might be for the Go team to add (something like) a log.WriterFilter package var of type func(io.Writer)io.Writer that could be used together with log.SetOutput() by an app or package and optionally (and less simply) some way to ensure log.WriterFilter is assigned by an app or package before log.SetOutput is called directly or indirectly by any other package in an init() or package var assignment. #fwiw

robert engels

unread,
Apr 1, 2025, 3:51:25 AM4/1/25
to Mike Schinkel, Andrew Harris, GoLang Nuts Mailing List
I don’t think this is that complicated. The go team can change the implementation of both log to delegate to slog with some defaults.

slog already has the infrastructure to support additional handlers which I think is all that is needed to support custom requirements - aka side effects - of external libraries.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/5EA9AFEA-D954-46B5-BE78-1550F7E52D94%40newclarity.net.

Mike Schinkel

unread,
Apr 2, 2025, 11:32:48 PM4/2/25
to robert engels, GoLang Nuts Mailing List
> On Apr 1, 2025, at 3:50 AM, robert engels <ren...@ix.netcom.com> wrote:
>
> I don’t think this is that complicated. ...
>
> slog already has the infrastructure to support additional handlers which I think is all that is needed to support custom requirements - aka side effects - of external libraries.

Hmm. Okay.

Then maybe you can help me understand how to handle something simple like getting an external package to use my handler vs. its hardcoded one?

Below is single file that *simulates* the use-case of calling an external package with private loggers. As the developer of main() I would like to control the output handler for all loggers. Maybe I am just missing something obvious for how to accomplish this?

func main() {
myLogger := slog.New(slog.NewJSONHandler(os.Stdout,nil))
slog.SetDefault(myLogger)
slog.Default().Info("Yay my handler!")
// Calling these functions simulates calling external package w/loggers
otherFunction()
thirdFunction()
}
var otherLogger = slog.New(slog.NewTextHandler(os.Stdout,nil))
func otherFunction() {
otherLogger.Info("Not my handler. :-(")
}
func thirdFunction() {
logger := slog.New(slog.NewTextHandler(os.Stdout,nil))
logger.Info("Also not my handler. :-(")
}

Here it is in Go play: https://go.dev/play/p/Sqlny1oyRlS?v=

-Mike
Reply all
Reply to author
Forward
0 new messages