Many of the code bases I have seen inject a logger into context.Context. While this is an anti-pattern, the reason for this is that there are request-scoped values that they want present with every log message.
- Request or Trace IDs for correlation
- User IDs for quick debugging
- IP address
- Host IP
- Instance ID
A quick (but not thorough) Google search returns a few examples in the Go community that encourage injecting a logger into context.Context for this reason.
The way I usually see this implemented is that a middleware creates a contextual logger and injects that into context.Context. When a dependency needs a logger, it extracts it from context.Context.
log := ctx.Value("logger").(log.Logger)
log.Warn("something you'll ignore later")
A couple of disadvantages of the above snippet that bother me:
context.Context they can be tedious to mock for tests.There are posts and forum threads that explain the disadvantages to injecting service dependencies via context.Context
One gap I see is that logging libraries and community posts don't demonstrate how we can inject a logger as a (decoupled) service dependency and write log messages with request-scoped values without passing a logger around and/or repeating arguments.
I propose that we wrap existing logging libraries to change their APIs slightly for real-world use-cases.
context.Context as an input parameter and extract logging context from the given context.Context.The overhead for logging can be significant so the implementation matters, but I set out with the hope that this behaviour could be added at a minimal cost to performance.
I decided to start with Uber's zap which is the fastest logging library I knew of at the time. I specifically focused on the zap.SugaredLogger.
My first iteration of this idea was to wrap zap.SugaredLogger, and for each method accept a context.Context and configure my wrapper to search context.Context for specific keys, one for each possible request-scoped value we want in logging context. This was slow.
log.WithFieldscontext.Context.context.Context and allocated a new slice of all found keys and their values. I merged the request-scoped fields with dynamic arguments and then logged the message.My second iteration, I decided a middle-ground would probably be more appropriate.
context.Context.log.WithFields or similar and the returned contextual logger is injected in context.Context.context.Context, my logger writes using the contextual logger. Otherwise, it defaults to itself.This put the performance essentially on-par with the underlying zap logger in a happy-path use-case and enabled me to break away from using context.Context for dependency injection.
The main improvement that my implementation offers is in the use-case where there is no logger in context.Context.
context.Context to pass along service dependencies, which is what I want to avoid.goos: darwin
goarch: amd64
pkg: github.com/ahmedalhulaibi/loggy
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkLoggy-8 60992144 394.0 ns/op 280 B/op 4 allocs/op
BenchmarkZap-8 61309858 390.8 ns/op 280 B/op 4 allocs/op
BenchmarkLoggy_NoLoggingContext-8 313296274 73.27 ns/op 0 B/op 0 allocs/op
BenchmarkZap_NoLoggingContext-8 20103192 1208 ns/op 1200 B/op 30 allocs/op
PASS
ok github.com/ahmedalhulaibi/loggy 105.226s
Some details on the benchmarks:
Nop logger and do not write any log messages since the purpose is to benchmark the overhead required to create/access the logging context.BenchmarkLoggy and BenchmarkZap both assume logging context already exists.BenchmarkLoggy_NoLoggingContext and BenchmarkZap_NoLoggingContext both assume that a logging context does not exist.I plan on following up with a benchmark use-case an HTTP API with middleware, but I expect similar results.
Check it out and let me know if my methods were flawed or if you have any thoughts on how to improve the tests or library! https://github.com/ahmedalhulaibi/loggy