Skip to content

Commit

Permalink
Support concurrent logging to a single WriteSyncer (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
prashantv committed Jul 27, 2016
1 parent 262fa02 commit 0a2fe7e
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 4 deletions.
35 changes: 35 additions & 0 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"io/ioutil"
"os"
"strings"
"sync"
"testing"

"github.com/uber-go/zap/spywrite"
Expand Down Expand Up @@ -274,3 +275,37 @@ func TestJSONLoggerSyncsOutput(t *testing.T) {
assert.Panics(t, func() { logger.Panic("foo") }, "Expected panic when logging at Panic level.")
assert.True(t, sink.Called(), "Expected logging at panic level to Sync underlying WriteSyncer.")
}

func TestLoggerConcurrent(t *testing.T) {
buf := &bytes.Buffer{}
withJSONLogger(t, []Option{Output(AddSync(buf))}, func(jl *jsonLogger, output func() []string) {
jl.StubTime()
jl2 := jl.With(String("foo", "bar"))

wg := &sync.WaitGroup{}
runNTimes(5 /* goroutines */, 10 /* iterations */, wg, func() {
jl.Info("info", String("foo", "bar"))
})
runNTimes(5 /* goroutines */, 10 /* iterations */, wg, func() {
jl2.Info("info")
})

wg.Wait()

// Make sure the output doesn't contain interspersed entries.
expected := `{"msg":"info","level":"info","ts":0,"fields":{"foo":"bar"}}` + "\n"
assert.Equal(t, strings.Repeat(expected, 100), buf.String())
})
}

func runNTimes(goroutines, iterations int, wg *sync.WaitGroup, f func()) {
wg.Add(goroutines)
for g := 0; g < goroutines; g++ {
go func() {
defer wg.Done()
for i := 0; i < iterations; i++ {
f()
}
}()
}
}
4 changes: 2 additions & 2 deletions meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ func NewMeta() *Meta {
return &Meta{
lvl: atomic.NewInt32(int32(InfoLevel)),
Encoder: newJSONEncoder(),
Output: os.Stdout,
ErrorOutput: os.Stderr,
Output: newLockedWriteSyncer(os.Stdout),
ErrorOutput: newLockedWriteSyncer(os.Stderr),
}
}

Expand Down
4 changes: 2 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ func Fields(fields ...Field) Option {
// Output sets the destination for the logger's output.
func Output(w WriteSyncer) Option {
return optionFunc(func(m *Meta) {
m.Output = w
m.Output = newLockedWriteSyncer(w)
})
}

// ErrorOutput sets the destination for errors generated by the logger.
func ErrorOutput(w WriteSyncer) Option {
return optionFunc(func(m *Meta) {
m.ErrorOutput = w
m.ErrorOutput = newLockedWriteSyncer(w)
})
}

Expand Down
24 changes: 24 additions & 0 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package zap
import (
"io"
"io/ioutil"
"sync"
)

// Discard is a convenience wrapper around ioutil.Discard.
Expand Down Expand Up @@ -56,6 +57,29 @@ func AddSync(w io.Writer) WriteSyncer {
}
}

type lockedWriteSyncer struct {
sync.Mutex
ws WriteSyncer
}

func newLockedWriteSyncer(ws WriteSyncer) WriteSyncer {
return &lockedWriteSyncer{ws: ws}
}

func (s *lockedWriteSyncer) Write(bs []byte) (int, error) {
s.Lock()
n, err := s.ws.Write(bs)
s.Unlock()
return n, err
}

func (s *lockedWriteSyncer) Sync() error {
s.Lock()
err := s.ws.Sync()
s.Unlock()
return err
}

type writerWrapper struct {
io.Writer
}
Expand Down

0 comments on commit 0a2fe7e

Please sign in to comment.