Skip to content

Commit 15be647

Browse files
authored
Add Objects and ObjectValues field constructors (#1071)
This pull request adds two new field constructors for slices, parameterized over the types of the values in the slices: ```go func Objects[T zapcore.ObjectMarshaler](key string, value []T) Field ``` This turns a slice of any Zap marshalable object into a Zap field. ```go func ObjectValues[T any, P objectMarshalerPtr[T]](key string, values []T) Field ``` This is a variant of Objects for the case where `*T` implements `zapcore.ObjectMarshaler` but we have a `[]T`. Together, these two field constructors obviate the need for writing `ArrayMarshaler` implementations for slices of objects: ```go // Before ////////////// type userArray []*User func (uu userArray) MarshalLogArray(enc zapcore.ArrayEncoder) error { for _, u := range uu { if err := enc.AppendObject(u); err != nil { return err } } return nil } var users []*User = .. zap.Array("users", userArray(users)) // Now ///////////////// var users []*User = .. zap.Objects("users", users) ``` ### Backwards compatibility Both new field constructors are hidden behind a build tag, but use of type parameters requires us to bump the `go` directive in the `go.mod` file to 1.18. Note that this *does not* break Zap on Go 1.17. Per the [documentation for the `go` directive](https://go.dev/ref/mod#go-mod-file-go), > If an older Go version builds one of the module’s packages and encounters a > compile error, the error notes that the module was written for a newer Go > version. It only breaks our ability to run `go mod tidy` inside Zap from Go 1.17, which is okay because with the `go` directive, we're stating that we're developing Zap while on Go 1.18. ### Linting staticcheck support for type parameters is expected in a few weeks. Meanwhile, this disables staticcheck for the `make lint` target. (I considered switching linting to Go 1.17 until then, but that isn't enough because the formatting linter expects valid Go code--which type parameters aren't in 1.17.)
2 parents 34cb012 + 854d895 commit 15be647

File tree

6 files changed

+376
-4
lines changed

6 files changed

+376
-4
lines changed

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ lint: $(GOLINT) $(STATICCHECK)
2626
@$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go vet ./... 2>&1) &&) true | tee -a lint.log
2727
@echo "Checking lint..."
2828
@$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(GOLINT) ./... 2>&1) &&) true | tee -a lint.log
29-
@echo "Checking staticcheck..."
30-
@$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(STATICCHECK) ./... 2>&1) &&) true | tee -a lint.log
29+
# @echo "Checking staticcheck..."
30+
# @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(STATICCHECK) ./... 2>&1) &&) true | tee -a lint.log
31+
# TODO: Re-enable after https://github.com/dominikh/go-tools/issues/1166.
3132
@echo "Checking for unresolved FIXMEs..."
3233
@git grep -i fixme | grep -v -e Makefile | tee -a lint.log
3334
@echo "Checking for license headers..."

