Skip to content

Commit

Permalink
Refactor Encoder.WriteEntry => Encoder.EncodeEntry (#245)
Browse files Browse the repository at this point in the history
* Refactor Encoder.{Write => Encode}Entry

* Just use final in jsonEncoder.EncodeEntry

* Add a short write test

* Fix dat package

* Drop AddSync from TestWriterFacility*
  • Loading branch information
jcorbin authored and akshayjshah committed Jan 13, 2017
1 parent 85a8431 commit c4d1142
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 116 deletions.
2 changes: 1 addition & 1 deletion logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func TestLoggerNoOpsDisabledLevels(t *testing.T) {
})
}

func TestLoggerWriteEntryFailure(t *testing.T) {
func TestLoggerWriteFailure(t *testing.T) {
errSink := &testutils.Buffer{}
logger := New(
zapcore.WriterFacility(
Expand Down
11 changes: 5 additions & 6 deletions zapcore/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@

package zapcore

import "io"

// ObjectEncoder is a strongly-typed, encoding-agnostic interface for adding a
// map- or struct-like object to the logging context. Like maps, ObjectEncoders
// aren't safe for concurrent use (though typical use shouldn't require locks).
Expand Down Expand Up @@ -54,15 +52,16 @@ type ArrayEncoder interface {
// lower-allocation.
//
// Implementations of the ObjectEncoder interface's methods can, of course,
// freely modify the receiver. However, the Clone and WriteEntry methods will
// freely modify the receiver. However, the Clone and EncodeEntry methods will
// be called concurrently and shouldn't modify the receiver.
type Encoder interface {
ObjectEncoder

// Clone copies the encoder, ensuring that adding fields to the copy doesn't
// affect the original.
Clone() Encoder
// Write a log entry and fields to the supplied writer, along with any
// accumulated context.
WriteEntry(io.Writer, Entry, []Field) error

// EncodeEntry encodes an entry and fields, along with any accumulated
// context, into a byte buffer and returns it.
EncodeEntry(Entry, []Field) ([]byte, error)
}
25 changes: 24 additions & 1 deletion zapcore/facility.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@
package zapcore

import (
"fmt"
"io"
"sync"
"time"

"go.uber.org/zap/internal/buffers"

"go.uber.org/atomic"
)

Expand Down Expand Up @@ -67,7 +71,13 @@ func (iof *ioFacility) Check(ent Entry, ce *CheckedEntry) *CheckedEntry {
}

func (iof *ioFacility) Write(ent Entry, fields []Field) error {
if err := iof.enc.WriteEntry(iof.out, ent, fields); err != nil {
buf, err := iof.enc.EncodeEntry(ent, fields)
if err != nil {
return err
}
err = checkPartialWrite(iof.out, buf)
buffers.Put(buf)
if err != nil {
return err
}
if ent.Level > ErrorLevel {
Expand Down Expand Up @@ -267,3 +277,16 @@ func (s *sampler) Check(ent Entry, ce *CheckedEntry) *CheckedEntry {
}
return s.Facility.Check(ent, ce)
}

// checkPartialWrite writes to an io.Writer, and upgrades partial writes to an
// error if no other write error occured.
func checkPartialWrite(w io.Writer, buf []byte) error {
n, err := w.Write(buf)
if err != nil {
return err
}
if n != len(buf) {
return fmt.Errorf("incomplete write: only wrote %v of %v bytes", n, len(buf))
}
return nil
}
14 changes: 12 additions & 2 deletions zapcore/facility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,23 @@ func TestWriterFacilitySyncsOutput(t *testing.T) {
}
}

func TestWriterFacilityWriteEntryFailure(t *testing.T) {
func TestWriterFacilityWriteFailure(t *testing.T) {
fac := WriterFacility(
NewJSONEncoder(testJSONConfig()),
Lock(AddSync(&testutils.FailWriter{})),
Lock(&testutils.FailWriter{}),
DebugLevel,
)
err := fac.Write(Entry{}, nil)
// Should log the error.
assert.Error(t, err, "Expected writing Entry to fail.")
}

func TestWriterFacilityShortWrite(t *testing.T) {
fac := WriterFacility(
NewJSONEncoder(testJSONConfig()),
Lock(&testutils.ShortWriter{}),
DebugLevel,
)
err := fac.Write(Entry{}, nil)
// Should log the error.
assert.Error(t, err, "Expected writing Entry to fail.")
Expand Down
64 changes: 16 additions & 48 deletions zapcore/json_encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ package zapcore

import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"strconv"
"sync"
"time"
"unicode/utf8"

"go.uber.org/zap/internal/buffers"
)

const (
Expand All @@ -39,19 +37,6 @@ const (
_initialBufSize = 1024
)

var (
// errNilSink signals that Encoder.WriteEntry was called with a nil WriteSyncer.
errNilSink = errors.New("can't write encoded message a nil WriteSyncer")

// TODO: use the new buffers package instead of pooling encoders.
jsonPool = sync.Pool{New: func() interface{} {
return &jsonEncoder{
// Pre-allocate a reasonably-sized buffer for each encoder.
bytes: make([]byte, 0, _initialBufSize),
}
}}
)

// A JSONConfig sets configuration options for a JSON encoder.
type JSONConfig struct {
MessageFormatter func(string) Field
Expand All @@ -75,10 +60,10 @@ type jsonEncoder struct {
// pair) when unmarshaling, but users should attempt to avoid adding duplicate
// keys.
func NewJSONEncoder(cfg JSONConfig) Encoder {
enc := jsonPool.Get().(*jsonEncoder)
enc.truncate()
enc.JSONConfig = &cfg
return enc
return &jsonEncoder{
JSONConfig: &cfg,
bytes: buffers.Get(),
}
}

func (enc *jsonEncoder) AddString(key, val string) {
Expand Down Expand Up @@ -159,29 +144,26 @@ func (enc *jsonEncoder) AppendBool(val bool) {
}

func (enc *jsonEncoder) Clone() Encoder {
clone := jsonPool.Get().(*jsonEncoder)
clone.truncate()
clone.bytes = append(clone.bytes, enc.bytes...)
clone.JSONConfig = enc.JSONConfig
clone := &jsonEncoder{JSONConfig: enc.JSONConfig}
clone.bytes = append(buffers.Get(), enc.bytes...)
return clone
}

func (enc *jsonEncoder) WriteEntry(sink io.Writer, ent Entry, fields []Field) error {
if sink == nil {
return errNilSink
func (enc *jsonEncoder) EncodeEntry(ent Entry, fields []Field) ([]byte, error) {
final := &jsonEncoder{
JSONConfig: enc.JSONConfig,
bytes: buffers.Get(),
}

final := jsonPool.Get().(*jsonEncoder)
final.truncate()
final.bytes = append(final.bytes, '{')
enc.LevelFormatter(ent.Level).AddTo(final)
enc.TimeFormatter(ent.Time).AddTo(final)
final.LevelFormatter(ent.Level).AddTo(final)
final.TimeFormatter(ent.Time).AddTo(final)
if ent.Caller.Defined {
// NOTE: we add the field here for parity compromise with text
// prepending, while not actually mutating the message string.
final.AddString("caller", ent.Caller.String())
}
enc.MessageFormatter(ent.Message).AddTo(final)
final.MessageFormatter(ent.Message).AddTo(final)
if len(enc.bytes) > 0 {
if len(final.bytes) > 1 {
// All the formatters may have been no-ops.
Expand All @@ -194,27 +176,13 @@ func (enc *jsonEncoder) WriteEntry(sink io.Writer, ent Entry, fields []Field) er
final.AddString("stacktrace", ent.Stack)
}
final.bytes = append(final.bytes, '}', '\n')

expectedBytes := len(final.bytes)
n, err := sink.Write(final.bytes)
final.free()
if err != nil {
return err
}
if n != expectedBytes {
return fmt.Errorf("incomplete write: only wrote %v of %v bytes", n, expectedBytes)
}
return nil
return final.bytes, nil
}

func (enc *jsonEncoder) truncate() {
enc.bytes = enc.bytes[:0]
}

func (enc *jsonEncoder) free() {
jsonPool.Put(enc)
}

func (enc *jsonEncoder) addKey(key string) {
enc.separateElements()
enc.bytes = append(enc.bytes, '"')
Expand Down
8 changes: 4 additions & 4 deletions zapcore/json_encoder_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ package zapcore

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

"go.uber.org/zap/internal/buffers"
)

func testJSONConfig() JSONConfig {
Expand Down Expand Up @@ -53,7 +54,6 @@ func BenchmarkJSONLogMarshalerFunc(b *testing.B) {
enc.AddInt64("i", int64(i))
return nil
}))
enc.free()
}
}

Expand All @@ -71,11 +71,11 @@ func BenchmarkZapJSON(b *testing.B) {
enc.AddString("string3", "🤔")
enc.AddString("string4", "🙊")
enc.AddBool("bool", true)
enc.WriteEntry(ioutil.Discard, Entry{
buf, _ := enc.EncodeEntry(Entry{
Message: "fake",
Level: DebugLevel,
}, nil)
enc.free()
buffers.Put(buf)
}
})
}
Expand Down
Loading

0 comments on commit c4d1142

Please sign in to comment.