More with Groups

April 29, 2026

Organizing log key/value pairs by group is a nice way to organize your logging data, but what about organizing the way your application groups data?

Maybe you want all logs created by a particular code path to be grouped together. How can you accomplish this?

Enter WithGroup

Groups

Use Logger.WithGroup to qualify all of a Logger’s output with a group name. Calling WithGroup on a Logger results in a new Logger with the same Handler as the original, but with all its attributes qualified by the group name.

This can help prevent duplicate attribute keys in large systems, where subsystems might use the same keys. Pass each subsystem a different Logger with its own group name so that potential duplicates are qualified:

logger := slog.Default().With("id", systemID)
parserLogger := logger.WithGroup("parser")
parseInput(input, parserLogger)

When parseInput logs with parserLogger, its keys will be qualified with “parser”, so even if it uses the common key “id”, the log line will have distinct keys.

So we’ve already seen how you can use With to add attributes to all logs in a logger:

logger = logger.With("request_id", req.Get("ID"))

WithGroup works the same way, but any additional keys added to the returned logger are added to the group:

func FooMiddleware(logger *slog.Logger) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        logger = logger.With("foo_middleware")

        /* ... */
        logger.Info("Handling request", "method", r.Method)

        /* ... */
        logger.Debug("Checking credentials")

        /* ... */
        if err != nil {
            logger.Error("foo failed", "error", err")
        }
    }
}

In this above example, all three logs add their keys inside the “foo_middleware” group, without having to specify the group more than once.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


slog.HandlerOptions

Now we’ll begin looking at HandlerOptions. Despite the Handler-centricity, this is something we care heavily about as mere users of the slog library. Whenever you instantiate either slog.JSONHandler or slog.TextHandler, you have the option of providing a HandlerOptions object, to tweak the default handler behavior. We’ll look at each of the config options, one at a time. type HandlerOptions type HandlerOptions struct { // AddSource causes the handler to compute the source code position // of the log statement and add a SourceKey attribute to the output.


The better test alternative to slog.DiscardHandler

Last time I described slog.DiscardHandler as a viable way to silence logs in tests. Now I want to tell you why I never use that option, and you probably shouldn’t, either. But first a story. The slog package was introduced with Go 1.21 in August of 2023. It didn’t yet have slog.DiscardHandler, so you had to roll your own. But that was easy: logger := slog.New(slog.NewTextHandler(io.Discard, nil)) Easy as it was, it was also annoying.


slog.DiscardHandler

Now that we’ve made it through the Handler interface, let’s look at the first of the implementations provided by the standard library: the DiscardHandler. var DiscardHandler Handler = discardHandler{} DiscardHandler discards all log output. DiscardHandler.Enabled returns false for all Levels. Why would you ever want a discard handler? Two likely scenarios come to mind. The one where I most frequently use it, is testing. You may not wish to log anything during a test, and this is a simple way to do that:

Get daily content like this in your inbox!

Subscribe