diff --git a/logger.go b/logger.go index c7d9b73..a0bd2e2 100644 --- a/logger.go +++ b/logger.go @@ -17,9 +17,15 @@ type Fielder interface { // Fields represents a map of entry level data used for structured logging. type Fields map[string]interface{} -// Fields implements Fielder. +// Fields implements Fielder, returning a shallow copy of Fields. +// This is to prevent runtime error that happens when we read and +// write a map at the same time. func (f Fields) Fields() Fields { - return f + res := map[string]interface{}{} + for k, v := range f { + res[k] = v + } + return res } // Get field value by name. diff --git a/logger_test.go b/logger_test.go index 17e2381..df3b78f 100644 --- a/logger_test.go +++ b/logger_test.go @@ -2,6 +2,7 @@ package log_test import ( "fmt" + "sync" "testing" "github.com/apex/log" @@ -194,6 +195,44 @@ func TestLogger_HandlerFunc(t *testing.T) { assert.Equal(t, e.Level, log.InfoLevel) } +// TestLogger_Concurrent is testing the thread-safety of the library. +// We're running a custom attribute that is read and written at the same +// time in different goroutines. +// +// If the library is thread safe, this operation won't have any runtime errors. +func TestLogger_Concurrent(t *testing.T) { + var l log.Interface + l = &log.Logger{ + Handler: discard.New(), + Level: log.DebugLevel, + } + + type attribute map[string]interface{} + type withAttr struct { + guard sync.Mutex + attrs attribute + } + + mm := withAttr{attrs: attribute{"one": 1}} + + // loop to make sure collision of read and write happens + for i := 0; i < 50; i++ { + // write the fields + go func(val int) { + mm.guard.Lock() + defer mm.guard.Unlock() + + mm.attrs[fmt.Sprintf("%d", val+1)] = val * val + l = l.WithFields(log.Fields(mm.attrs)) + }(i) + + // read the fields + go func(val int) { + l.Debugf("read %d", val) + }(i) + } +} + func BenchmarkLogger_small(b *testing.B) { l := &log.Logger{ Handler: discard.New(),