From c6dbd12f7571821ca03259bc5d5fdb74f1a95cc5 Mon Sep 17 00:00:00 2001 From: Prashant Varanasi Date: Mon, 7 Dec 2020 23:23:37 -0800 Subject: [PATCH 1/4] Support multi-field encoding using zap.InlineObject Fixes #876 Currently, a `zap.Field` can only represent a single key-value. Add `zap.InlineObject` so 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. --- example_test.go | 23 +++++++++++++++++++++++ field.go | 9 +++++++++ field_test.go | 1 + zapcore/field.go | 5 +++++ zapcore/field_test.go | 23 ++++++++++++++++++++++- 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index ab5733f45..0ca4a4e56 100644 --- a/example_test.go +++ b/example_test.go @@ -165,6 +165,29 @@ func ExampleNamespace() { // {"level":"info","msg":"tracked some metrics","metrics":{"counter":1}} } +type request struct { + URL string + IP string +} + +func (r request) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("url", r.URL) + enc.AddString("ip", r.IP) + return nil +} + +func ExampleObject() { + logger := zap.NewExample() + defer logger.Sync() + + req := &request{"/test", "127.0.0.1"} + logger.Info("new request, in nested object", zap.Object("req", req)) + logger.Info("new request, inline", zap.InlineObject(req)) + // Output: + // {"level":"info","msg":"new request, in nested object","req":{"url":"/test","ip":"127.0.0.1"}} + // {"level":"info","msg":"new request, inline","url":"/test","ip":"127.0.0.1"} +} + func ExampleNewStdLog() { logger := zap.NewExample() defer logger.Sync() diff --git a/field.go b/field.go index 3c0d7d957..02018a834 100644 --- a/field.go +++ b/field.go @@ -400,6 +400,15 @@ func Object(key string, val zapcore.ObjectMarshaler) Field { return Field{Key: key, Type: zapcore.ObjectMarshalerType, Interface: val} } +// InlineObject is similar to Object, but does not nest the object under a field +// name, but adds the fields to the current namespace inline. +func InlineObject(val zapcore.ObjectMarshaler) Field { + return zapcore.Field{ + Type: zapcore.InlineObjectMarshalerType, + 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. diff --git a/field_test.go b/field_test.go index fbfc635d5..0753874fb 100644 --- a/field_test.go +++ b/field_test.go @@ -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)}, + {"InlineObject", Field{Type: zapcore.InlineObjectMarshalerType, Interface: name}, InlineObject(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)}, diff --git a/zapcore/field.go b/zapcore/field.go index e0105868e..b7799a4e0 100644 --- a/zapcore/field.go +++ b/zapcore/field.go @@ -39,6 +39,9 @@ const ( ArrayMarshalerType // ObjectMarshalerType indicates that the field carries an ObjectMarshaler. ObjectMarshalerType + // InlineObjectMarshalerType indicates that the field carries an ObjectMarshaler + // that should be inlined. + InlineObjectMarshalerType // BinaryType indicates that the field carries an opaque binary blob. BinaryType // BoolType indicates that the field carries a bool. @@ -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 InlineObjectMarshalerType: + err = f.Interface.(ObjectMarshaler).MarshalLogObject(enc) case BinaryType: enc.AddBinary(f.Key, f.Interface.([]byte)) case BoolType: diff --git a/zapcore/field_test.go b/zapcore/field_test.go index 31de0b623..60e158afd 100644 --- a/zapcore/field_test.go +++ b/zapcore/field_test.go @@ -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: InlineObjectMarshalerType, 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"}, @@ -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}, @@ -180,6 +180,27 @@ func TestFields(t *testing.T) { } } +func TestInlineObjectMarshaler(t *testing.T) { + enc := NewMapObjectEncoder() + + topLevelStr := Field{Key: "k", Type: StringType, String: "s"} + topLevelStr.AddTo(enc) + + inlineObj := Field{Key: "ignored", Type: InlineObjectMarshalerType, 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) From fff4bfa9828adfb0ce96535446561886583c6a7b Mon Sep 17 00:00:00 2001 From: Prashant Varanasi Date: Mon, 1 Mar 2021 17:34:28 -0800 Subject: [PATCH 2/4] Add nested object test --- example_test.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/example_test.go b/example_test.go index 0ca4a4e56..42258aa06 100644 --- a/example_test.go +++ b/example_test.go @@ -165,27 +165,43 @@ func ExampleNamespace() { // {"level":"info","msg":"tracked some metrics","metrics":{"counter":1}} } +type addr struct { + IP string + Port int +} + type request struct { - URL string - IP string + 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) - enc.AddString("ip", r.IP) - return nil + zap.InlineObject(r.Listen).AddTo(enc) + return enc.AddObject("remote", r.Remote) } func ExampleObject() { logger := zap.NewExample() defer logger.Sync() - req := &request{"/test", "127.0.0.1"} + 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.InlineObject(req)) // Output: - // {"level":"info","msg":"new request, in nested object","req":{"url":"/test","ip":"127.0.0.1"}} - // {"level":"info","msg":"new request, inline","url":"/test","ip":"127.0.0.1"} + // {"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() { From cdfbaec42b4b581be27e70659c62dc24c4f15aca Mon Sep 17 00:00:00 2001 From: Prashant Varanasi Date: Mon, 1 Mar 2021 17:40:04 -0800 Subject: [PATCH 3/4] s/InlineObject/Inline --- example_test.go | 4 ++-- field.go | 6 +++--- field_test.go | 2 +- zapcore/field.go | 6 +++--- zapcore/field_test.go | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/example_test.go b/example_test.go index 42258aa06..28474d0cd 100644 --- a/example_test.go +++ b/example_test.go @@ -184,7 +184,7 @@ func (a addr) MarshalLogObject(enc zapcore.ObjectEncoder) error { func (r request) MarshalLogObject(enc zapcore.ObjectEncoder) error { enc.AddString("url", r.URL) - zap.InlineObject(r.Listen).AddTo(enc) + zap.Inline(r.Listen).AddTo(enc) return enc.AddObject("remote", r.Remote) } @@ -198,7 +198,7 @@ func ExampleObject() { Remote: addr{"127.0.0.1", 31200}, } logger.Info("new request, in nested object", zap.Object("req", req)) - logger.Info("new request, inline", zap.InlineObject(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}} diff --git a/field.go b/field.go index 02018a834..cc74deb87 100644 --- a/field.go +++ b/field.go @@ -400,11 +400,11 @@ func Object(key string, val zapcore.ObjectMarshaler) Field { return Field{Key: key, Type: zapcore.ObjectMarshalerType, Interface: val} } -// InlineObject is similar to Object, but does not nest the object under a field +// Inline is similar to Object, but does not nest the object under a field // name, but adds the fields to the current namespace inline. -func InlineObject(val zapcore.ObjectMarshaler) Field { +func Inline(val zapcore.ObjectMarshaler) Field { return zapcore.Field{ - Type: zapcore.InlineObjectMarshalerType, + Type: zapcore.InlineMarshalerType, Interface: val, } } diff --git a/field_test.go b/field_test.go index 0753874fb..010e6fb4d 100644 --- a/field_test.go +++ b/field_test.go @@ -123,7 +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)}, - {"InlineObject", Field{Type: zapcore.InlineObjectMarshalerType, Interface: name}, InlineObject(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)}, diff --git a/zapcore/field.go b/zapcore/field.go index b7799a4e0..29daaace9 100644 --- a/zapcore/field.go +++ b/zapcore/field.go @@ -39,9 +39,9 @@ const ( ArrayMarshalerType // ObjectMarshalerType indicates that the field carries an ObjectMarshaler. ObjectMarshalerType - // InlineObjectMarshalerType indicates that the field carries an ObjectMarshaler + // InlineMarshalerType indicates that the field carries an ObjectMarshaler // that should be inlined. - InlineObjectMarshalerType + InlineMarshalerType // BinaryType indicates that the field carries an opaque binary blob. BinaryType // BoolType indicates that the field carries a bool. @@ -118,7 +118,7 @@ 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 InlineObjectMarshalerType: + case InlineMarshalerType: err = f.Interface.(ObjectMarshaler).MarshalLogObject(enc) case BinaryType: enc.AddBinary(f.Key, f.Interface.([]byte)) diff --git a/zapcore/field_test.go b/zapcore/field_test.go index 60e158afd..c4363297c 100644 --- a/zapcore/field_test.go +++ b/zapcore/field_test.go @@ -111,7 +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: InlineObjectMarshalerType, iface: users(-1), want: nil, 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"}, @@ -180,13 +180,13 @@ func TestFields(t *testing.T) { } } -func TestInlineObjectMarshaler(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: InlineObjectMarshalerType, Interface: users(10)} + inlineObj := Field{Key: "ignored", Type: InlineMarshalerType, Interface: users(10)} inlineObj.AddTo(enc) nestedObj := Field{Key: "nested", Type: ObjectMarshalerType, Interface: users(11)} From f17ba226437d913230a11ebffbb424f606b8cf69 Mon Sep 17 00:00:00 2001 From: Prashant Varanasi Date: Fri, 19 Mar 2021 16:23:37 -0700 Subject: [PATCH 4/4] Update Inline comment --- field.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/field.go b/field.go index cc74deb87..bbb745db5 100644 --- a/field.go +++ b/field.go @@ -400,8 +400,9 @@ func Object(key string, val zapcore.ObjectMarshaler) Field { return Field{Key: key, Type: zapcore.ObjectMarshalerType, Interface: val} } -// Inline is similar to Object, but does not nest the object under a field -// name, but adds the fields to the current namespace inline. +// 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,