Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Objects and ObjectValues field constructors #1071

Merged
merged 4 commits into from Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions Makefile
Expand Up @@ -26,8 +26,9 @@ lint: $(GOLINT) $(STATICCHECK)
@$(foreach dir,$(MODULE_DIRS),(cd $(dir) && go vet ./... 2>&1) &&) true | tee -a lint.log
@echo "Checking lint..."
@$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(GOLINT) ./... 2>&1) &&) true | tee -a lint.log
@echo "Checking staticcheck..."
@$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(STATICCHECK) ./... 2>&1) &&) true | tee -a lint.log
# @echo "Checking staticcheck..."
# @$(foreach dir,$(MODULE_DIRS),(cd $(dir) && $(STATICCHECK) ./... 2>&1) &&) true | tee -a lint.log
# TODO: Re-enable after https://github.com/dominikh/go-tools/issues/1166.
@echo "Checking for unresolved FIXMEs..."
@git grep -i fixme | grep -v -e Makefile | tee -a lint.log
@echo "Checking for license headers..."
Expand Down
124 changes: 124 additions & 0 deletions array_go118.go
@@ -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
}
181 changes: 181 additions & 0 deletions array_go118_test.go
@@ -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"])
})
}
}
66 changes: 66 additions & 0 deletions example_go118_test.go
@@ -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}}]}
}
2 changes: 1 addition & 1 deletion example_test.go
Expand Up @@ -182,7 +182,7 @@ func (a addr) MarshalLogObject(enc zapcore.ObjectEncoder) error {
return nil
}

func (r request) MarshalLogObject(enc zapcore.ObjectEncoder) error {
func (r *request) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("url", r.URL)
zap.Inline(r.Listen).AddTo(enc)
return enc.AddObject("remote", r.Remote)
Expand Down