array_go118.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) 2022 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
//go:build go1.18
22+
// +build go1.18
23+
24+
package zap
25+
26+
import "go.uber.org/zap/zapcore"
27+
28+
// Objects constructs a field with the given key, holding a list of the
29+
// provided objects that can be marshaled by Zap.
30+
//
31+
// Note that these objects must implement zapcore.ObjectMarshaler directly.
32+
// That is, if you're trying to marshal a []Request, the MarshalLogObject
33+
// method must be declared on the Request type, not its pointer (*Request).
34+
// If it's on the pointer, use ObjectValues.
35+
//
36+
// Given an object that implements MarshalLogObject on the value receiver, you
37+
// can log a slice of those objects with Objects like so:
38+
//
39+
// type Author struct{ ... }
40+
// func (a Author) MarshalLogObject(enc zapcore.ObjectEncoder) error
41+
//
42+
// var authors []Author = ...
43+
// logger.Info("loading article", zap.Objects("authors", authors))
44+
//
45+
// Similarly, given a type that implements MarshalLogObject on its pointer
46+
// receiver, you can log a slice of pointers to that object with Objects like
47+
// so:
48+
//
49+
// type Request struct{ ... }
50+
// func (r *Request) MarshalLogObject(enc zapcore.ObjectEncoder) error
51+
//
52+
// var requests []*Request = ...
53+
// logger.Info("sending requests", zap.Objects("requests", requests))
54+
//
55+
// If instead, you have a slice of values of such an object, use the
56+
// ObjectValues constructor.
57+
//
58+
// var requests []Request = ...
59+
// logger.Info("sending requests", zap.ObjectValues("requests", requests))
60+
func Objects[T zapcore.ObjectMarshaler](key string, values []T) Field {
61+
return Array(key, objects[T](values))
62+
}
63+
64+
type objects[T zapcore.ObjectMarshaler] []T
65+
66+
func (os objects[T]) MarshalLogArray(arr zapcore.ArrayEncoder) error {
67+
for _, o := range os {
68+
if err := arr.AppendObject(o); err != nil {
69+
return err
70+
}
71+
}
72+
return nil
73+
}
74+
75+
// objectMarshalerPtr is a constraint that specifies that the given type
76+
// implements zapcore.ObjectMarshaler on a pointer receiver.
77+
type objectMarshalerPtr[T any] interface {
78+
*T
79+
zapcore.ObjectMarshaler
80+
}
81+
82+
// ObjectValues constructs a field with the given key, holding a list of the
83+
// provided objects, where pointers to these objects can be marshaled by Zap.
84+
//
85+
// Note that pointers to these objects must implement zapcore.ObjectMarshaler.
86+
// That is, if you're trying to marshal a []Request, the MarshalLogObject
87+
// method must be declared on the *Request type, not the value (Request).
88+
// If it's on the value, use Objects.
89+
//
90+
// Given an object that implements MarshalLogObject on the pointer receiver,
91+
// you can log a slice of those objects with ObjectValues like so:
92+
//
93+
// type Request struct{ ... }
94+
// func (r *Request) MarshalLogObject(enc zapcore.ObjectEncoder) error
95+
//
96+
// var requests []Request = ...
97+
// logger.Info("sending requests", zap.ObjectValues("requests", requests))
98+
//
99+
// If instead, you have a slice of pointers of such an object, use the Objects
100+
// field constructor.
101+
//
102+
// var requests []*Request = ...
103+
// logger.Info("sending requests", zap.Objects("requests", requests))
104+
func ObjectValues[T any, P objectMarshalerPtr[T]](key string, values []T) Field {
105+
return Array(key, objectValues[T, P](values))
106+
}
107+
108+
type objectValues[T any, P objectMarshalerPtr[T]] []T
109+
110+
func (os objectValues[T, P]) MarshalLogArray(arr zapcore.ArrayEncoder) error {
111+
for i := range os {
112+
// It is necessary for us to explicitly reference the "P" type.
113+
// We cannot simply pass "&os[i]" to AppendObject because its type
114+
// is "*T", which the type system does not consider as
115+
// implementing ObjectMarshaler.
116+
// Only the type "P" satisfies ObjectMarshaler, which we have
117+
// to convert "*T" to explicitly.
118+
var p P = &os[i]
119+
if err := arr.AppendObject(p); err != nil {
120+
return err
121+
}
122+
}
123+
return nil
124+
}

array_go118_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright (c) 2022 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
//go:build go1.18
22+
// +build go1.18
23+
24+
package zap
25+
26+
import (
27+
"errors"
28+
"testing"
29+
30+
"github.com/stretchr/testify/assert"
31+
"github.com/stretchr/testify/require"
32+
"go.uber.org/zap/zapcore"
33+
)
34+
35+
func TestObjectsAndObjectValues(t *testing.T) {
36+
t.Parallel()
37+
38+
tests := []struct {
39+
desc string
40+
give Field
41+
want []any
42+
}{
43+
{
44+
desc: "Objects/nil slice",
45+
give: Objects[*emptyObject]("", nil),
46+
want: []any{},
47+
},
48+
{
49+
desc: "ObjectValues/nil slice",
50+
give: ObjectValues[emptyObject]("", nil),
51+
want: []any{},
52+
},
53+
{
54+
desc: "ObjectValues/empty slice",
55+
give: ObjectValues("", []emptyObject{}),
56+
want: []any{},
57+
},
58+
{
59+
desc: "ObjectValues/single item",
60+
give: ObjectValues("", []emptyObject{
61+
{},
62+
}),
63+
want: []any{
64+
map[string]any{},
65+
},
66+
},
67+
{
68+
desc: "Objects/multiple different objects",
69+
give: Objects("", []*fakeObject{
70+
{value: "foo"},
71+
{value: "bar"},
72+
{value: "baz"},
73+
}),
74+
want: []any{
75+
map[string]any{"value": "foo"},
76+
map[string]any{"value": "bar"},
77+
map[string]any{"value": "baz"},
78+
},
79+
},
80+
{
81+
desc: "ObjectValues/multiple different objects",
82+
give: ObjectValues("", []fakeObject{
83+
{value: "foo"},
84+
{value: "bar"},
85+
{value: "baz"},
86+
}),
87+
want: []any{
88+
map[string]any{"value": "foo"},
89+
map[string]any{"value": "bar"},
90+
map[string]any{"value": "baz"},
91+
},
92+
},
93+
}
94+
95+
for _, tt := range tests {
96+
tt := tt
97+
t.Run(tt.desc, func(t *testing.T) {
98+
t.Parallel()
99+
100+
tt.give.Key = "k"
101+
102+
enc := zapcore.NewMapObjectEncoder()
103+
tt.give.AddTo(enc)
104+
assert.Equal(t, tt.want, enc.Fields["k"])
105+
})
106+
}
107+
}
108+
109+
type emptyObject struct{}
110+
111+
func (*emptyObject) MarshalLogObject(zapcore.ObjectEncoder) error {
112+
return nil
113+
}
114+
115+
type fakeObject struct {
116+
value string
117+
err error // marshaling error, if any
118+
}
119+
120+
func (o *fakeObject) MarshalLogObject(enc zapcore.ObjectEncoder) error {
121+
enc.AddString("value", o.value)
122+
return o.err
123+
}
124+
125+
func TestObjectsAndObjectValues_marshalError(t *testing.T) {
126+
t.Parallel()
127+
128+
tests := []struct {
129+
desc string
130+
give Field
131+
want []any
132+
wantErr string
133+
}{
134+
{
135+
desc: "Objects",
136+
give: Objects("", []*fakeObject{
137+
{value: "foo"},
138+
{value: "bar", err: errors.New("great sadness")},
139+
{value: "baz"}, // does not get marshaled
140+
}),
141+
want: []any{
142+
map[string]any{"value": "foo"},
143+
map[string]any{"value": "bar"},
144+
},
145+
wantErr: "great sadness",
146+
},
147+
{
148+
desc: "ObjectValues",
149+
give: ObjectValues("", []fakeObject{
150+
{value: "foo"},
151+
{value: "bar", err: errors.New("stuff failed")},
152+
{value: "baz"}, // does not get marshaled
153+
}),
154+
want: []any{
155+
map[string]any{"value": "foo"},
156+
map[string]any{"value": "bar"},
157+
},
158+
wantErr: "stuff failed",
159+
},
160+
}
161+
162+
for _, tt := range tests {
163+
tt := tt
164+
t.Run(tt.desc, func(t *testing.T) {
165+
t.Parallel()
166+
167+
tt.give.Key = "k"
168+
169+
enc := zapcore.NewMapObjectEncoder()
170+
tt.give.AddTo(enc)
171+
172+
require.Contains(t, enc.Fields, "k")
173+
assert.Equal(t, tt.want, enc.Fields["k"])
174+
175+
// AddTo puts the error in a "%vError" field based on the name of the
176+
// original field.
177+
require.Contains(t, enc.Fields, "kError")
178+
assert.Equal(t, tt.wantErr, enc.Fields["kError"])
179+
})
180+
}
181+
}

