Logging in Go With Zap

2020-06-15 • edited 2020-06-22

I'm still getting used to developing native applications, and Go logging was a challenge for me: How do I get different logging output in tests and production?

Writing a human-facing CLI application, here is what I want from logging:

  • log levels;

  • human-readable messages, always;

  • high verbosity during tests, including timestamps;

  • lower verbosity in production, only actionable output.

Go's built-in logging does not seem the have all of these features, or maybe I did not see them due to its rather arcane API.

A quick search leads to zap, "blazing fast, structured, leveled logging in Go". Test logging works immediately as advertised:

logger, _ := zap.NewDevelopment()
logger.Sugar().Debugf("Parsed config: %+v", config)

//> 2020-06-15T21:41:06.109+0200	DEBUG	app.go:77	Parsed config: { ... }

Very cool, that is exactly what we need in test logs! Production logging, on the other hand, is not an immediate fit:

logger, _ = zap.NewProduction()
logger.Warn("Oh no!")

//> {"level":"warn","ts":1592250230.533966,"caller":"app.go:77","msg":"Oh no!"}

While JSON logs are very useful when running services with log aggregation, we need something else for CLI tools. Now, NewProduction does take Option parameters, but I could not figure out how to create these values.

So here is how you can set up a slightly modified production logger that writes console format with formatted time stamps:

prodConfig := zap.NewProductionConfig()
prodConfig.Encoding = "console"
prodConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
prodConfig.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
logger, _ := prodConfig.Build()
logger.Warn("Oh no!")

//> 2020-06-15T21:49:16.513+0200	warn	app.go:77	Oh no!

The documentation of Config is helpful and it seems easy to customize the logger even further.

Tip

This may be obvious for veterans of native development, but for Java-land people like me this seems odd: you "configure" things like your logger by re-compiling your application. Makes sense — there is no JIT to clean away debug code, after all!

I created a runnable example based on the above: Gist

Of course, you can also use dynamic state, at the cost of fewer potential for compiler optimization. For example, I have pivoted to using an environment variable in container-do (and trimmed down the output even more).

ProgrammingGoLibrary
Comment & Share on Twitter 

Control SVG Groups in LaTeX

Sane YAML With JSON Schema