Skip to content

Commit

Permalink
Support multi-field encoding using zap.Inline (#912)
Browse files Browse the repository at this point in the history
Fixes #876

Currently, a `zap.Field` can only represent a single key-value. Add
`zap.Inline` to allow adding multiple fields to the current
namespace from a type implementing `zap.ObjectMarshaler`.

This also solves a more general problem: a single `zap.Field` can now
be used to add multiple key/value pairs.
  • Loading branch information
prashantv committed Mar 23, 2021
1 parent 89e3820 commit ca7ddee
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 1 deletion.
39 changes: 39 additions & 0 deletions example_test.go
Expand Up @@ -165,6 +165,45 @@ func ExampleNamespace() {
// {"level":"info","msg":"tracked some metrics","metrics":{"counter":1}}
}

type addr struct {
IP string
Port int
}

type request struct {
URL string
Listen addr
Remote addr
}

func (a addr) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("ip", a.IP)
enc.AddInt("port", a.Port)
return nil
}

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)
}

func ExampleObject() {
logger := zap.NewExample()
defer logger.Sync()

req := &request{
URL: "/test",
Listen: addr{"127.0.0.1", 8080},
Remote: addr{"127.0.0.1", 31200},
}
logger.Info("new request, in nested object", zap.Object("req", req))
logger.Info("new request, inline", zap.Inline(req))
// Output:
// {"level":"info","msg":"new request, in nested object","req":{"url":"/test","ip":"127.0.0.1","port":8080,"remote":{"ip":"127.0.0.1","port":31200}}}
// {"level":"info","msg":"new request, inline","url":"/test","ip":"127.0.0.1","port":8080,"remote":{"ip":"127.0.0.1","port":31200}}
}

func ExampleNewStdLog() {
logger := zap.NewExample()
defer logger.Sync()
Expand Down
10 changes: 10 additions & 0 deletions field.go
Expand Up @@ -400,6 +400,16 @@ func Object(key string, val zapcore.ObjectMarshaler) Field {
return Field{Key: key, Type: zapcore.ObjectMarshalerType, Interface: val}
}

// Inline constructs a Field that is similar to Object, but it
// will add the elements of the provided ObjectMarshaler to the
// current namespace.
func Inline(val zapcore.ObjectMarshaler) Field {
return zapcore.Field{
Type: zapcore.InlineMarshalerType,
Interface: val,
}
}

// Any takes a key and an arbitrary value and chooses the best way to represent
// them as a field, falling back to a reflection-based approach only if
// necessary.
Expand Down
1 change: 1 addition & 0 deletions field_test.go
Expand Up @@ -123,6 +123,7 @@ func TestFieldConstructors(t *testing.T) {
{"Reflect", Field{Key: "k", Type: zapcore.ReflectType}, Reflect("k", nil)},
{"Stringer", Field{Key: "k", Type: zapcore.StringerType, Interface: addr}, Stringer("k", addr)},
{"Object", Field{Key: "k", Type: zapcore.ObjectMarshalerType, Interface: name}, Object("k", name)},
{"Inline", Field{Type: zapcore.InlineMarshalerType, Interface: name}, Inline(name)},
{"Any:ObjectMarshaler", Any("k", name), Object("k", name)},
{"Any:ArrayMarshaler", Any("k", bools([]bool{true})), Array("k", bools([]bool{true}))},
{"Any:Stringer", Any("k", addr), Stringer("k", addr)},
Expand Down
5 changes: 5 additions & 0 deletions zapcore/field.go
Expand Up @@ -39,6 +39,9 @@ const (
ArrayMarshalerType
// ObjectMarshalerType indicates that the field carries an ObjectMarshaler.
ObjectMarshalerType
// InlineMarshalerType indicates that the field carries an ObjectMarshaler
// that should be inlined.
InlineMarshalerType
// BinaryType indicates that the field carries an opaque binary blob.
BinaryType
// BoolType indicates that the field carries a bool.
Expand Down Expand Up @@ -115,6 +118,8 @@ func (f Field) AddTo(enc ObjectEncoder) {
err = enc.AddArray(f.Key, f.Interface.(ArrayMarshaler))
case ObjectMarshalerType:
err = enc.AddObject(f.Key, f.Interface.(ObjectMarshaler))
case InlineMarshalerType:
err = f.Interface.(ObjectMarshaler).MarshalLogObject(enc)
case BinaryType:
enc.AddBinary(f.Key, f.Interface.([]byte))
case BoolType:
Expand Down
23 changes: 22 additions & 1 deletion zapcore/field_test.go
Expand Up @@ -111,6 +111,7 @@ func TestFieldAddingError(t *testing.T) {
}{
{t: ArrayMarshalerType, iface: users(-1), want: []interface{}{}, err: "too few users"},
{t: ObjectMarshalerType, iface: users(-1), want: map[string]interface{}{}, err: "too few users"},
{t: InlineMarshalerType, iface: users(-1), want: nil, err: "too few users"},
{t: StringerType, iface: obj{}, want: empty, err: "PANIC=interface conversion: zapcore_test.obj is not fmt.Stringer: missing method String"},
{t: StringerType, iface: &obj{1}, want: empty, err: "PANIC=panic with string"},
{t: StringerType, iface: &obj{2}, want: empty, err: "PANIC=panic with error"},
Expand All @@ -136,7 +137,6 @@ func TestFields(t *testing.T) {
}{
{t: ArrayMarshalerType, iface: users(2), want: []interface{}{"user", "user"}},
{t: ObjectMarshalerType, iface: users(2), want: map[string]interface{}{"users": 2}},
{t: BinaryType, iface: []byte("foo"), want: []byte("foo")},
{t: BoolType, i: 0, want: false},
{t: ByteStringType, iface: []byte("foo"), want: "foo"},
{t: Complex128Type, iface: 1 + 2i, want: 1 + 2i},
Expand Down Expand Up @@ -180,6 +180,27 @@ func TestFields(t *testing.T) {
}
}

func TestInlineMarshaler(t *testing.T) {
enc := NewMapObjectEncoder()

topLevelStr := Field{Key: "k", Type: StringType, String: "s"}
topLevelStr.AddTo(enc)

inlineObj := Field{Key: "ignored", Type: InlineMarshalerType, Interface: users(10)}
inlineObj.AddTo(enc)

nestedObj := Field{Key: "nested", Type: ObjectMarshalerType, Interface: users(11)}
nestedObj.AddTo(enc)

assert.Equal(t, map[string]interface{}{
"k": "s",
"users": 10,
"nested": map[string]interface{}{
"users": 11,
},
}, enc.Fields)
}

func TestEquals(t *testing.T) {
// Values outside the UnixNano range were encoded incorrectly (#737, #803).
timeOutOfRangeHigh := time.Unix(0, math.MaxInt64).Add(time.Nanosecond)
Expand Down

0 comments on commit ca7ddee

Please sign in to comment.