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{}