go 66 lines · 8 steps

Deduplicating log attributes in Go's slog

A custom slog.Handler wrapper that collapses duplicate attribute keys so later values win before logs are emitted.

Explained by highlit
1package logging
2 
3import (
4 "context"
5 "log/slog"
6)
7 
8type DedupeHandler struct {
9 inner slog.Handler
10 attrs []slog.Attr
11}
12 
13func NewDedupeHandler(inner slog.Handler) *DedupeHandler {
14 return &DedupeHandler{inner: inner}
15}
16 
17func (h *DedupeHandler) Enabled(ctx context.Context, level slog.Level) bool {
18 return h.inner.Enabled(ctx, level)
19}
20 
21func (h *DedupeHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
22 return &DedupeHandler{inner: h.inner, attrs: mergeAttrs(h.attrs, attrs)}
23}
24 
25func (h *DedupeHandler) WithGroup(name string) slog.Handler {
26 return &DedupeHandler{inner: h.inner.WithGroup(name), attrs: h.attrs}
27}
28 
29func (h *DedupeHandler) Handle(ctx context.Context, r slog.Record) error {
30 recordAttrs := make([]slog.Attr, 0, r.NumAttrs())
31 r.Attrs(func(a slog.Attr) bool {
32 recordAttrs = append(recordAttrs, a)
33 return true
34 })
35 
36 final := mergeAttrs(h.attrs, recordAttrs)
37 
38 clean := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
39 clean.AddAttrs(final...)
40 return h.inner.Handle(ctx, clean)
41}
42 
43func mergeAttrs(base, incoming []slog.Attr) []slog.Attr {
44 index := make(map[string]int, len(base)+len(incoming))
45 merged := make([]slog.Attr, 0, len(base)+len(incoming))
46 
47 upsert := func(a slog.Attr) {
48 if a.Equal(slog.Attr{}) {
49 return
50 }
51 if i, ok := index[a.Key]; ok {
52 merged[i] = a
53 return
54 }
55 index[a.Key] = len(merged)
56 merged = append(merged, a)
57 }
58 
59 for _, a := range base {
60 upsert(a)
61 }
62 for _, a := range incoming {
63 upsert(a)
64 }
65 return merged
66}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping an existing interface lets you inject behavior without changing callers or the underlying implementation.
  2. 2Returning fresh copies from WithAttrs and WithGroup keeps handlers safe to share across goroutines.
  3. 3A key-to-index map turns last-write-wins deduplication into a single linear pass while preserving order.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Deduplicating log attributes in Go's slog — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code