Closed Nikolo closed 1 year ago
Hello @Nikolo I tried to understand your issue, but I just don't get it. Could you add more information? What did you expect? What is the actual behaviour you see? Why is it wrong, in your opinion? 😇
Oh. I'm sorry. All json in exaples is broken... I expected to see the valid json. And I expect to see valid data in json, all ID must be more than 41240000.
💡 Now I got it and can reproduce it with your script.
I got somewhat garbled output like this, where symbols are totally out of place.
"string":"xxxxxxx,"I,"I41240731048,"time":
"xxxxxxxxx","ID":4,"time":"20
I tried running your example with just two log lines and activated race detection:
go run -race . master-!?
==================
WARNING: DATA RACE
Write at 0x00c00014a001 by goroutine 7:
github.com/rs/zerolog/internal/json.Encoder.AppendString()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/string.go:46 +0xa8
github.com/rs/zerolog/internal/json.Encoder.AppendKey()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/base.go:18 +0x153
github.com/rs/zerolog.Context.Str()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/context.go:77 +0x73
main.SetLogVar.func1()
/Projects/delegation_poker/main.go:62 +0xb7
github.com/rs/zerolog.(*Logger).UpdateContext()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:288 +0x29d
main.SetLogVar()
/Projects/delegation_poker/main.go:60 +0x95
main.main.func1()
/Projects/delegation_poker/main.go:47 +0x14f
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Previous read at 0x00c00014a001 by goroutine 8:
runtime.slicecopy()
/Go/go1.19.2/src/runtime/slice.go:307 +0x0
github.com/rs/zerolog/internal/json.Encoder.AppendObjectData()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/types.go:388 +0x4e8
github.com/rs/zerolog.(*Logger).newEvent()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:459 +0x1d0
github.com/rs/zerolog.(*Logger).Debug()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:326 +0x16d
main.main.func1()
/Projects/delegation_poker/main.go:50 +0x150
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Goroutine 7 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
Goroutine 8 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
==================
==================
WARNING: DATA RACE
Write at 0x00c00014a008 by goroutine 7:
github.com/rs/zerolog/internal/json.Encoder.AppendString()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/string.go:63 +0x2c8
github.com/rs/zerolog/internal/json.Encoder.AppendKey()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/base.go:18 +0x153
github.com/rs/zerolog.Context.Str()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/context.go:77 +0x73
main.SetLogVar.func1()
/Projects/delegation_poker/main.go:62 +0xb7
github.com/rs/zerolog.(*Logger).UpdateContext()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:288 +0x29d
main.SetLogVar()
/Projects/delegation_poker/main.go:60 +0x95
main.main.func1()
/Projects/delegation_poker/main.go:47 +0x14f
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Previous read at 0x00c00014a008 by goroutine 8:
runtime.slicecopy()
/Go/go1.19.2/src/runtime/slice.go:307 +0x0
github.com/rs/zerolog/internal/json.Encoder.AppendObjectData()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/types.go:388 +0x4e8
github.com/rs/zerolog.(*Logger).newEvent()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:459 +0x1d0
github.com/rs/zerolog.(*Logger).Debug()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:326 +0x16d
main.main.func1()
/Projects/delegation_poker/main.go:50 +0x150
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Goroutine 7 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
Goroutine 8 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
==================
==================
WARNING: DATA RACE
Write at 0x00c00014a016 by goroutine 7:
github.com/rs/zerolog/internal/json.Encoder.AppendString()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/string.go:63 +0x2c8
github.com/rs/zerolog.Context.Str()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/context.go:77 +0x1c4
main.SetLogVar.func1()
/Projects/delegation_poker/main.go:62 +0xb7
github.com/rs/zerolog.(*Logger).UpdateContext()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:288 +0x29d
main.SetLogVar()
/Projects/delegation_poker/main.go:60 +0x95
main.main.func1()
/Projects/delegation_poker/main.go:47 +0x14f
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Previous read at 0x00c00014a010 by goroutine 8:
runtime.slicecopy()
/Go/go1.19.2/src/runtime/slice.go:307 +0x0
github.com/rs/zerolog/internal/json.Encoder.AppendObjectData()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/types.go:388 +0x4e8
github.com/rs/zerolog.(*Logger).newEvent()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:459 +0x1d0
github.com/rs/zerolog.(*Logger).Debug()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:326 +0x16d
main.main.func1()
/Projects/delegation_poker/main.go:50 +0x150
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Goroutine 7 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
Goroutine 8 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
==================
==================
WARNING: DATA RACE
Write at 0x00c00014a018 by goroutine 7:
github.com/rs/zerolog/internal/json.Encoder.AppendString()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/string.go:46 +0xa8
github.com/rs/zerolog/internal/json.Encoder.AppendKey()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/base.go:18 +0x136
github.com/rs/zerolog.Context.Uint64()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/context.go:290 +0x56
main.SetLogVar.func1()
/Projects/delegation_poker/main.go:62 +0x131
github.com/rs/zerolog.(*Logger).UpdateContext()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:288 +0x29d
main.SetLogVar()
/Projects/delegation_poker/main.go:60 +0x95
main.main.func1()
/Projects/delegation_poker/main.go:47 +0x14f
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Previous write at 0x00c00014a018 by goroutine 8:
runtime.slicecopy()
/Go/go1.19.2/src/runtime/slice.go:307 +0x0
strconv.formatBits()
/Go/go1.19.2/src/strconv/itoa.go:196 +0x26d
strconv.AppendUint()
/Go/go1.19.2/src/strconv/itoa.go:54 +0x1a4
github.com/rs/zerolog/internal/json.Encoder.AppendUint64()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/internal/json/types.go:281 +0x19e
github.com/rs/zerolog.Context.Uint64()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/context.go:290 +0x56
main.SetLogVar.func1()
/Projects/delegation_poker/main.go:62 +0x131
github.com/rs/zerolog.(*Logger).UpdateContext()
/Go/pkg/mod/github.com/rs/zerolog@v1.28.0/log.go:288 +0x29d
main.SetLogVar()
/Projects/delegation_poker/main.go:60 +0x95
main.main.func1()
/Projects/delegation_poker/main.go:47 +0x14f
main.main.func2()
/Projects/delegation_poker/main.go:53 +0x47
Goroutine 7 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
Goroutine 8 (running) created at:
main.main()
/Projects/delegation_poker/main.go:39 +0x65c
==================
{"level":"debug","string":"xxx","ID":41240081,"time":"2022-10-08T13:04:32+02:00","caller":"/Projects/delegation_poker/main.go:50","message":"Test message"}
{"level":"debug","string":"xxxxxxxxxxx","ID":41240847,"time":"2022-10-08T13:04:32+02:00","caller":"/Projects/delegation_poker/main.go:50","message":"Test message"}
Found 4 data race(s)
exit status 66
I dug into your example, and I think I now understand what is happening.
You share the one logger created in main()
with several hundred goroutines. Those concurrently write/change data in the logger's context without synchronisation. Thus, data races occur, which leads to garbled output.
The documentation of UpdateContext states that it should be used with caution:
Use this method with caution. If unsure, prefer the With method.
Perhaps creating child loggers with With would be better? This depends on your specific use case. Maybe something like this:
go func(log *zlog.Logger) {
var1 := uint64(41240000 + rand.Intn(1000))
var2 := strings.Repeat("x", rand.Intn(12))
localLogger := log.With().
Str("string", var2).
Uint64("ID", var1).
Logger()
ctx := localLogger.WithContext(context.Background())
zlog.Ctx(ctx).Debug().Msg("Test message")
wg.Done()
}(zlog.Ctx(ctx))
A Logger is not thread safe. Pass copies and not a pointer to goroutines.
But I'm creating new context as context.Background and associate with him a logger. And I expect that UpdateContext will update new context instance. How I can copy logger instance before create gorutine?
You don't need context:
func main() {
// Setup zerolog
logger = zlog.New(os.Stderr).With().Timestamp().Caller().Logger().Level(zlog.DebugLevel)
wg := sync.WaitGroup{}
for f := 0; f < 500; f++ {
wg.Add(1)
// Create gorutine
go func(logger zlog.Logger) {
// Update logger in gorutine context
logger = SetLogContext(logger, uint64(41240000+rand.Intn(1000)), strings.Repeat("x", rand.Intn(12)))
// Use logger
logger.Debug().Msg("Test message")
wg.Done()
}(logger)
}
wg.Wait()
}
func SetLogContext(logger zlog.Logger, var1 uint64, var2 string) {
return logger.With().Str("string", var2).Uint64("ID", var1).Logger()
}
But what if I have many functions that call each other and only the first function add to logger any variable? Should I pass logger through all functions?
func FirstFunc() {
logger = SetLogContext(logger, uint64(41240000+rand.Intn(1000)), strings.Repeat("x", rand.Intn(12)))
SecondFunc(logger)
}
func SecondFunc(logger *zerolog.Logger) {
// Use logger
logger.Debug().Msg("Test message from SecondFunc")
AnotherFunc(logger)
}
func AnotherFunc(logger *zerolog.Logger) {
// Use logger
logger.Debug().Msg("Test message from AnotherFunc")
}
My teammates and I experienced the same problem and independently arrived at the same solution before I found this issue report. We are happy calling .With()
to create a child logger before setting any fields from within a child goroutine. However, in order to diagnose the problem and arrive at this solution, we found it necessary to trace the zerolog library using a debugger.
I feel it shouldn't be necessary to browse the issues reports or use a debugger to learn the proper use of this library with concurrency. Would it be possible to add a note about this to the README to assist future users?
A Logger is not thread safe. Pass copies and not a pointer to goroutines.
@rs short question: How is it then safe to use the global Logger? (e.g. log.Debug()
from "github.com/rs/zerolog/log"
)
@sruehl You always get a copy of the logger. It can’t be change underneath you. When stored in a context, it is stored as a pointer so you can update the logger. It was probably a design mistake on my part.
In the case of the OP's code sample, goroutines and pointers were created directly.
In my particular case, neither pointers nor goroutines were directly used. From within a request handler, it looked something like:
logger := log.Logger.Level(zerolog.InfoLevel)
logger.UpdateContext(...)
This seemed to be a perfectly reasonable thing to do. No pointers were created (directly) and no goroutines were created (directly). However:
UpdateContext
uses a pointer receiverI noticed the following inside the godoc for UpdateContext
:
Use this method with caution. If unsure, prefer the With method.
So actually, there is a warning about this. However, I was confused because the warning did not say specifically why caution is required, when not to use UpdateContext
(or when it can be safely used), and what might go wrong otherwise.
I was wondering if it would be possible to make the warning in the godoc more specific, something like.
UpdateContext is not concurrency safe. Concurrent goroutines will overwrite a shared context. Use the With method to create a child logger before modifying the context from concurrent goroutines.
Updating the global logger from the main goroutine before starting any additional goroutines would be safe. I don't know if it's necessary to mention this in the documentation, though.
FYI, even if UpdateContext
were to use a value receiver, I still don't think it would be adequate protection because value receivers only make a shallow copy, the specific implementation of adding fields uses append()
to modify the Logger.context []byte
, and append
can modify the contents of the array in-place if sufficient capacity is allocated. However, With()
makes a copy of the Logger.context []byte
, which is complete protection.
Good idea, feel free to open a PR.
PR opened (see automated comment, above). Does someone have a chance to review it? Thanks!
Example wrong output:
Go playground link