← Home

Contextual Logging Pattern

By Ahmed Al-Hulaibi

Contextual logging

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:

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.

Better contextual logging

I propose that we wrap existing logging libraries to change their APIs slightly for real-world use-cases.

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.

My second iteration, I decided a middle-ground would probably be more appropriate.

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.

Benchmarks

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:

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