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


Performance considerations

You’ve likely wondered why the log/slog package has some odd-looking functions and concepts in some places. Why do you set a handler’s level to a Leveler value, rather than a simple Level? Why so many ways to create key/value pairs ("key", "value" vs "key", slog.AnyValue("value") vs "key", slog.StringValue("value") vs slog.Any("key", "value") vs slog.String("key", "value"))? It mostly comes down to one thing: Performance. Or, more accurately, trying to balance performance with an easy-to-use API.


Working with Records

In my experience, it’s rare you’ll need to worry about slog Records, unless you’re writing a Handler, or some kind of middleware/transform. But even if you never need to manipulate a Record directly, understanding the concept can be useful. Working with Records Sometimes a Handler will need to modify a Record before passing it on to another Handler or backend. A Record contains a mixture of simple public fields (e.g. Time, Level, Message) and hidden fields that refer to state (such as attributes) indirectly.


Wrapping output methods

Let’s talk about a feature I’ve never used, or even knew existed… I mentioned a while back that the AddSource field of HandlerOptions controls whether the log output includes the source code position of the log call. But what if that log call is wrapped by a helper, obscuring the meaningful source position? Wrapping output methods The logger functions use reflection over the call stack to find the file name and line number of the logging call within the application.

Get daily content like this in your inbox!

Subscribe