Coding Bits
July 8th, 2024

Zerolog's API Mistake

Moan-routine

I'll be honest, I was expecting a lot more moan-routine posts than I've written to date. Guess I've been in a positive mood. That is, until I started using Zerolog again this morning.

Zerolog is a Go logging package that we use at work. It's pretty successful, and all in all a good logger. But they made a fundamental mistake in their API which trips me up from time to time: they're not consistent with their return types.

When you want to create a new logger, you get a zerolog.Logger instance. This is also returned when you're creating a new logger from an existing one, say with some extra string attributes. All good. But, when you want to retrieve a logger from the context, it returns a *zerolog.Logger instance.

This makes it difficult to write functions that accept logger instances from arbitrary sources. If there's any chance that you need to supply a logger from a context, you find yourself writing parameters with the *zerolog.Logger type. But that makes passing in new logs annoying, as you'd have to instantiate the log into a variable, then pass that variable address through. You could use zerolog.Logger instead, but then you've got the opposite problem, where you'll need to dereference the logger returned from the context.

In short, this is impossible:

func myFunc(log ???) {
  // do something
}

myFunc(zerolog.New(os.Stderr))
myFunc(zerolog.Ctx(ctx))

And yeah, in the grand scheme of things, this is not a huge mistake. And usually I'm passing in logs exclusively from the context, so *zerolog.Logger works just fine most of the time. But I'm never comfortable with deciding which is the right type to use, and when I get it wrong, and I resort to using temporary variables, it just leaves the code looking untidy.

So please, when you're writing an API that returns a specific entity, be consistent in your types. Either return the struct itself, or the pointer. I don't care which one you use, but please, just choose one.