From ca7ddee73f91d02d14769b555327b30d967d87bd Mon Sep 17 00:00:00 2001
From: Prashant Varanasi <prashant@uber.com>
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)