aboutsummaryrefslogtreecommitdiffstats
path: root/logging/chain.go
diff options
context:
space:
mode:
Diffstat (limited to 'logging/chain.go')
-rw-r--r--logging/chain.go152
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)
+}