Skip to content

Commit 46d9e77

Browse files
committed
slog: redesign Record
- Return to the implementation consisting of an inline array followed by a slice. - Document the alias problems that can arise. - Provide Record.Clone to make a deep copy so aliasing bugs can be avoided. - Replace AddAttr with AddAttrs, to permit more efficiently adding Attrs, and to provide more implementation flexibility. - Eliminate random access: support only forward iteration. Accomplish it with a method that takes a func(Attr). Call this method "Attrs"; the old "Attrs", which returned a []Attr, can now be written in terms of the new one. We do not provide a way to stop iteration early, because the large majority of uses will want to visit all the Attrs. - Call the constructor NewRecord, not MakeRecord, because Record isn't truly a value type. Change-Id: Iac6843cd8a27e351fc4bde79aa59c6cc2e526e46 Reviewed-on: https://go-review.googlesource.com/c/exp/+/431276 Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent b168a2c commit 46d9e77

File tree

8 files changed

+159
-132
lines changed

8 files changed

+159
-132
lines changed

slog/handler.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ func (h *defaultHandler) Handle(r Record) error {
5757
b.WriteString(r.Level().String())
5858
b.WriteByte(' ')
5959
}
60-
for i := 0; i < r.NumAttrs(); i++ {
61-
fmt.Fprint(&b, r.Attr(i)) // Attr.Format will print key=value
60+
r.Attrs(func(a Attr) {
61+
fmt.Fprint(&b, a) // Attr.Format will print key=value
6262
b.WriteByte(' ')
63-
}
63+
})
6464
b.WriteString(r.Message())
6565
return log.Output(4, b.String())
6666
}
@@ -205,13 +205,12 @@ func (h *commonHandler) handle(r Record) error {
205205
replace(String(key, val))
206206
}
207207
*buf = append(*buf, h.preformattedAttrs...)
208-
for i := 0; i < r.NumAttrs(); i++ {
209-
a := r.Attr(i)
208+
r.Attrs(func(a Attr) {
210209
if rep != nil {
211210
a = rep(a)
212211
}
213212
appendAttr(app, a)
214-
}
213+
})
215214
app.appendEnd()
216215
buf.WriteByte('\n')
217216

