Skip to content

Commit d43d523

Browse files
committed
slog: Attrs
Define Attr, an alloc-efficient key-value pair. Attr is like any, but does not allocate for most basic types. It can be used for low-allocation logging. The unsafe version, which is the default, exploits the representation of Go strings to save space. The ideas here are largely taken from Ian Cottrell's Label implementation, https://cs.opensource.google/go/x/exp/+/334a2380:event/label.go. Change-Id: I8283fefc208ab6d306d5cc32bc0a0ccc295fab57 Reviewed-on: https://go-review.googlesource.com/c/exp/+/426021 Run-TryBot: Jonathan Amsterdam <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 5c2e9b4 commit d43d523

File tree

4 files changed

+813
-0
lines changed

4 files changed

+813
-0
lines changed

slog/attr.go

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slog
6+
7+
import (
8+
"fmt"
9+
"math"
10+
"strconv"
11+
"time"
12+
)
13+
14+
// Kind is the kind of an Attr's value.
15+
type Kind int
16+
17+
// The following list is sorted alphabetically, but it's also important that
18+
// AnyKind is 0 so that a zero Attr's value is nil.
19+
20+
const (
21+
AnyKind Kind = iota
22+
BoolKind
23+
DurationKind
24+
Float64Kind
25+
Int64Kind
26+
StringKind
27+
TimeKind
28+
Uint64Kind
29+
)
30+
31+
var kindStrings = []string{
32+
"Any",
33+
"Bool",
34+
"Duration",
35+
"Float64",
36+
"Int64",
37+
"String",
38+
"Time",
39+
"Uint64",
40+
}
41+
42+
func (k Kind) String() string {
43+
if k >= 0 && int(k) < len(kindStrings) {
44+
return kindStrings[k]
45+
}
46+
return "<unknown slog.Kind>"
47+
}
48+
49+
//////////////// Constructors
50+
51+
// Int64 returns an Attr for an int64.
52+
func Int64(key string, value int64) Attr {
53+
return Attr{key: key, num: uint64(value), any: Int64Kind}
54+
}
55+
56+
// Int converts an int to an int64 and returns
57+
// an Attr with that value.
58+
func Int(key string, value int) Attr {
59+
return Int64(key, int64(value))
60+
}
61+
62+
// Uint64 returns an Attr for a uint64.
63+
func Uint64(key string, value uint64) Attr {
64+
return Attr{key: key, num: value, any: Uint64Kind}
65+
}
66+
67+
// Float64 returns an Attr for a floating-point number.
68+
func Float64(key string, value float64) Attr {
69+
return Attr{key: key, num: math.Float64bits(value), any: Float64Kind}
70+
}
71+
72+
// Bool returns an Attr for a bool.
73+
func Bool(key string, value bool) Attr {
74+
u := uint64(0)
75+
if value {
76+
u = 1
77+
}
78+
return Attr{key: key, num: u, any: BoolKind}
79+
}
80+
81+
// Time returns an Attr for a time.Time.
82+
// It discards the monotonic portion.
83+
func Time(key string, value time.Time) Attr {
84+
return Attr{key: key, num: uint64(value.UnixNano()), any: value.Location()}
85+
}
86+
87+
// Duration returns an Attr for a time.Duration.
88+
func Duration(key string, value time.Duration) Attr {
89+
return Attr{key: key, num: uint64(value.Nanoseconds()), any: DurationKind}
90+
}
91+
92+
// Any returns an Attr for the supplied value.
93+
//
94+
// Given a value of one of Go's predeclared string, bool, or
95+
// (non-complex) numeric types, Any returns an Attr of kind
96+
// String, Bool, Uint64, Int64, or Float64. The width of the
97+
// original numeric type is not preserved.
98+
//
99+
// Given a time.Time or time.Duration value, Any returns an Attr of kind
100+
// TimeKind or DurationKind. The monotonic time is not preserved.
101+
//
102+
// For nil, or values of all other types, including named types whose
103+
// underlying type is numeric, Any returns a value of kind AnyKind.
104+
func Any(key string, value any) Attr {
105+
switch v := value.(type) {
106+
case string:
107+
return String(key, v)
108+
case int:
109+
return Int(key, v)
110+
case int64:
111+
return Int64(key, v)
112+
case uint64:
113+
return Uint64(key, v)
114+
case bool:
115+
return Bool(key, v)
116+
case time.Duration:
117+
return Duration(key, v)
118+
case time.Time:
119+
return Time(key, v)
120+
case uint8:
121+
return Uint64(key, uint64(v))
122+
case uint16:
123+
return Uint64(key, uint64(v))
124+
case uint32:
125+
return Uint64(key, uint64(v))
126+
case uintptr:
127+
return Uint64(key, uint64(v))
128+
case int8:
129+
return Int64(key, int64(v))
130+
case int16:
131+
return Int64(key, int64(v))
132+
case int32:
133+
return Int64(key, int64(v))
134+
case float64:
135+
return Float64(key, v)
136+
case float32:
137+
return Float64(key, float64(v))
138+
case Kind:
139+
panic("cannot store a slog.Kind in an Attr")
140+
case *time.Location:
141+
panic("cannot store a *time.Location in an Attr")
142+
default:
143+
return Attr{key: key, any: v}
144+
}
145+
}
146+
147+
//////////////// Accessors
148+
149+
// Key returns the Attr's key.
150+
func (a Attr) Key() string { return a.key }
151+
152+
// Value returns the Attr's value as an any.
153+
// If the Attr does not have a value, it returns nil.
154+
func (a Attr) Value() any {
155+
switch a.Kind() {
156+
case AnyKind:
157+
return a.any
158+
case Int64Kind:
159+
return int64(a.num)
160+
case Uint64Kind:
161+
return a.num
162+
case Float64Kind:
163+
return a.float()
164+
case StringKind:
165+
return a.str()
166+
case BoolKind:
167+
return a.bool()
168+
case DurationKind:
169+
return a.duration()
170+
case TimeKind:
171+
return a.time()
172+
default:
173+
panic("bad kind")
174+
}
175+
}
176+
177+
// HasValue reports whether the Attr has a non-nil value.
178+
func (a Attr) HasValue() bool { return a.any != nil }
179+
180+
// Int64 returns the Attr's value as an int64. It panics
181+
// if the value is not a signed integer.
182+
func (a Attr) Int64() int64 {
183+
if g, w := a.Kind(), Int64Kind; g != w {
184+
panic(fmt.Sprintf("Attr kind is %s, not %s", g, w))
185+
}
186+
return int64(a.num)
187+
}
188+
189+
// Uint64 returns the Attr's value as a uint64. It panics
190+
// if the value is not an unsigned integer.
191+
func (a Attr) Uint64() uint64 {
192+
if g, w := a.Kind(), Uint64Kind; g != w {
193+
panic(fmt.Sprintf("Attr kind is %s, not %s", g, w))
194+
}
195+
return a.num
196+
}
197+
198+
// Bool returns the Attr's value as a bool. It panics
199+
// if the value is not a bool.
200+
func (a Attr) Bool() bool {
201+
if g, w := a.Kind(), BoolKind; g != w {
202+
panic(fmt.Sprintf("Attr kind is %s, not %s", g, w))
203+
}
204+
return a.bool()
205+
}
206+
207+
func (a Attr) bool() bool {
208+
return a.num == 1
209+
}
210+
211+
// Duration returns the Attr's value as a time.Duration. It panics
212+
// if the value is not a time.Duration.
213+
func (a Attr) Duration() time.Duration {
214+
if g, w := a.Kind(), DurationKind; g != w {
215+
panic(fmt.Sprintf("Attr kind is %s, not %s", g, w))
216+
}
217+
218+
return a.duration()
219+
}
220+
221+
func (a Attr) duration() time.Duration {
222+
return time.Duration(int64(a.num))
223+
}
224+
225+
// Float64 returns the Attr's value as a float64. It panics
226+
// if the value is not a float64.
227+
func (a Attr) Float64() float64 {
228+
if g, w := a.Kind(), Float64Kind; g != w {
229+
panic(fmt.Sprintf("Attr kind is %s, not %s", g, w))
230+
}
231+
232+
return a.float()
233+
}
234+
235+
func (a Attr) float() float64 {
236+
return math.Float64frombits(a.num)
237+
}
238+
239+
// Time returns the Attr's value as a time.Time. It panics
240+
// if the value is not a time.Time.
241+
func (a Attr) Time() time.Time {
242+
if g, w := a.Kind(), TimeKind; g != w {
243+
panic(fmt.Sprintf("Attr kind is %s, not %s", g, w))
244+
}
245+
return a.time()
246+
}
247+
248+
func (a Attr) time() time.Time {
249+
return time.Unix(0, int64(a.num)).In(a.any.(*time.Location))
250+
}
251+
252+
//////////////// Other
253+
254+
// WithKey returns an attr with the given key and the receiver's value.
255+
func (a Attr) WithKey(key string) Attr {
256+
a.key = key
257+
return a
258+
}
259+
260+
// Equal reports whether two Attrs have equal keys and values.
261+
func (a1 Attr) Equal(a2 Attr) bool {
262+
if a1.key != a2.key {
263+
return false
264+
}
265+
k1 := a1.Kind()
266+
k2 := a2.Kind()
267+
if k1 != k2 {
268+
return false
269+
}
270+
switch k1 {
271+
case Int64Kind, Uint64Kind, BoolKind, DurationKind:
272+
return a1.num == a2.num
273+
case StringKind:
274+
return a1.str() == a2.str()
275+
case Float64Kind:
276+
return a1.float() == a2.float()
277+
case TimeKind:
278+
return a1.time().Equal(a2.time())
279+
case AnyKind:
280+
return a1.any == a2.any // may panic if non-comparable
281+
default:
282+
panic(fmt.Sprintf("bad kind: %s", k1))
283+
}
284+
}
285+
286+
// AppendValue appends a text representation of the Attr's value to dst.
287+
// The value is formatted as with fmt.Sprint.
288+
func (a Attr) AppendValue(dst []byte) []byte {
289+
switch a.Kind() {
290+
case StringKind:
291+
return append(dst, a.str()...)
292+
case Int64Kind:
293+
return strconv.AppendInt(dst, int64(a.num), 10)
294+
case Uint64Kind:
295+
return strconv.AppendUint(dst, a.num, 10)
296+
case Float64Kind:
297+
return strconv.AppendFloat(dst, a.float(), 'g', -1, 64)
298+
case BoolKind:
299+
return strconv.AppendBool(dst, a.bool())
300+
case DurationKind:
301+
return append(dst, a.duration().String()...)
302+
case TimeKind:
303+
return append(dst, a.time().String()...)
304+
case AnyKind:
305+
return append(dst, fmt.Sprint(a.any)...)
306+
default:
307+
panic(fmt.Sprintf("bad kind: %s", a.Kind()))
308+
}
309+
}
310+
311+
// Format implements fmt.Formatter.
312+
// It formats an Attr as "KEY=VALUE".
313+
func (a Attr) Format(s fmt.State, verb rune) {
314+
// TODO: consider verbs and flags
315+
fmt.Fprintf(s, "%s=%v", a.Key(), a.Value())
316+
}

