Open stevestotter opened 4 years ago
This would introduce serialization of all the writes of a logger, just to handle this edge case. For me it's a no-go.
You should only change the global logger before you start using it concurrently (during your app init for instance). If you tests need to customize the global logger, it should be creating it's own logger and not rely on the global logger.
It's a design decision for sure and I do appreciate that. The problem for me is that I've come from using logrus which I could do this as it was thread-safe. Looking at zap's global logger, it also allows this.
I mean don't get me wrong, a global logger stinks of an anti-pattern and screams dependency injection - it's just so damn convenient (and is accepted in the standard library).
In the case of logrus, they have a MutexWrap on the logger, which can essentially turn on serialisation across the logger. This would solve the problem and we could get rid of the SyncWriter, but I appreciate this is a big change...
I wouldn’t like my logging library to add mutexes in my back :)
This is only tangentially related, but we're looking to use Zerolog and we're running into a similar data race when zerolog.Context
instances are passed across goroutines. Glad to split out into another issue if you'd prefer.
This test consistently reproduces the problem for me
func TestZerologRaces(t *testing.T) {
zl := zerolog.New(io.Discard).With()
f := func(c zerolog.Context, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 10000; i++ {
l := c.Int("i", i).Str("yee", "haw").Str("bar", "baz").Logger()
l.Info().Msg("foo")
}
}
var wg sync.WaitGroup
wg.Add(4)
go f(zl.Str("name", "1"), &wg)
go f(zl.Str("name", "2"), &wg)
go f(zl.Str("name", "3"), &wg)
go f(zl.Str("name", "4"), &wg)
wg.Wait()
}
The data races are happening during writes to the Logger.context
byte slice.
What is the expected/recommended usage of Zerolog in a concurrent environment? Are Logger
instances - rather than Context
- safe to pass across goroutines?
A given Logger
instance is not thread safe (by design). Passing a logger through a context is meant to be used in the context of an HTTP request for instance, with a single goroutine per context. If you are getting the logger from a different goroutine you should make a copy before using it.
Thanks for the reply. We've updated our code to pass around Logger
instances instead of Context
and we're not seeing the data races anymore.
@rs could you share the link or code snippet where explain how to use zerolog
properly for multiple go-routines?
How to properly instance logger based on global logger?
When I try
import "github.com/rs/zerolog/log"
func .. {
go somethingDo(log.With().Str("logger","somtehing").Logger())
}
I still got race conditions
@rs I tried to use diode as a thread-safe writer ;( but still get a lot of race conditions please help, how to properly initialize childs from global logger? which will thread safe?
for my issue, solved with https://github.com/Altinity/clickhouse-backup/pull/670#discussion_r1677387856 help me, thanks @rdmrcv
I've noticed that when I'm trying to test the output of logs in some tests, it can cause a race condition to be detected. This is because reassigning the global logger's output is not thread-safe. Just to clarify, I've understood that the only way to do this is
log.Logger = log.Output(<writer>)
. log.Logger.Output() returns a copy of the global logger, it doesn't adjust itself.In the Go standard library, log.SetOutput() is thread-safe. Can we implement the same here? If so, I'd be happy to do the PR.
Example code to make it as simple as possible to replicate the issue: (run it with
go test -race -count=1 ./...
)Output is:
Interestingly, rearranging the tests the other way "fixes" the issue, but of course that's super brittle and doesn't solve the problem 😅