cannon.go

  1package log
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log/slog"
  7	"time"
  8)
  9
 10// Merger allows a user-defined type to merge its value with a previous attribute with the same key.  Normally, two attributes
 11// with the same key default to last write wins.  By implementing this function for your type, you can control how the canonical
 12// log line merges with previous values for the same key.  See the README for examples of how this function works.  Note that you
 13// do not have to implement this function for Group values.  Groups are automatically merged for the same key.
 14type Merger interface {
 15	Merge(previous slog.Value) slog.Value
 16}
 17
 18type cannon struct {
 19	parent *cannon
 20	r      []slog.Record
 21	h      slog.Handler
 22	groups []string
 23}
 24
 25// NewDefault sets up the canonical logger using the slog.Default() handler
 26func NewDefault() *slog.Logger {
 27	return slog.New(wrap(slog.Default().Handler()))
 28}
 29
 30// New sets up the canonical logger with a specified handler. You can use any handler from the log/slog package
 31// or implement your own.
 32func New(h slog.Handler) *slog.Logger {
 33	return slog.New(wrap(h))
 34}
 35
 36func wrap(h slog.Handler) slog.Handler {
 37	return &cannon{
 38		r: make([]slog.Record, 0),
 39		h: h,
 40	}
 41}
 42
 43// Emit logs the canonical log line.  This is typically called at the end of the request.  The canonical log line will
 44// coalesce all attributes called since the creation of the logger.  It will merge repeated keys using the following strategy:
 45// (1) if the keys fall under the same group, they are merged together under the group key; (2) if the type implements the Merger
 46// interface, it is called to merge values for the same key; (3) otherwise, the last write wins.
 47func Emit(log *slog.Logger, args ...any) error {
 48	h, ok := log.Handler().(*cannon)
 49	if !ok || log == nil {
 50		return fmt.Errorf("expected cannon handler, got type: %T", h)
 51	}
 52	if len(args) > 0 {
 53		rec := slog.NewRecord(time.Now(), slog.LevelInfo, "", 0)
 54		rec.Add(args...)
 55		h.r = append(h.r, rec)
 56	}
 57	return h.emit()
 58}
 59
 60// Enabled implements the slog.Handler interface
 61func (c *cannon) Enabled(ctx context.Context, lvl slog.Level) bool {
 62	return c.h.Enabled(ctx, lvl)
 63}
 64
 65// Handle implements the slog.Handler interface
 66func (c *cannon) Handle(ctx context.Context, r slog.Record) error {
 67	c.addRecord(r, c.groups...)
 68	return c.h.Handle(ctx, r)
 69}
 70
 71// addRecord combines attributes and deals with group calls
 72func (c *cannon) addRecord(r slog.Record, groups ...string) {
 73	if c.parent != nil {
 74		c.parent.addRecord(r, groups...)
 75	}
 76	if len(groups) > 0 {
 77		// builds up a new record with a group key by extracting attributes from
 78		// subsequent calls and nesting them under the group key(s)
 79		rec := slog.NewRecord(time.Now(), slog.LevelInfo, "g", 0)
 80		attrs := make([]slog.Attr, r.NumAttrs())
 81		r.Attrs(func(a slog.Attr) bool {
 82			attrs = append(attrs, a)
 83			return true
 84		})
 85		attr := slog.Any(groups[0], slog.GroupValue(attrs...))
 86		if len(groups) > 1 {
 87			// nested groups, recursively build up the attr with the
 88			// group hierarchy as the key
 89			for _, g := range groups[1:] {
 90				attr = slog.Any(g, attr)
 91			}
 92		}
 93		rec.Add(attr)
 94		c.r = append(c.r, rec)
 95	} else {
 96		// if no group, sufficient to clone record and keep it
 97		c.r = append(c.r, r.Clone())
 98	}
 99}
100
101// WithAttrs implements the slog.Handler interface.  In addition, you can add attributes to the canonical log line
102// without actually logging them at the time of the call.  For example, you could call log.WithAttrs("a", "b"). No immediate
103// logging will occur at the call site, but the attributes will appear in the canonical log line.
104func (c *cannon) WithAttrs(attrs []slog.Attr) slog.Handler {
105	// creates a record with the attributes at handler creation time
106	// even if this handler is never used to log, the attrs will remain in the canonical line
107	// if used to log, attrs will be deduplicated
108	rec := slog.NewRecord(time.Now(), slog.LevelInfo, "", 0)
109	rec.AddAttrs(attrs...)
110	c.addRecord(rec)
111	return &cannon{
112		c,
113		nil,
114		c.h.WithAttrs(attrs),
115		c.groups,
116	}
117}
118
119// WithGroup implements the slog.Handler interface
120func (c *cannon) WithGroup(name string) slog.Handler {
121	return &cannon{
122		c,
123		nil,
124		c.h.WithGroup(name),
125		append(c.groups, name),
126	}
127}
128
129// emit implements the merge strategy for repeated attribute keys and writes the log line using the defined handler
130func (c *cannon) emit() error {
131	if c.parent == nil && len(c.r) == 0 && len(c.groups) == 0 {
132		//empty record, nothing to emit
133		return nil
134	}
135	if c.parent != nil {
136		return c.parent.emit()
137	}
138	rec := slog.NewRecord(time.Now(), slog.LevelInfo, "canonical_log_line", 0)
139
140	// create set of all attrs by key, attempting to merge same keyed values. If merge not possible, LWW.
141	merge := make(map[string]slog.Attr)
142	for _, r := range c.r {
143		r.Attrs(func(a slog.Attr) bool {
144			_, exists := merge[a.Key]
145			if exists {
146				switch a.Value.Kind() {
147				case slog.KindGroup:
148					// merge groups with LWW within the group
149					attrs := append(merge[a.Key].Value.Group(), a.Value.Group()...)
150					merge[a.Key] = slog.Attr{Key: a.Key, Value: slog.GroupValue(attrs...)}
151				default:
152					// for user-provided merge function, use that or default to LWW
153					value := a.Value.Any()
154					mergeable, ok := value.(Merger)
155					if ok {
156						merge[a.Key] = slog.Attr{Key: a.Key, Value: mergeable.Merge(merge[a.Key].Value)}
157					} else {
158						merge[a.Key] = a
159					}
160
161				}
162			} else {
163				// no need to merge
164				merge[a.Key] = a
165			}
166			return true
167		})
168	}
169	attrs := make([]slog.Attr, len(merge))
170	for _, a := range merge {
171		attrs = append(attrs, a)
172	}
173	rec.AddAttrs(attrs...)
174	return c.h.Handle(context.Background(), rec)
175}
176
177var _ slog.Handler = &cannon{}