-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelpers.go
More file actions
197 lines (179 loc) · 6.47 KB
/
helpers.go
File metadata and controls
197 lines (179 loc) · 6.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package slogdedup
import (
"fmt"
"log/slog"
"strings"
"modernc.org/b/v2"
)
// IncrementIfBuiltinKeyConflict is a ResolveKey function that will, if there is
// a conflict/duplication at the root level (not in a group) with one of the
// built-in keys, add "#01" to the end of the key.
func IncrementIfBuiltinKeyConflict(groups []string, key string, index int) (string, bool) {
if len(groups) == 0 && doesBuiltinKeyConflict(key) {
return incrementKeyName(key, index+1), true // Don't overwrite the built-in attribute keys
}
return incrementKeyName(key, index), true
}
// DropIfBuiltinKeyConflict is a ResolveKey function that will, if there is a
// conflict/duplication at the root level (not in a group) with one of the
// built-in keys, drop the whole attribute
func DropIfBuiltinKeyConflict(groups []string, key string, index int) (string, bool) {
if len(groups) == 0 && doesBuiltinKeyConflict(key) {
return "", false // Drop the attribute
}
return incrementKeyName(key, index), true
}
// KeepIfBuiltinKeyConflict is a ResolveKey function that will keep all keys
// even if there would be a conflict/duplication at the root level (not in a
// group) with one of the built-in keys
func KeepIfBuiltinKeyConflict(_ []string, key string, index int) (string, bool) {
return incrementKeyName(key, index), true // Keep all
}
// doesBuiltinKeyConflict returns true if the key conflicts with the builtin keys.
// This will only be called on all root level (not in a group) attribute keys.
func doesBuiltinKeyConflict(key string) bool {
if key == slog.TimeKey || key == slog.LevelKey || key == slog.MessageKey || key == slog.SourceKey {
return true
}
return false
}
// incrementKeyName adds a count onto the key name after the first seen.
// Example: keyname, keyname#01, keyname#02, keyname#03
func incrementKeyName(key string, index int) string {
if index == 0 {
return key
}
return fmt.Sprintf("%s#%02d", key, index)
}
// CaseSensitiveCmp is a case-sensitive comparison and ordering function that orders by byte values
func CaseSensitiveCmp(a, b string) int {
if a == b {
return 0
}
if a > b {
return 1
}
return -1
}
// CaseInsensitiveCmp is a case-insensitive comparison and ordering function that orders by byte values
func CaseInsensitiveCmp(a, b string) int {
a = strings.ToLower(a)
b = strings.ToLower(b)
if a == b {
return 0
}
if a > b {
return 1
}
return -1
}
// appended is a type that exists to allow us to differentiate between a log attribute that is a slice or any's ([]any),
// versus when we are appending to the key so that it becomes a slice. Only used with the AppendHandler.
type appended []any
// buildAttrs converts the deduplicated map back into an attribute array,
// with any subtrees converted into slog.Group's
func buildAttrs(uniq *b.Tree[string, any]) []slog.Attr {
en, emptyErr := uniq.SeekFirst()
if emptyErr != nil {
return nil // Empty (btree only returns an error when empty)
}
defer en.Close()
// Iterate through all values in the map, add to slice
attrs := make([]slog.Attr, 0, uniq.Len())
for k, i, err := en.Next(); err == nil; k, i, err = en.Next() {
// Values will either be an attribute, a subtree, or a specially appended slice of the former two
switch v := i.(type) {
case slog.Attr:
attrs = append(attrs, v)
case *b.Tree[string, any]:
// Convert subtree into a group
attrs = append(attrs, slog.Attr{Key: k, Value: slog.GroupValue(buildAttrs(v)...)})
case appended:
// This case only happens in the AppendHandler
anys := make([]any, 0, len(v))
for _, sliceVal := range v {
switch sliceV := sliceVal.(type) {
case slog.Attr:
anys = append(anys, sliceV.Value.Any())
case *b.Tree[string, any]:
// Convert subtree into a map (because having a Group Attribute within a slice doesn't render)
anys = append(anys, buildGroupMap(buildAttrs(sliceV)))
default:
panic("unexpected type in attribute map")
}
}
attrs = append(attrs, slog.Any(k, anys))
default:
panic("unexpected type in attribute map")
}
}
return attrs
}
// buildGroupMap takes a slice of attributes (the attributes within a group), and turns them into a map of string keys
// to a non-attribute resolved value (any).
// This function exists solely to deal with groups that are inside appended-slices (for the AppendHandler),
// because slog does not have a "slice" kind, which means that those groups and their values do not render at all.
func buildGroupMap(attrs []slog.Attr) map[string]any {
group := map[string]any{}
for _, attr := range attrs {
if attr.Value.Kind() != slog.KindGroup {
group[attr.Key] = attr.Value.Any()
} else {
group[attr.Key] = buildGroupMap(attr.Value.Group())
}
}
return group
}
// groupOrAttrs holds either a group name or a list of slog.Attrs.
// It also holds a reference/link to its parent groupOrAttrs, forming a linked list.
type groupOrAttrs struct {
group string // group name if non-empty
attrs []slog.Attr // attrs if non-empty
next *groupOrAttrs // parent
}
// WithGroup returns a new groupOrAttrs that includes the given group, and links to the old groupOrAttrs.
// Safe to call on a nil groupOrAttrs.
func (g *groupOrAttrs) WithGroup(name string) *groupOrAttrs {
// Empty-name groups are inlined as if they didn't exist
if name == "" {
return g
}
return &groupOrAttrs{
group: name,
next: g,
}
}
// WithAttrs returns a new groupOrAttrs that includes the given attrs, and links to the old groupOrAttrs.
// Safe to call on a nil groupOrAttrs.
func (g *groupOrAttrs) WithAttrs(attrs []slog.Attr) *groupOrAttrs {
if len(attrs) == 0 {
return g
}
return &groupOrAttrs{
attrs: attrs,
next: g,
}
}
// collectGroupOrAttrs unrolls all individual groupOrAttrs and collects them into a slice, ordered from oldest to newest
func collectGroupOrAttrs(gs ...*groupOrAttrs) []*groupOrAttrs {
// Get a total count of all groups in the group linked-list chain
n := 0
for _, g := range gs {
for ga := g; ga != nil; ga = ga.next {
n++
}
}
// The groupOrAttrs on the handler is a linked list starting from the newest to the oldest set of attributes/groups.
// Within each groupOrAttrs, all attributes are in a slice that is ordered from oldest to newest.
// To make things consistent we will reverse the order of the groupOrAttrs, so that it goes from oldest to newest,
// thereby matching the order of the attributes.
res := make([]*groupOrAttrs, n)
j := 0
for i := len(gs) - 1; i >= 0; i-- {
for ga := gs[i]; ga != nil; ga = ga.next {
res[len(res)-j-1] = ga
j++
}
}
return res
}