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


Expanding errors with ReplaceAttr

Let’s look at one other example where I reach for ReplaceAttr regularly: Logging additional error detail. (I talk more about this approach in my Boot.Dev course Learn Logging and Observability in Go (see below for a discount code).) For this technique to be meaningful, I first need errors that contain additional details. And this can take many forms, but probably the most ubiquitous form is an error that contains a stack trace, such as those produced by the popular package github.


Test logging with ReplaceAttr

Today’s topic is probably the most interesting, subtle, and complicated of the three HandlerOptions fields: ReplaceAttr. type HandlerOptions type HandlerOptions struct { … // ReplaceAttr is called to rewrite each non-group attribute before it is logged. // The attribute's value has been resolved (see [Value.Resolve]). // If ReplaceAttr returns a zero Attr, the attribute is discarded. // // The built-in attributes with keys "time", "level", "source", and "msg" // are passed to this function, except that time is omitted // if zero, and source is omitted if AddSource is false.


Handler level

Each handler has an optional Level value, which controls which logs to drop, and which to actually log: type HandlerOptions type HandlerOptions struct { … // Level reports the minimum record level that will be logged. // The handler discards records with lower levels. // If Level is nil, the handler assumes LevelInfo. // The handler calls Level.Level for each record processed; // to adjust the minimum level dynamically, use a LevelVar.

Get daily content like this in your inbox!

Subscribe