slog/handler_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ func TestWith(t *testing.T) {
3636

3737
func TestCommonHandle(t *testing.T) {
3838
tm := time.Now()
39-
r := MakeRecord(tm, InfoLevel, "message", 1)
40-
r.AddAttr(String("a", "one"))
41-
r.AddAttr(Int("b", 2))
42-
r.AddAttr(Any("", "ignore me"))
39+
r := NewRecord(tm, InfoLevel, "message", 1)
40+
r.AddAttrs(String("a", "one"), Int("b", 2), Any("", "ignore me"))
4341

4442
for _, test := range []struct {
4543
name string

slog/json_handler_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import (
2121
func TestJSONHandler(t *testing.T) {
2222
var buf bytes.Buffer
2323
h := NewJSONHandler(&buf)
24-
r := MakeRecord(testTime, InfoLevel, "m", 0)
25-
r.AddAttr(Int("a", 1))
24+
r := NewRecord(testTime, InfoLevel, "m", 0)
25+
r.AddAttrs(Int("a", 1))
2626
if err := h.Handle(r); err != nil {
2727
t.Fatal(err)
2828
}

slog/logger.go

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (w *handlerWriter) Write(buf []byte) (int, error) {
4747
if len(buf) > 0 && buf[len(buf)-1] == '\n' {
4848
buf = buf[:len(buf)-1]
4949
}
50-
r := MakeRecord(time.Now(), InfoLevel, string(buf), depth)
50+
r := NewRecord(time.Now(), InfoLevel, string(buf), depth)
5151
return origLen, w.h.Handle(r)
5252
}
5353

@@ -113,7 +113,7 @@ func (l *Logger) LogDepth(calldepth int, level Level, msg string, args ...any) {
113113
return
114114
}
115115
r := l.makeRecord(msg, level, calldepth)
116-
setAttrs(&r, args)
116+
r.setAttrsFromArgs(args)
117117
_ = l.Handler().Handle(r)
118118
}
119119

@@ -127,39 +127,7 @@ func (l *Logger) makeRecord(msg string, level Level, depth int) Record {
127127
if useSourceLine {
128128
depth += 5
129129
}
130-
return MakeRecord(time.Now(), level, msg, depth)
131-
}
132-
133-
const badKey = "!BADKEY"
134-
135-
func setAttrs(r *Record, args []any) {
136-
var attr Attr
137-
for len(args) > 0 {
138-
attr, args = argsToAttr(args)
139-
r.AddAttr(attr)
140-
}
141-
}
142-
143-
// argsToAttr turns a prefix of the args slice into an Attr and returns
144-
// the unused portion of the slice.
145-
// If args[0] is an Attr, it returns it.
146-
// If args[0] is a string, it treats the first two elements as
147-
// a key-value pair.
148-
// Otherwise, it treats args[0] as a value with a missing key.
149-
func argsToAttr(args []any) (Attr, []any) {
150-
switch x := args[0].(type) {
151-
case string:
152-
if len(args) == 1 {
153-
return String(badKey, x), nil
154-
}
155-
return Any(x, args[1]), args[2:]
156-
157-
case Attr:
158-
return x, args[1:]
159-
160-
default:
161-
return Any(badKey, x), args[1:]
162-
}
130+
return NewRecord(time.Now(), level, msg, depth)
163131
}
164132

165133
// LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs.
@@ -174,7 +142,7 @@ func (l *Logger) LogAttrsDepth(calldepth int, level Level, msg string, attrs ...
174142
return
175143
}
176144
r := l.makeRecord(msg, level, calldepth)
177-
r.addAttrs(attrs)
145+
r.AddAttrs(attrs...)
178146
_ = l.Handler().Handle(r)
179147
}
180148

slog/logger_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func TestAttrs(t *testing.T) {
9191
l2.Info("m", "c", 3)
9292
h := l2.Handler().(*captureHandler)
9393
check(h.attrs, Int("a", 1), Int("b", 2))
94-
check(h.r.Attrs(), Int("c", 3))
94+
check(attrsSlice(h.r), Int("c", 3))
9595
}
9696

9797
func TestCallDepth(t *testing.T) {
@@ -199,7 +199,7 @@ func TestAlloc(t *testing.T) {
199199
s := "abc"
200200
i := 2000
201201
d := time.Second
202-
wantAllocs(t, 10, func() {
202+
wantAllocs(t, 11, func() {
203203
dl.Info("hello",
204204
"n", i, "s", s, "d", d,
205205
"n", i, "s", s, "d", d,
@@ -253,9 +253,9 @@ func TestSetAttrs(t *testing.T) {
253253
{[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
254254
{[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
255255
} {
256-
r := MakeRecord(time.Time{}, 0, "", 0)
257-
setAttrs(&r, test.args)
258-
got := r.Attrs()
256+
r := NewRecord(time.Time{}, 0, "", 0)
257+
r.setAttrsFromArgs(test.args)
258+
got := attrsSlice(r)
259259
if !attrsEqual(got, test.want) {
260260
t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want)
261261
}

slog/record.go

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
const nAttrsInline = 5
1313

1414
// A Record holds information about a log event.
15+
// Copies of a Record share state.
16+
// Do not modify a Record after handing out a copy to it.
17+
// Use [Record.Clone] to create a copy with no shared state.
1518
type Record struct {
1619
// The time at which the output method (Log, Info, etc.) was called.
1720
time time.Time
@@ -28,26 +31,28 @@ type Record struct {
2831

2932
// Allocation optimization: an inline array sized to hold
3033
// the majority of log calls (based on examination of open-source
31-
// code). The array holds the end of the sequence of Attrs.
32-
tail [nAttrsInline]Attr
34+
// code). It holds the start of the list of Attrs.
35+
front [nAttrsInline]Attr
3336

34-
// The number of Attrs in tail.
35-
nTail int
37+
// The number of Attrs in front.
38+
nFront int
3639

37-
// The sequence of Attrs except for the tail, represented as a functional
38-
// list of arrays.
39-
attrs list[[nAttrsInline]Attr]
40+
// The list of Attrs except for those in front.
41+
// Invariants:
42+
// - len(back) > 0 iff nFront == len(front)
43+
// - Unused array elements are zero. Used to detect mistakes.
44+
back []Attr
4045
}
4146

42-
// MakeRecord creates a new Record from the given arguments.
43-
// Use [Record.AddAttr] to add attributes to the Record.
47+
// NewRecord creates a Record from the given arguments.
48+
// Use [Record.AddAttrs] to add attributes to the Record.
4449
// If calldepth is greater than zero, [Record.SourceLine] will
4550
// return the file and line number at that depth,
46-
// where 1 means the caller of MakeRecord.
51+
// where 1 means the caller of NewRecord.
4752
//
48-
// MakeRecord is intended for logging APIs that want to support a [Handler] as
53+
// NewRecord is intended for logging APIs that want to support a [Handler] as
4954
// a backend.
50-
func MakeRecord(t time.Time, level Level, msg string, calldepth int) Record {
55+
func NewRecord(t time.Time, level Level, msg string, calldepth int) Record {
5156
var p uintptr
5257
if calldepth > 0 {
5358
p = pc(calldepth + 1)
@@ -85,50 +90,97 @@ func (r *Record) SourceLine() (file string, line int) {
8590
return f.File, f.Line
8691
}
8792

88-
// Attrs returns a copy of the sequence of Attrs in r.
89-
func (r *Record) Attrs() []Attr {
90-
res := make([]Attr, 0, r.attrs.len()*nAttrsInline+r.nTail)
91-
r.attrs = r.attrs.normalize()
92-
for _, f := range r.attrs.front {
93-
res = append(res, f[:]...)
93+
// Clone returns a copy of the record with no shared state.
94+
// The original record and the clone can both be modified
95+
// without interfering with each other.
96+
func (r *Record) Clone() Record {
97+
c := *r
98+
if len(c.back) > 0 {
99+
c.back = make([]Attr, len(c.back))
100+
copy(c.back, r.back)
94101
}
95-
for _, a := range r.tail[:r.nTail] {
96-
res = append(res, a)
97-
}
98-
return res
102+
return c
99103
}
100104

101-
// NumAttrs returns the number of Attrs in r.
105+
// NumAttrs returns the number of attributes in the Record.
102106
func (r *Record) NumAttrs() int {
103-
return r.attrs.len()*nAttrsInline + r.nTail
107+
return r.nFront + len(r.back)
108+
}
109+
110+
// Attrs calls f on each Attr in the Record.
111+
func (r *Record) Attrs(f func(Attr)) {
112+
for i := 0; i < r.nFront; i++ {
113+
f(r.front[i])
114+
}
115+
for _, a := range r.back {
116+
f(a)
117+
}
104118
}
105119

106-
// Attr returns the i'th Attr in r.
107-
func (r *Record) Attr(i int) Attr {
108-
if r.attrs.back != nil {
109-
r.attrs = r.attrs.normalize()
120+
// AddAttrs appends the given attrs to the Record's list of Attrs.
121+
func (r *Record) AddAttrs(attrs ...Attr) {
122+
n := copy(r.front[r.nFront:], attrs)
123+
r.nFront += n
124+
// Check if a copy was modified by slicing past the end
125+
// and seeing if the Attr there is non-zero.
126+
if cap(r.back) > len(r.back) {
127+
end := r.back[:len(r.back)+1][len(r.back)]
128+
if end != (Attr{}) {
129+
panic("copies of a slog.Record were both modified")
130+
}
110131
}
111-
alen := r.attrs.len() * nAttrsInline
112-
if i < alen {
113-
return r.attrs.at(i / nAttrsInline)[i%nAttrsInline]
132+
r.back = append(r.back, attrs[n:]...)
133+
}
134+
135+
func (r *Record) setAttrsFromArgs(args []any) {
136+
var a Attr
137+
for len(args) > 0 {
138+
a, args = argsToAttr(args)
139+
if r.nFront < len(r.front) {
140+
r.front[r.nFront] = a
141+
r.nFront++
142+
} else {
143+
if r.back == nil {
144+
r.back = make([]Attr, 0, countAttrs(args))
145+
}
146+
r.back = append(r.back, a)
147+
}
114148
}
115-
return r.tail[i-alen]
149+
116150
}
117151

118-
// AddAttr appends an attributes to the record's list of attributes.
119-
// It does not check for duplicate keys.
120-
func (r *Record) AddAttr(a Attr) {
121-
if r.nTail == len(r.tail) {
122-
r.attrs = r.attrs.append(r.tail)
123-
r.nTail = 0
152+
// countAttrs returns the number of Attrs that would be created from args.
153+
func countAttrs(args []any) int {
154+
n := 0
155+
for i := 0; i < len(args); i++ {
156+
n++
157+
if _, ok := args[i].(string); ok {
158+
i++
159+
}
124160
}
125-
r.tail[r.nTail] = a
126-
r.nTail++
161+
return n
127162
}
128163

129-
func (r *Record) addAttrs(attrs []Attr) {
130-
// TODO: be cleverer.
131-
for _, a := range attrs {
132-
r.AddAttr(a)
164+
const badKey = "!BADKEY"
165+
166+
// argsToAttr turns a prefix of the nonempty args slice into an Attr
167+
// and returns the unconsumed portion of the slice.
168+
// If args[0] is an Attr, it returns it.
169+
// If args[0] is a string, it treats the first two elements as
170+
// a key-value pair.
171+
// Otherwise, it treats args[0] as a value with a missing key.
172+
func argsToAttr(args []any) (Attr, []any) {
173+
switch x := args[0].(type) {
174+
case string:
175+
if len(args) == 1 {
176+
return String(badKey, x), nil
177+
}
178+
return Any(x, args[1]), args[2:]
179+
180+
case Attr:
181+
return x, args[1:]
182+
183+
default:
184+
return Any(badKey, x), args[1:]
133185
}
134186
}

0 commit comments

Comments
 (0)