slog/attr_safe.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build safe_attrs
6+
7+
package slog
8+
9+
import "time"
10+
11+
// This file defines the most portable representation of Attr.
12+
13+
// An Attr is a key-value pair.
14+
// It can represent most small values without an allocation.
15+
// The zero Attr has a key of "" and a value of nil.
16+
type Attr struct {
17+
key string
18+
// num holds the value for Kinds Int64, Uint64, Float64, Bool and Duration,
19+
// and nanoseconds since the epoch for TimeKind.
20+
num uint64
21+
// s holds the value for StringKind.
22+
s string
23+
// If any is of type Kind, then the value is in num or s as described above.
24+
// If any is of type *time.Location, then the Kind is Time and time.Time
25+
// value can be constructed from the Unix nanos in num and the location
26+
// (monotonic time is not preserved).
27+
// Otherwise, the Kind is Any and any is the value.
28+
// (This implies that Attrs cannot store Kinds or *time.Locations.)
29+
any any
30+
}
31+
32+
// Kind returns the Attr's Kind.
33+
func (a Attr) Kind() Kind {
34+
switch k := a.any.(type) {
35+
case Kind:
36+
return k
37+
case *time.Location:
38+
return TimeKind
39+
default:
40+
return AnyKind
41+
}
42+
}
43+
44+
func (a Attr) str() string {
45+
return a.s
46+
}
47+
48+
// String returns a new Attr for a string.
49+
func String(key, value string) Attr {
50+
return Attr{key: key, s: value, any: StringKind}
51+
}
52+
53+
// String returns Attr's value as a string, formatted like fmt.Sprint. Unlike
54+
// the methods Int64, Float64, and so on, which panic if the Attr is of the
55+
// wrong kind, String never panics.
56+
func (a Attr) String() string {
57+
if a.Kind() == StringKind {
58+
return a.str()
59+
}
60+
var buf []byte
61+
return string(a.AppendValue(buf))
62+
}

0 commit comments

Comments
 (0)