From 248b2a44e7304a9ee80a9e8d1696ce743b9fc0e1 Mon Sep 17 00:00:00 2001 From: Prashant Varanasi Date: Mon, 7 Dec 2020 23:23:37 -0800 Subject: [PATCH] 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)