-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.)
- Loading branch information
Showing
6 changed files
with
376 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
// Copyright (c) 2022 Uber Technologies, Inc. | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in | ||
// all copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
// THE SOFTWARE. | ||
|
||
//go:build go1.18 | ||
// +build go1.18 | ||
|
||
package zap | ||
|
||
import "go.uber.org/zap/zapcore" | ||
|
||
// Objects constructs a field with the given key, holding a list of the | ||
// provided objects that can be marshaled by Zap. | ||
// | ||
// Note that these objects must implement zapcore.ObjectMarshaler directly. | ||
// That is, if you're trying to marshal a []Request, the MarshalLogObject | ||
// method must be declared on the Request type, not its pointer (*Request). | ||
// If it's on the pointer, use ObjectValues. | ||
// | ||
// Given an object that implements MarshalLogObject on the value receiver, you | ||
// can log a slice of those objects with Objects like so: | ||
// | ||
// type Author struct{ ... } | ||
// func (a Author) MarshalLogObject(enc zapcore.ObjectEncoder) error | ||
// | ||
// var authors []Author = ... | ||
// logger.Info("loading article", zap.Objects("authors", authors)) | ||
// | ||
// Similarly, given a type that implements MarshalLogObject on its pointer | ||
// receiver, you can log a slice of pointers to that object with Objects like | ||
// so: | ||
// | ||
// type Request struct{ ... } | ||
// func (r *Request) MarshalLogObject(enc zapcore.ObjectEncoder) error | ||
// | ||
// var requests []*Request = ... | ||
// logger.Info("sending requests", zap.Objects("requests", requests)) | ||
// | ||
// If instead, you have a slice of values of such an object, use the | ||
// ObjectValues constructor. | ||
// | ||
// var requests []Request = ... | ||
// logger.Info("sending requests", zap.ObjectValues("requests", requests)) | ||
func Objects[T zapcore.ObjectMarshaler](key string, values []T) Field { | ||
return Array(key, objects[T](values)) | ||
} | ||
|
||
type objects[T zapcore.ObjectMarshaler] []T | ||
|
||
func (os objects[T]) MarshalLogArray(arr zapcore.ArrayEncoder) error { | ||
for _, o := range os { | ||
if err := arr.AppendObject(o); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// objectMarshalerPtr is a constraint that specifies that the given type | ||
// implements zapcore.ObjectMarshaler on a pointer receiver. | ||
type objectMarshalerPtr[T any] interface { | ||
*T | ||
zapcore.ObjectMarshaler | ||
} | ||
|
||
// ObjectValues constructs a field with the given key, holding a list of the | ||
// provided objects, where pointers to these objects can be marshaled by Zap. | ||
// | ||
// Note that pointers to these objects must implement zapcore.ObjectMarshaler. | ||
// That is, if you're trying to marshal a []Request, the MarshalLogObject | ||
// method must be declared on the *Request type, not the value (Request). | ||
// If it's on the value, use Objects. | ||
// | ||
// Given an object that implements MarshalLogObject on the pointer receiver, | ||
// you can log a slice of those objects with ObjectValues like so: | ||
// | ||
// type Request struct{ ... } | ||
// func (r *Request) MarshalLogObject(enc zapcore.ObjectEncoder) error | ||
// | ||
// var requests []Request = ... | ||
// logger.Info("sending requests", zap.ObjectValues("requests", requests)) | ||
// | ||
// If instead, you have a slice of pointers of such an object, use the Objects | ||
// field constructor. | ||
// | ||
// var requests []*Request = ... | ||
// logger.Info("sending requests", zap.Objects("requests", requests)) | ||
func ObjectValues[T any, P objectMarshalerPtr[T]](key string, values []T) Field { | ||
return Array(key, objectValues[T, P](values)) | ||
} | ||
|
||
type objectValues[T any, P objectMarshalerPtr[T]] []T | ||
|
||
func (os objectValues[T, P]) MarshalLogArray(arr zapcore.ArrayEncoder) error { | ||
for i := range os { | ||
// It is necessary for us to explicitly reference the "P" type. | ||
// We cannot simply pass "&os[i]" to AppendObject because its type | ||
// is "*T", which the type system does not consider as | ||
// implementing ObjectMarshaler. | ||
// Only the type "P" satisfies ObjectMarshaler, which we have | ||
// to convert "*T" to explicitly. | ||
var p P = &os[i] | ||
if err := arr.AppendObject(p); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
// Copyright (c) 2022 Uber Technologies, Inc. | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in | ||
// all copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
// THE SOFTWARE. | ||
|
||
//go:build go1.18 | ||
// +build go1.18 | ||
|
||
package zap | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"go.uber.org/zap/zapcore" | ||
) | ||
|
||
func TestObjectsAndObjectValues(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := []struct { | ||
desc string | ||
give Field | ||
want []any | ||
}{ | ||
{ | ||
desc: "Objects/nil slice", | ||
give: Objects[*emptyObject]("", nil), | ||
want: []any{}, | ||
}, | ||
{ | ||
desc: "ObjectValues/nil slice", | ||
give: ObjectValues[emptyObject]("", nil), | ||
want: []any{}, | ||
}, | ||
{ | ||
desc: "ObjectValues/empty slice", | ||
give: ObjectValues("", []emptyObject{}), | ||
want: []any{}, | ||
}, | ||
{ | ||
desc: "ObjectValues/single item", | ||
give: ObjectValues("", []emptyObject{ | ||
{}, | ||
}), | ||
want: []any{ | ||
map[string]any{}, | ||
}, | ||
}, | ||
{ | ||
desc: "Objects/multiple different objects", | ||
give: Objects("", []*fakeObject{ | ||
{value: "foo"}, | ||
{value: "bar"}, | ||
{value: "baz"}, | ||
}), | ||
want: []any{ | ||
map[string]any{"value": "foo"}, | ||
map[string]any{"value": "bar"}, | ||
map[string]any{"value": "baz"}, | ||
}, | ||
}, | ||
{ | ||
desc: "ObjectValues/multiple different objects", | ||
give: ObjectValues("", []fakeObject{ | ||
{value: "foo"}, | ||
{value: "bar"}, | ||
{value: "baz"}, | ||
}), | ||
want: []any{ | ||
map[string]any{"value": "foo"}, | ||
map[string]any{"value": "bar"}, | ||
map[string]any{"value": "baz"}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.desc, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
tt.give.Key = "k" | ||
|
||
enc := zapcore.NewMapObjectEncoder() | ||
tt.give.AddTo(enc) | ||
assert.Equal(t, tt.want, enc.Fields["k"]) | ||
}) | ||
} | ||
} | ||
|
||
type emptyObject struct{} | ||
|
||
func (*emptyObject) MarshalLogObject(zapcore.ObjectEncoder) error { | ||
return nil | ||
} | ||
|
||
type fakeObject struct { | ||
value string | ||
err error // marshaling error, if any | ||
} | ||
|
||
func (o *fakeObject) MarshalLogObject(enc zapcore.ObjectEncoder) error { | ||
enc.AddString("value", o.value) | ||
return o.err | ||
} | ||
|
||
func TestObjectsAndObjectValues_marshalError(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := []struct { | ||
desc string | ||
give Field | ||
want []any | ||
wantErr string | ||
}{ | ||
{ | ||
desc: "Objects", | ||
give: Objects("", []*fakeObject{ | ||
{value: "foo"}, | ||
{value: "bar", err: errors.New("great sadness")}, | ||
{value: "baz"}, // does not get marshaled | ||
}), | ||
want: []any{ | ||
map[string]any{"value": "foo"}, | ||
map[string]any{"value": "bar"}, | ||
}, | ||
wantErr: "great sadness", | ||
}, | ||
{ | ||
desc: "ObjectValues", | ||
give: ObjectValues("", []fakeObject{ | ||
{value: "foo"}, | ||
{value: "bar", err: errors.New("stuff failed")}, | ||
{value: "baz"}, // does not get marshaled | ||
}), | ||
want: []any{ | ||
map[string]any{"value": "foo"}, | ||
map[string]any{"value": "bar"}, | ||
}, | ||
wantErr: "stuff failed", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.desc, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
tt.give.Key = "k" | ||
|
||
enc := zapcore.NewMapObjectEncoder() | ||
tt.give.AddTo(enc) | ||
|
||
require.Contains(t, enc.Fields, "k") | ||
assert.Equal(t, tt.want, enc.Fields["k"]) | ||
|
||
// AddTo puts the error in a "%vError" field based on the name of the | ||
// original field. | ||
require.Contains(t, enc.Fields, "kError") | ||
assert.Equal(t, tt.wantErr, enc.Fields["kError"]) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright (c) 2022 Uber Technologies, Inc. | ||
// | ||
// Permission is hereby granted, free of charge, to any person obtaining a copy | ||
// of this software and associated documentation files (the "Software"), to deal | ||
// in the Software without restriction, including without limitation the rights | ||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
// copies of the Software, and to permit persons to whom the Software is | ||
// furnished to do so, subject to the following conditions: | ||
// | ||
// The above copyright notice and this permission notice shall be included in | ||
// all copies or substantial portions of the Software. | ||
// | ||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
// THE SOFTWARE. | ||
|
||
//go:build go1.18 | ||
// +build go1.18 | ||
|
||
package zap_test | ||
|
||
import "go.uber.org/zap" | ||
|
||
func ExampleObjects() { | ||
logger := zap.NewExample() | ||
defer logger.Sync() | ||
|
||
// Use the Objects field constructor when you have a list of objects, | ||
// all of which implement zapcore.ObjectMarshaler. | ||
logger.Debug("opening connections", | ||
zap.Objects("addrs", []addr{ | ||
{IP: "123.45.67.89", Port: 4040}, | ||
{IP: "127.0.0.1", Port: 4041}, | ||
{IP: "192.168.0.1", Port: 4042}, | ||
})) | ||
// Output: | ||
// {"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}]} | ||
} | ||
|
||
func ExampleObjectValues() { | ||
logger := zap.NewExample() | ||
defer logger.Sync() | ||
|
||
// Use the ObjectValues field constructor when you have a list of | ||
// objects that do not implement zapcore.ObjectMarshaler directly, | ||
// but on their pointer receivers. | ||
logger.Debug("starting tunnels", | ||
zap.ObjectValues("addrs", []request{ | ||
{ | ||
URL: "/foo", | ||
Listen: addr{"127.0.0.1", 8080}, | ||
Remote: addr{"123.45.67.89", 4040}, | ||
}, | ||
{ | ||
URL: "/bar", | ||
Listen: addr{"127.0.0.1", 8080}, | ||
Remote: addr{"127.0.0.1", 31200}, | ||
}, | ||
})) | ||
// Output: | ||
// {"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}}]} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.