Level-based logging in Go with Uber Zap

Take logging into another level

Siraphob K.
CodeX
5 min readJan 8, 2022

--

featured image

Table of Contents

  • To log or not to log
  • Go’s built-in logging package
  • What is Level-Based Logging?
  • Introducing “Uber Zap: Blazing fast, structured, leveled logging in Go.”
  • Example Logging with Zap
  • Summary

To log or not to log

The benefits of logging

Most web servers in the tech industry adopt logging. Logging has some benefits when it comes to a web application.

  • Logging helps developers spot errors and performance issues
  • Logs could be reconstructed and used for system auditing

What should be logged

Actually, it’s anything that makes the bug spotting and debugging process easier. A log message may contain useful information for debugging such as timestamp, error message, etc.

What shouldn’t be logged

Security information is not encouraged for logging especially in a production environment. For example, you shouldn’t log credit card information, the personal information of a user, etc. You get the idea. Because if those logs leak, they could be exploited by hackers.

Go’s built-in logging package

Go provides developers with a built-in logging package. It can be used to easily log anything into the standard output or a file.

Basic logging example

The following code shows logging with Go’s built-in logging library.

package mainimport "log"func main() {
log.Println("This is logging!")
}

The above code produces the following result to the terminal. You can see that there’s date and time information along with the log message.

2022/01/08 14:17:45 This is logging!

You can also log messages into a file! Please copy the following code and run.

package mainimport (
"log"
"os"
)
func main() {
logFile := openLogFile()
defer logFile.Close()
log.SetOutput(logFile)
log.Println("This is logging!")
}
func openLogFile() *os.File {
f, err := os.OpenFile("access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
return f
}

The above code produces a file called access.log . The file will contain a message “2022/01/08 14:22:51 This is logging!”.

What’s wrong with Go’s built-in library?

One major flaw of the built-in logging library is it’s rather too simple. It’s good for quick development when the developer wants to see the result as fast as they can. But when it comes to a production environment. We usually want something more structured rather than simple text messages.

What do you mean something more structured?” you may ask. We can structure logs by categorizing them into levels which is usually called Level-Based Logging.

What is Level-Based Logging?

An application usually prints many types of logging information. There may be debugging messages, errors, warnings, general info, and so on. All that information slamming into our faces may cause a problem when it comes to debugging or error spotting. Luckily, there is a concept of prioritizing log messages into different severity levels. When logs are categorized into levels, we can filter them to see only the levels we want to analyze.

For example, an error message may have a higher priority than a warning message then we could filter all the warnings out to see only the errors.

Introducing “Uber Zap: Blazing fast, structured, leveled logging in Go.”

Zap is an open-source project developed by Uber. It is a logging framework that is “Blazing fast”. You can see the performance & benchmark information from their repository.

Zap supports seven types of log levels which are Debug, Info, Warning, Error, DPanic, Panic, and Fatal. The description of each level is described below.

const (
// DebugLevel logs are typically voluminous, and are usually disabled in
// production.
DebugLevel = zapcore.DebugLevel
// InfoLevel is the default logging priority.
InfoLevel = zapcore.InfoLevel
// WarnLevel logs are more important than Info, but don't need individual
// human review.
WarnLevel = zapcore.WarnLevel
// ErrorLevel logs are high-priority. If an application is running smoothly,
// it shouldn't generate any error-level logs.
ErrorLevel = zapcore.ErrorLevel
// DPanicLevel logs are particularly important errors. In development the
// logger panics after writing the message.
DPanicLevel = zapcore.DPanicLevel
// PanicLevel logs a message, then panics.
PanicLevel = zapcore.PanicLevel
// FatalLevel logs a message, then calls os.Exit(1).
FatalLevel = zapcore.FatalLevel
)

Example Logging with Zap

Disclaimer: I’m not gonna dig into much details about how to use the package. I want to show some basic examples of what you can do with Zap. You can find detailed documentation here.

Logging with a preset logger

Normally, you’d have to configure a logger before using it. What if we just want to use it right away? Fortunately, Zap offers a preset logger for convenient use. See the example code below.

package mainimport (
"log"
"os"
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewDevelopment()
defer logger.Sync()
logger.Info("Hello Zap!")
logger.Warn("Beware of getting Zapped! (Pun)")
logger.Error("I'm out of Zap joke!")
}

From the above code, zap.NewDevelopment() returns a preset development logger which prints messages in a human-readable format. After that, we use methods Info , Warn , and Error to print different log levels. When you execute the code, it produces the following result.

2022-01-08T15:33:54.504+0700    INFO    logging-zap/main.go:13  Hello Zap!2022-01-08T15:33:54.504+0700    WARN    logging-zap/main.go:14  Beware of getting Zapped! (Pun)
main.main
/home/siraphob-wsl-ubuntu/go/src/github.com/copsterr/logging-zap/main.go:14
runtime.main
/usr/local/go/src/runtime/proc.go:255
2022-01-08T15:33:54.504+0700 ERROR logging-zap/main.go:15 I'm out of Zap joke!
main.main
/home/siraphob-wsl-ubuntu/go/src/github.com/copsterr/logging-zap/main.go:15
runtime.main
/usr/local/go/src/runtime/proc.go:255

For INFO, a normal log message is printed. It contains a timestamp, log-level, code location, and a message.

For WARN and ERROR, they also print out the stack traces too. This is useful for detecting where the error occurs.

Configuring a logger

Sometimes, preset loggers don’t suit your needs. Zap allows you to configure your own logger. You can define logger configuration in a JSON or YAML file then parse it into a Zap logger. The example is shown below.

package mainimport (
"encoding/json"
"log"
"os"
"go.uber.org/zap"
)
func main() {
rawJSON := []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout"],
"errorOutputPaths": ["stderr"],
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("Hi, custom logger!")
logger.Warn("Custom logger is warning you!")
logger.Error("Let's do error instead.")
}

The above code produces the following result.

{"level":"info","message":"Hi, custom logger!"}
{"level":"warn","message":"Custom logger is warning you!"}
{"level":"error","message":"Let's do error instead."}

As you can see, the logger produces JSON outputs so it’d be better if we put them into a log file. We can achieve that by modifying the logger configuration as shown below.

rawJSON := []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout, "/my.log"], <-- This line
"errorOutputPaths": ["stderr", "/my.log"], <-- This line
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)

From the above snippet, you can see that the key outputPaths and errorOutputPaths has a file my.log as an element of their value array. When you execute the code, it will produce a my.log file with log messages inside.

This is just the tip of the iceberg. You can do so much more with Zap. Check out their documentation for more information.

Summary

Logging allows developers to detect bugs and errors. Level-based logging helps filter only necessary information. In this article, I’ve shown you how to log by using Uber Zap. However, it may not be the best logger that suits your need. There are many logging frameworks for you to choose from. Using the logger correctly can improve software quality and increase productivity of the development process. I hope this article gives you some ideas about how to do logging in your project. Happy Coding!

--

--