slog Handler Design Deep Dive

Updated

September 8, 2026

slog Handler Design Deep Dive

Overview

log/slog separates Logger (API) from Handler (serialization + output). Custom handlers power redaction, sampling, OpenTelemetry bridges, and multi-sink fanout.

Basics: fmt/log/slog, structured logging.

Diagram: Logger vs Handler

  slog.Logger
       │
       v
  Handler
     ├── Enabled(level)?
     ├── Handle(Record) ──► JSON / text / OTel
     └── WithAttrs / WithGroup

Interfaces

type Handler interface {
    Enabled(ctx context.Context, level slog.Level) bool
    Handle(ctx context.Context, r slog.Record) error
    WithAttrs(attrs []slog.Attr) Handler
    WithGroup(name string) Handler
}

Rules for custom handlers:

  1. Honor Enabled to avoid expensive attr work
  2. WithAttrs/WithGroup return new handlers (immutability)
  3. Be concurrent-safe — loggers are shared
  4. Do not panic on normal records

Patterns

Pattern Idea
Middleware handler Wrap another handler; filter/redact
Sampling Drop debug under load
Fanout Write JSON + ship metrics counter
Context attrs Pull request_id from ctx in Handle
type RedactHandler struct{ slog.Handler }

func (h RedactHandler) Handle(ctx context.Context, r slog.Record) error {
    r.Attrs(func(a slog.Attr) bool {
        if a.Key == "password" || a.Key == "token" {
            // rebuild record without secrets — production code clones carefully
        }
        return true
    })
    return h.Handler.Handle(ctx, r)
}

Performance

  • JSON handler cost ≈ encoding + I/O
  • Avoid fmt.Sprintf in hot attr values
  • Precompute constant attrs via With

Experiment

go mod init example
go run .
package main

import (
    "context"
    "log/slog"
    "os"
)

type MinLevel struct {
    slog.Handler
    min slog.Level
}

func (h MinLevel) Enabled(ctx context.Context, l slog.Level) bool {
    return l >= h.min && h.Handler.Enabled(ctx, l)
}

func main() {
    base := slog.NewJSONHandler(os.Stdout, nil)
    log := slog.New(MinLevel{Handler: base, min: slog.LevelInfo})
    log.Debug("hidden")
    log.Info("visible", "n", 1)
}

What to notice: Debug suppressed without formatting work beyond Enabled checks on the wrapper.

Try next: Add attribute redaction for keys authorization and cookie.