slog: add Group Values
Introduce a kind of Value that is a tagged group of other Values,
represented by a []Attr.
Now a type can provide a LogValue method that returns multiple Attrs,
so it can represent a loggable version of itself to slog.Handlers in
a general way.
This CL just adds the implementation to Value.
Subsequent CLs will provide Handler implementations and tests.
Change-Id: Ib0204daf146f545bd20abe4e58664899c8ef45db
Reviewed-on: https://go-review.googlesource.com/c/exp/+/437959
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/attr.go b/slog/attr.go
index 32d862d..90541ee 100644
--- a/slog/attr.go
+++ b/slog/attr.go
@@ -57,6 +57,17 @@
return Attr{key, DurationValue(v)}
}
+// Group returns an Attr for a Group Value.
+// The caller must not subsequently mutate the
+// argument slice.
+//
+// Use Group to collect several Attrs under a single
+// key on a log line, or as the result of LogValue
+// in order to log a single value as multiple Attrs.
+func Group(key string, as ...Attr) Attr {
+ return Attr{key, GroupValue(as...)}
+}
+
// Any returns an Attr for the supplied value.
// See [Value.AnyValue] for how values are treated.
func Any(key string, value any) Attr {
diff --git a/slog/example_marshaler_test.go b/slog/example_marshaler_test.go
new file mode 100644
index 0000000..1b07f82
--- /dev/null
+++ b/slog/example_marshaler_test.go
@@ -0,0 +1,34 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package slog_test
+
+import (
+ "golang.org/x/exp/slog"
+)
+
+type Name struct {
+ First, Last string
+}
+
+func (n Name) MarshalLog() slog.Value {
+ return slog.GroupValue(
+ slog.String("first", n.First),
+ slog.String("last", n.Last))
+}
+
+func ExampleMarshaler() {
+ n := Name{"Perry", "Platypus"}
+ slog.Info("mission accomplished", "agent", n)
+
+ // JSON Output would look in part like:
+ // {
+ // ...
+ // "msg": "mission accomplished",
+ // "agent": {
+ // "first": "Perry",
+ // "last": "Platypus"
+ // }
+ // }
+}
diff --git a/slog/example_test.go b/slog/example_test.go
new file mode 100644
index 0000000..5681ab2
--- /dev/null
+++ b/slog/example_test.go
@@ -0,0 +1,25 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package slog_test
+
+import (
+ "net/http"
+ "time"
+
+ "golang.org/x/exp/slog"
+)
+
+func ExampleGroup() {
+ var r *http.Request
+ start := time.Now()
+ // ...
+
+ slog.Info("finished",
+ slog.Group("req",
+ slog.String("method", r.Method),
+ slog.String("url", r.URL.String())),
+ slog.Int("status", http.StatusOK),
+ slog.Duration("duration", time.Since(start)))
+}
diff --git a/slog/value.go b/slog/value.go
index 120e7cf..bf6d930 100644
--- a/slog/value.go
+++ b/slog/value.go
@@ -9,6 +9,8 @@
"math"
"strconv"
"time"
+
+ "golang.org/x/exp/slices"
)
// Definitions for Value.
@@ -29,6 +31,7 @@
StringKind
TimeKind
Uint64Kind
+ GroupKind
LogValuerKind
)
@@ -41,6 +44,7 @@
"String",
"Time",
"Uint64",
+ "GroupKind",
"LogValuer",
}
@@ -93,6 +97,12 @@
return Value{num: uint64(v.Nanoseconds()), any: DurationKind}
}
+// GroupValue returns a new Value for a list of Attrs.
+// The caller must not subsequently mutate the argument slice.
+func GroupValue(as ...Attr) Value {
+ return groupValue(as)
+}
+
// AnyValue returns a Value for the supplied value.
//
// Given a value of one of Go's predeclared string, bool, or
@@ -139,6 +149,8 @@
return Float64Value(v)
case float32:
return Float64Value(float64(v))
+ case []Attr:
+ return GroupValue(v...)
case Kind:
panic("cannot store a slog.Kind in a slog.Value")
case *time.Location:
@@ -153,7 +165,7 @@
// Any returns the Value's value as an any.
func (v Value) Any() any {
switch v.Kind() {
- case AnyKind, LogValuerKind:
+ case AnyKind, GroupKind, LogValuerKind:
return v.any
case Int64Kind:
return int64(v.num)
@@ -252,6 +264,12 @@
return v.any.(LogValuer)
}
+// Group returns the Value's value as a []Attr.
+// It panics if the Value's Kind is not GroupKind.
+func (v Value) Group() []Attr {
+ return v.group()
+}
+
//////////////// Other
// Equal reports whether two Values have equal keys and values.
@@ -272,6 +290,8 @@
return v1.time().Equal(v2.time())
case AnyKind, LogValuerKind:
return v1.any == v2.any // may panic if non-comparable
+ case GroupKind:
+ return slices.EqualFunc(v1.uncheckedGroup(), v2.uncheckedGroup(), Attr.Equal)
default:
panic(fmt.Sprintf("bad kind: %s", k1))
}
@@ -295,7 +315,7 @@
return append(dst, v.duration().String()...)
case TimeKind:
return append(dst, v.time().String()...)
- case AnyKind, LogValuerKind:
+ case AnyKind, GroupKind, LogValuerKind:
return append(dst, fmt.Sprint(v.any)...)
default:
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
diff --git a/slog/value_safe.go b/slog/value_safe.go
index f6dc422..532d4de 100644
--- a/slog/value_safe.go
+++ b/slog/value_safe.go
@@ -35,6 +35,8 @@
return k
case *time.Location:
return TimeKind
+ case []Attr:
+ return GroupKind
case LogValuer:
return LogValuerKind
default:
@@ -61,3 +63,13 @@
var buf []byte
return string(v.append(buf))
}
+
+func groupValue(as []Attr) Value {
+ return Value{any: as}
+}
+
+func (v Value) group() []Attr {
+ return v.any.([]Attr)
+}
+
+func (v Value) uncheckedGroup() []Attr { return v.group() }
diff --git a/slog/value_test.go b/slog/value_test.go
index 71e4d8c..8e27bda 100644
--- a/slog/value_test.go
+++ b/slog/value_test.go
@@ -23,6 +23,7 @@
BoolValue(false),
AnyValue(&x),
AnyValue(&y),
+ GroupValue(Bool("b", true), Int("i", 3)),
}
for i, v1 := range vals {
for j, v2 := range vals {
diff --git a/slog/value_unsafe.go b/slog/value_unsafe.go
index 5a4ad6c..8fe69d3 100644
--- a/slog/value_unsafe.go
+++ b/slog/value_unsafe.go
@@ -33,8 +33,10 @@
any any
}
-// stringptr is used in field `a` when the Value is a string.
-type stringptr unsafe.Pointer
+type (
+ stringptr unsafe.Pointer // used in Value.any when the Value is a string
+ groupptr unsafe.Pointer // used in Value.any when the Value is a []Attr
+)
// Kind returns the Value's Kind.
func (v Value) Kind() Kind {
@@ -45,6 +47,8 @@
return StringKind
case *time.Location:
return TimeKind
+ case groupptr:
+ return GroupKind
case LogValuer:
return LogValuerKind
default:
@@ -81,3 +85,21 @@
var buf []byte
return string(v.append(buf))
}
+
+func groupValue(as []Attr) Value {
+ hdr := (*reflect.SliceHeader)(unsafe.Pointer(&as))
+ return Value{num: uint64(hdr.Len), any: groupptr(hdr.Data)}
+}
+
+// Group returns the Value's value as a []Attr.
+// It panics if the Value's Kind is not GroupKind.
+func (v Value) group() []Attr {
+ if sp, ok := v.any.(groupptr); ok {
+ return unsafe.Slice((*Attr)(sp), v.num)
+ }
+ panic("Group: bad kind")
+}
+
+func (v Value) uncheckedGroup() []Attr {
+ return unsafe.Slice((*Attr)(v.any.(groupptr)), v.num)
+}