From c345fd83b60ff36e10172e2522ee1dd7e8a82180 Mon Sep 17 00:00:00 2001 From: Prashant Varanasi Date: Tue, 23 Mar 2021 08:27:47 -0700 Subject: [PATCH] Support multi-field encoding using zap.Inline (#912) 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. --- example_test.go | 39 +++++++++++++++++++++++++++++++++++++++ field.go | 10 ++++++++++ field_test.go | 1 + zapcore/field.go | 5 +++++ zapcore/field_test.go | 23 ++++++++++++++++++++++- 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index ab5733f45..28474d0cd 100644 --- a/example_test.go +++ b/example_test.go @@ -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() diff --git a/field.go b/field.go index 3c0d7d957..bbb745db5 100644 --- a/field.go +++ b/field.go @@ -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. diff --git a/field_test.go b/field_test.go index fbfc635d5..010e6fb4d 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)}, + {"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 e0105868e..29daaace9 100644 --- a/zapcore/field.go +++ b/zapcore/field.go @@ -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. @@ -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: diff --git a/zapcore/field_test.go b/zapcore/field_test.go index 31de0b623..c4363297c 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: 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"}, @@ -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 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)