example_go118_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) 2022 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
//go:build go1.18
22+
// +build go1.18
23+
24+
package zap_test
25+
26+
import "go.uber.org/zap"
27+
28+
func ExampleObjects() {
29+
logger := zap.NewExample()
30+
defer logger.Sync()
31+
32+
// Use the Objects field constructor when you have a list of objects,
33+
// all of which implement zapcore.ObjectMarshaler.
34+
logger.Debug("opening connections",
35+
zap.Objects("addrs", []addr{
36+
{IP: "123.45.67.89", Port: 4040},
37+
{IP: "127.0.0.1", Port: 4041},
38+
{IP: "192.168.0.1", Port: 4042},
39+
}))
40+
// Output:
41+
// {"level":"debug","msg":"opening connections","addrs":[{"ip":"123.45.67.89","port":4040},{"ip":"127.0.0.1","port":4041},{"ip":"192.168.0.1","port":4042}]}
42+
}
43+
44+
func ExampleObjectValues() {
45+
logger := zap.NewExample()
46+
defer logger.Sync()
47+
48+
// Use the ObjectValues field constructor when you have a list of
49+
// objects that do not implement zapcore.ObjectMarshaler directly,
50+
// but on their pointer receivers.
51+
logger.Debug("starting tunnels",
52+
zap.ObjectValues("addrs", []request{
53+
{
54+
URL: "/foo",
55+
Listen: addr{"127.0.0.1", 8080},
56+
Remote: addr{"123.45.67.89", 4040},
57+
},
58+
{
59+
URL: "/bar",
60+
Listen: addr{"127.0.0.1", 8080},
61+
Remote: addr{"127.0.0.1", 31200},
62+
},
63+
}))
64+
// Output:
65+
// {"level":"debug","msg":"starting tunnels","addrs":[{"url":"/foo","ip":"127.0.0.1","port":8080,"remote":{"ip":"123.45.67.89","port":4040}},{"url":"/bar","ip":"127.0.0.1","port":8080,"remote":{"ip":"127.0.0.1","port":31200}}]}
66+
}

example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func (a addr) MarshalLogObject(enc zapcore.ObjectEncoder) error {
182182
return nil
183183
}
184184

185-
func (r request) MarshalLogObject(enc zapcore.ObjectEncoder) error {
185+
func (r *request) MarshalLogObject(enc zapcore.ObjectEncoder) error {
186186
enc.AddString("url", r.URL)
187187
zap.Inline(r.Listen).AddTo(enc)
188188
return enc.AddObject("remote", r.Remote)

0 commit comments

Comments
 (0)