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.
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). |