Skip to content

Commit

Permalink
feat: support custom ReflectType encoder (#1039)
Browse files Browse the repository at this point in the history
This adds support for overriding the mechanism we use to encode
`ReflectType` fields. That is, in the following,

    log.Info("foo", zap.Reflect("bar", baz))

It allows `baz` to be serialized using a third-party JSON library
by providing a custom ReflectedEncoder in the zapcore.EncoderConfig.
`encoding/json`'s Encoder type is a valid ReflectedEncoder.

Resolves #1034

Co-authored-by: Sung Yoon Whang <[email protected]>
Co-authored-by: Abhinav Gupta <[email protected]>
  • Loading branch information
3 people authored Dec 15, 2021
1 parent 9367581 commit 369c1bd
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 7 deletions.
4 changes: 4 additions & 0 deletions zapcore/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package zapcore

import (
"encoding/json"
"io"
"time"

"go.uber.org/zap/buffer"
Expand Down Expand Up @@ -331,6 +332,9 @@ type EncoderConfig struct {
// Unlike the other primitive type encoders, EncodeName is optional. The
// zero value falls back to FullNameEncoder.
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// Configure the encoder for interface{} type objects.
// If not provided, objects are encoded using json.Encoder
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// Configures the field separator used by the console encoder. Defaults
// to tab.
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
Expand Down
13 changes: 7 additions & 6 deletions zapcore/json_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ package zapcore

import (
"encoding/base64"
"encoding/json"
"math"
"sync"
"time"
Expand Down Expand Up @@ -64,7 +63,7 @@ type jsonEncoder struct {

// for encoding generic values by reflection
reflectBuf *buffer.Buffer
reflectEnc *json.Encoder
reflectEnc ReflectedEncoder
}

// NewJSONEncoder creates a fast, low-allocation JSON encoder. The encoder
Expand All @@ -88,6 +87,11 @@ func newJSONEncoder(cfg EncoderConfig, spaced bool) *jsonEncoder {
cfg.LineEnding = DefaultLineEnding
}

// If no EncoderConfig.NewReflectedEncoder is provided by the user, then use default
if cfg.NewReflectedEncoder == nil {
cfg.NewReflectedEncoder = defaultReflectedEncoder
}

return &jsonEncoder{
EncoderConfig: &cfg,
buf: bufferpool.Get(),
Expand Down Expand Up @@ -152,10 +156,7 @@ func (enc *jsonEncoder) AddInt64(key string, val int64) {
func (enc *jsonEncoder) resetReflectBuf() {
if enc.reflectBuf == nil {
enc.reflectBuf = bufferpool.Get()
enc.reflectEnc = json.NewEncoder(enc.reflectBuf)

// For consistency with our custom JSON encoder.
enc.reflectEnc.SetEscapeHTML(false)
enc.reflectEnc = enc.NewReflectedEncoder(enc.reflectBuf)
} else {
enc.reflectBuf.Reset()
}
Expand Down
2 changes: 1 addition & 1 deletion zapcore/json_encoder_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ func assertJSON(t *testing.T, expected string, enc *jsonEncoder) {
}

func assertOutput(t testing.TB, cfg EncoderConfig, expected string, f func(Encoder)) {
enc := &jsonEncoder{buf: bufferpool.Get(), EncoderConfig: &cfg}
enc := NewJSONEncoder(cfg).(*jsonEncoder)
f(enc)
assert.Equal(t, expected, enc.buf.String(), "Unexpected encoder output after adding.")

Expand Down
66 changes: 66 additions & 0 deletions zapcore/json_encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package zapcore_test

import (
"io"
"testing"
"time"

Expand Down Expand Up @@ -171,3 +172,68 @@ func TestJSONEmptyConfig(t *testing.T) {
})
}
}

// Encodes any object into empty json '{}'
type emptyReflectedEncoder struct {
writer io.Writer
}

func (enc *emptyReflectedEncoder) Encode(obj interface{}) error {
_, err := enc.writer.Write([]byte("{}"))
return err
}

func TestJSONCustomReflectedEncoder(t *testing.T) {
tests := []struct {
name string
field zapcore.Field
expected string
}{
{
name: "encode custom map object",
field: zapcore.Field{
Key: "data",
Type: zapcore.ReflectType,
Interface: map[string]interface{}{
"foo": "hello",
"bar": 1111,
},
},
expected: `{"data":{}}`,
},
{
name: "encode nil object",
field: zapcore.Field{
Key: "data",
Type: zapcore.ReflectType,
},
expected: `{"data":null}`,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

enc := zapcore.NewJSONEncoder(zapcore.EncoderConfig{
NewReflectedEncoder: func(writer io.Writer) zapcore.ReflectedEncoder {
return &emptyReflectedEncoder{
writer: writer,
}
},
})

buf, err := enc.EncodeEntry(zapcore.Entry{
Level: zapcore.DebugLevel,
Time: time.Now(),
LoggerName: "logger",
Message: "things happened",
}, []zapcore.Field{tt.field})
if assert.NoError(t, err, "Unexpected JSON encoding error.") {
assert.JSONEq(t, tt.expected, buf.String(), "Incorrect encoded JSON entry.")
}
buf.Free()
})
}
}
41 changes: 41 additions & 0 deletions zapcore/reflected_encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2016 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package zapcore

import (
"encoding/json"
"io"
)

// ReflectedEncoder serializes log fields that can't be serialized with Zap's
// JSON encoder. These have the ReflectType field type.
// Use EncoderConfig.NewReflectedEncoder to set this.
type ReflectedEncoder interface {
// Encode encodes and writes to the underlying data stream.
Encode(interface{}) error
}

func defaultReflectedEncoder(w io.Writer) ReflectedEncoder {
enc := json.NewEncoder(w)
// For consistency with our custom JSON encoder.
enc.SetEscapeHTML(false)
return enc
}

0 comments on commit 369c1bd

Please sign in to comment.