diff options
Diffstat (limited to 'logging/chain.go')
| -rw-r--r-- | logging/chain.go | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/logging/chain.go b/logging/chain.go new file mode 100644 index 0000000..d6ccb8a --- /dev/null +++ b/logging/chain.go @@ -0,0 +1,152 @@ +package logging + +import ( + "context" + "iter" + "log/slog" + "slices" +) + +func attrAll(r slog.Record) iter.Seq[slog.Attr] { + return func(yield func(slog.Attr) bool) { + r.Attrs(func(attr slog.Attr) bool { + return yield(attr) + }) + } +} + +// GetNormalizedAttrs extracts, resolves, filters, and flattens all attributes +// from a slog.Record into a ready-to-process slice of slog.Attr. +// This helper is essential for any custom slog.Handler implementation +// that needs to process the full set of attributes. +func GetNormalizedAttrs(r slog.Record) []slog.Attr { + attrs := attrAll(r) + + return normalizeAttrsSlice(attrs) +} + +// normalizeAttrsSlice contains the core logic for resolving, filtering, and flattening. +// This function must be private as it is a utility for GetNormalizedAttrs. +func normalizeAttrsSlice(attrs iter.Seq[slog.Attr]) []slog.Attr { + var out []slog.Attr + for attr := range attrs { + if attr.Equal(slog.Attr{}) { + continue + } + + v := attr.Value.Resolve() + + switch v.Kind() { + case slog.KindGroup: + // Skip empty group + if len(v.Group()) == 0 { + continue + } + + inner := normalizeAttrsSlice(slices.Values(v.Group())) + + // Flatten anonymous group (Key == "") + if attr.Key == "" { + out = append(out, inner...) + } else { + // Named group, keep as a GroupValue + out = append(out, slog.Attr{ + Key: attr.Key, + Value: slog.GroupValue(inner...), + }) + } + default: + // Regular attribute + out = append(out, slog.Attr{ + Key: attr.Key, + Value: v, + }) + } + } + return out +} + +type chainingAttrHandler struct { + parent slog.Handler + attrs []slog.Attr +} + +var _ slog.Handler = (*chainingAttrHandler)(nil) + +// ChainAttrs stores attributes/groups added via WithAttrs/WithGroup +// and ensures they are applied to the record before passing it to the next handler. +func ChainAttrs(h slog.Handler, attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + return &chainingAttrHandler{ + parent: h, + attrs: attrs, + } +} + +func (h *chainingAttrHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.parent.Enabled(ctx, level) +} + +func (h *chainingAttrHandler) Handle(ctx context.Context, r slog.Record) error { + newRecord := slog.NewRecord(r.Time, r.Level, r.Message, r.PC) + + attrs := slices.Collect(attrAll(r)) + newRecord.AddAttrs(h.attrs...) + newRecord.AddAttrs(attrs...) + + return h.parent.Handle(ctx, newRecord) +} + +func (h *chainingAttrHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return ChainAttrs(h, attrs) + +} + +func (h *chainingAttrHandler) WithGroup(name string) slog.Handler { + return ChainGroup(h, name) +} + +type chainingGroupHandler struct { + parent slog.Handler + group string +} + +// ChainGroup stores attributes/groups added via WithAttrs/WithGroup +// and ensures they are applied to the record before passing it to the next handler. +func ChainGroup(h slog.Handler, name string) slog.Handler { + if name == "" { + return h + } + return &chainingGroupHandler{ + parent: h, + group: name, + } +} + +var _ slog.Handler = (*chainingGroupHandler)(nil) + +func (h *chainingGroupHandler) Enabled(ctx context.Context, level slog.Level) bool { + return h.parent.Enabled(ctx, level) +} + +func (h *chainingGroupHandler) Handle(ctx context.Context, r slog.Record) error { + newRecord := slog.NewRecord(r.Time, r.Level, r.Message, r.PC) + + newRecord.AddAttrs(slog.Attr{ + Key: h.group, + Value: slog.GroupValue(GetNormalizedAttrs(r)...), + }) + + return h.parent.Handle(ctx, newRecord) +} + +func (h *chainingGroupHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return ChainAttrs(h, attrs) + +} + +func (h *chainingGroupHandler) WithGroup(name string) slog.Handler { + return ChainGroup(h, name) +} |
