Skip to content

Commit

Permalink
Support multi-field encoding using zap.InlineObject
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.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.
  • Loading branch information
prashantv committed Feb 5, 2021
1 parent a68efdb commit 248b2a4
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 1 deletion.
23 changes: 23 additions & 0 deletions example_test.go
Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions field.go
Expand Up @@ -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.
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)},
{"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)},
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
// 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.
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 InlineObjectMarshalerType:
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: 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"},
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 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)
Expand Down

0 comments on commit 248b2a4

Please sign in to comment.