Something went wrong. Try again.
/x/perimental project monorepo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138package slog
import ( "context" "log/slog" "slices")
// ctxAttrsKey is the private context key under which ContextWithAttrs stores// accumulated attributes.type ctxAttrsKey struct{}
// ContextWithAttrs returns a copy of ctx carrying attrs, in addition to any// attributes already attached to ctx by an outer ContextWithAttrs call. A// [ContextHandler] emits these attributes on every record logged with the// returned context.//// Calls accumulate: attrs from an outer call and attrs from an inner call are// all emitted. Each call allocates a fresh, exactly-sized slice, so deriving// two children from the same parent context never lets one child's attributes// leak into the other.func ContextWithAttrs(ctx context.Context, attrs ...slog.Attr) context.Context { if len(attrs) == 0 { return ctx } prev := attrsFromContext(ctx) merged := make([]slog.Attr, 0, len(prev)+len(attrs)) merged = append(merged, prev...) merged = append(merged, attrs...) return context.WithValue(ctx, ctxAttrsKey{}, merged)}
// attrsFromContext returns the attributes attached to ctx, or nil.func attrsFromContext(ctx context.Context) []slog.Attr { attrs, _ := ctx.Value(ctxAttrsKey{}).([]slog.Attr) return attrs}
// handlerOp is a single recorded WithAttrs or WithGroup operation. Exactly one// of group / attrs is meaningful: a non-empty group means WithGroup, otherwise// it is a WithAttrs carrying attrs.type handlerOp struct { group string attrs []slog.Attr}
// ContextHandler is a [slog.Handler] that emits the attributes attached to a// record's context by [ContextWithAttrs]. The context attributes are always// written at the top level of the record, never nested inside a group opened// with WithGroup, so that call-scoped fields (request IDs, user IDs, ...) stay// queryable regardless of how a derived logger is grouped.type ContextHandler struct { // base is the wrapped handler before any WithAttrs / WithGroup applied to // this ContextHandler. Context attributes are injected here, ahead of ops, // so they land above any group. base slog.Handler // ops records the WithAttrs / WithGroup calls made on this handler, in // order, to be replayed after the context attributes are injected. ops []handlerOp // inner is base with ops already applied. It handles records that carry no // context attributes, and answers Enabled, without per-record rebuilding. inner slog.Handler // grouped reports whether any op opens a group. When false, context // attributes can be appended straight to the record cheaply. grouped bool}
// NewContextHandler wraps inner so that attributes attached with// [ContextWithAttrs] are emitted on every record. The returned handler// delegates Enabled, WithAttrs, and WithGroup to inner.func NewContextHandler(inner slog.Handler) *ContextHandler { return &ContextHandler{base: inner, inner: inner}}
// Enabled delegates to the wrapped handler.func (h *ContextHandler) Enabled(ctx context.Context, level slog.Level) bool { return h.inner.Enabled(ctx, level)}
// Handle injects the context attributes at the top level of rec and delegates// to the wrapped handler.func (h *ContextHandler) Handle(ctx context.Context, rec slog.Record) error { attrs := attrsFromContext(ctx) if len(attrs) == 0 { return h.inner.Handle(ctx, rec) }
// No group is open, so appending to the record keeps the attributes at the // top level. Clone first: the caller still owns rec. if !h.grouped { rec = rec.Clone() rec.AddAttrs(attrs...) return h.inner.Handle(ctx, rec) }
// A group is open. Adding the attributes to the record would bury them // inside that group, so rebuild the handler chain with the context // attributes applied to base, ahead of the recorded groups. hdl := h.base.WithAttrs(slices.Clone(attrs)) for _, op := range h.ops { if op.group != "" { hdl = hdl.WithGroup(op.group) } else { hdl = hdl.WithAttrs(op.attrs) } } return hdl.Handle(ctx, rec)}
// WithAttrs delegates to the wrapped handler and re-wraps the result so the// context behaviour survives on the derived handler.func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } return &ContextHandler{ base: h.base, ops: append(slices.Clip(h.ops), handlerOp{attrs: attrs}), inner: h.inner.WithAttrs(attrs), grouped: h.grouped, }}
// WithGroup delegates to the wrapped handler and re-wraps the result so the// context behaviour survives on the derived handler. Context attributes// continue to be emitted above the group.func (h *ContextHandler) WithGroup(name string) slog.Handler { if name == "" { return h } return &ContextHandler{ base: h.base, ops: append(slices.Clip(h.ops), handlerOp{group: name}), inner: h.inner.WithGroup(name), grouped: true, }}