author
Kevin Kelche

A Hands-On Approach to Zap - Structured Logging in Go


Introduction

Zap is a high-performance library for Go that offers a fast and efficient way to log messages in Go applications. It is a structured logger, which means that it logs messages in a format, such as JSON, which can be easily parsed by other tools. With its fast performance, it allows simple logging without impacting the performance of your application.

Zap has two main components: the core library and the sugared logger. The core library is a low-level API that provides a fast and efficient way to log messages. The sugared logger is a high-level API that provides a more flexible and convenient way to log messages. It is a wrapper around the core library that provides a more familiar API to developers coming from other languages.

In this article, we will explore logging with zap, starting from the basics and moving on to more advanced topics. We will take a hands-on approach, providing step-by-step instructions, and real-world examples to help you get started with zap. Let’s get started!

Getting Started

To use Zap, you need to install the package into your project. You can do this by running the following command:

terminal
go get go.uber.org/zap

Copied!

Once you have installed the package, you can import it into your project by adding the following line to your code:

import "go.uber.org/zap"

Copied!

Next, you need to initialize Zap in your code. This can be done using the zap.New function, which returns a new logger instance. By default, it will log messages to the standard output and uses a JSON encoder.

Some predifined configurations are available to help you get started quickly. These configurations are:

Here is an example of how to initialize Zap:

logger, err := zap.NewProduction()
if err != nil {
    log.Fataf("failed to initialize zap logger: %v", err)
}

defer logger.Sync()

Copied!

This code snippet instantiates a new logger and checks for any errors. If any errors are found, the program will log the error and exit. The defer keyword ensures that the logger is synced when the program exits. With this code in place, you can now start logging messages.

Logging Basics

With the setup complete, you can now start logging messages. Logging in zap is straightforward to use. The example below shows how to log a message with the Info level:

logger.Info("Go can be fun!")

Copied!

The Info method takes a string as an argument, which is the message to be logged. The message will be logged with the Info level, which is the default level in zap.NewProduction().

Adding Fields to Log Messages

logger.Info("Go can be fun!", zap.String("level", "info"))

Copied!

The Info method also takes a variadic list of key-value pairs as an argument. The key-value pairs are used to add additional information to the log message. In the example above, the key-value pair zap.String("level", "info") is added to the log message. The zap.String function is used to create a key-value pair with a string value. The first argument is the key, and the second argument is the value.

Some other functions that can be used to create key-value pairs are:

logger.Info("Go can be fun!", zap.String("level", "info"), zap.Int("count", 1), zap.Bool("is_active", true), zap.Float64("price", 10.99))

Copied!

Output
{"level":"info","ts":1675760781.7614264,"caller":"zap-logging/init.go:21","msg":"Go can be fun!","level":"info","count":1,"is_active":true,"price":10.99}

Copied!

Logging Levels

Just like other logging libraries, zap supports different logging levels. The logging levels are used to indicate the severity of the log message. The logging levels are:

The logging levels are ordered from least to most severe. The Debug level is the least severe, and the Fatal level is the most severe. The Fatal level is the highest, and it will exit the program after logging the message.

Setting the Logging Level

By default, the logging level is set to Info. This means that only messages with the Info level and above will be logged. You can change the logging level by using the SetLevel method. The SetLevel method is a member of the AtomicLevel type and can be accessed using the Level field of the logger instance.

logger.SetLevel(zap.DebugLevel)

Copied!

The SetLevel method takes a zapcore.Level enum as an argument. The zapcore.Level type is an alias for the int8 type. The zapcore.Level type is used to represent the logging levels. The zapcore.Level type has the following constants:

Example:

main.go
package main

import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"

)

func main() {
    atomic := zap.NewAtomicLevel()

    atomic.SetLevel(zapcore.ErrorLevel)

    logger := zap.New(
        zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
        zapcore.Lock(os.Stdout),
        atomic,
    )

    logger.Info("I am disabled")
    logger.Error("I am enabled")
}

Copied!

In this example, we declare a new AtomicLevel instance and set the logging level to ErrorLevel. We then create a new logger instance and pass the configuration to it. The Info method will not be logged, but the Error method will be logged.

Customizing the Log Format

The zap.NewProductionEncoderConfig function returns a zapcore.EncoderConfig instance. This instance is used to configure the log format. The zapcore.EncoderConfig type has the following fields:

among others.

We could change the time format by setting the TimeKey field to time and the EncodeTime field to zapcore.ISO8601TimeEncoder. The EncodeTime field is a function that takes a time.Time instance and returns a string instance. The zapcore.ISO8601TimeEncoder function is used to encode the time in the ISO8601 format.

main.go
package main

import (
  "os"

  "go.uber.org/zap"
  "go.uber.org/zap/zapcore"
)

func main() {
  atomic := zap.NewAtomicLevel()

  atomic.SetLevel(zapcore.DebugLevel)

  enconfig := zap.NewProductionEncoderConfig()
  enconfig.EncodeTime = zapcore.ISO8601TimeEncoder
  enconfig.TimeKey = "timestamp"

  logger := zap.New(zapcore.NewCore(
    zapcore.NewJSONEncoder(enconfig),
    zapcore.Lock(os.Stdout),
    atomic,
  ))

  logger.Debug("Change time format")
  logger.Info("Change time format")

  logger.Sync()

}

Copied!

Logging to a File

Logging to a file is very similar to logging to the console. The only difference is that you need to create a file and pass it to the logger. The example below shows how to log to a file:

main.go
package main

import (
  "os"

  "go.uber.org/zap"
  "go.uber.org/zap/zapcore"
)

func main() {
  file, err := os.OpenFile("log.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
  if err != nil {
    panic(err)
  }
  defer file.Close()

  logger := zap.New(
    zapcore.NewCore(
      zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
      zapcore.Lock(file),
      zapcore.DebugLevel,
    ),
  )

  logger.Info("Hello World!")
  logger.Info("Go can be fun!", zap.String("level", "info"))
  logger.Info("Go can be fun!", zap.String("level", "info"),
    zap.Int("count", 1), zap.Bool("is_active", true), zap.Float64("price", 10.99))

}

Copied!

As you can see instead of passing the os.Stdout to the zapcore.Lock function, we pass the file instance. The zapcore.Lock function is used to lock the file so that only one goroutine can write to the file at a time.

Logging to a File and the Console

You can log to both a file and the console at the same time. To log to both file and the console, we use the zapcore.NewTee function. This function takes a list of zapcore.Core instances as arguments. The zapcore.NewTee function returns a new zapcore.Core instance. We then pass this instance to the zap.New function.

main.go
package main

import (
  "log"
  "os"

  "go.uber.org/zap"
  "go.uber.org/zap/zapcore"
)

func main() {
  file, err := os.OpenFile("log.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
  if err != nil {
    log.Fatalf("Could not open file %v", err)
  }

  defer file.Close()

  logger := zap.New(
    zapcore.NewTee(
      zapcore.NewCore(
        zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
        zapcore.Lock(file),
        zapcore.DebugLevel,
      ),
      zapcore.NewCore(
        zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
        zapcore.Lock(os.Stdout),
        zapcore.DebugLevel,
      ),
    ),
  )

  logger.Info("We are logging to file and stdout")
  logger.Info("We are logging to file and stdout", zap.String("level", "info"))
  logger.Info("We are logging to file and stdout", zap.String("level", "info"),
    zap.Int("count", 1), zap.Bool("is_active", true), zap.Float64("price", 10.99))
}

Copied!

Defining Configurations with JSON

Zap allows you to define configurations in different formats such as JSON or YAML. This is passed to a zap.Config instance.

main.go
package main

import (
  "encoding/json"

  "go.uber.org/zap"
)

func main() {
  jsonData := []byte(`{
    "level": "debug",
    "encoding": "json",
    "outputPaths": ["stdout", "log.txt"],
    "errorOutputPaths": ["stderr"],
    "initialFields": {"foo": "bar"},
    "encoderConfig": {
        "messageKey": "message",
        "levelKey": "level",
        "levelEncoder": "lowercase"
    }
    }`)

  configs := zap.Config{}
  if err := json.Unmarshal(jsonData, &configs); err != nil {
    panic(err)
  }

  logger, err := configs.Build()
  if err != nil {
    panic(err)
  }

  logger.Info("Configured logger")
  logger.Warn("Configured logger", zap.String("level", "info"))
}

Copied!

In the example above, we define a JSON string that contains the configuration for the logger. The JSON string is unmarshalled into a zap.Config instance. The zap.Config instance is then passed to the zap.Config.Build function. This function returns a *zap.Logger instance.

Using these alternative formats of passing configurations to the logger, allows you to create your configuration files and pass them to the logger.

Using the Suggered Logger

The suggered logger is a wrapper around the *zap.Logger instance. It provides a simpler API for logging. The suggered logger is created by calling the zap.Suggered function. This can be used in applications where performance is not a concern though it is faster than its other structured logging counterparts.

main.go
package main

import (
    "go.uber.org/zap"
)

func main() {
    logger := zap.NewExample()
    defer logger.Sync()

    suggeredLogger := logger.Sugar()

    suggeredLogger.Infow("Failed to fetch URL",
        "url", "https://example.com",
        "attempt", 3,
        "backoff", 1,
    )

    suggeredLogger.Infof("Failed to fetch URL: %s", "https://example.com")
}

Copied!

Suggered logging is more convenient than zapcore logging, It is great when you want to quickly add logging to your application.

Conclusion

In this article, we covered the basics of logging with Zap, including creating a logger, logging to a file/console, defining configurations with JSON, and using the sugared logger. With this hands-on guide, you now have a solid understanding of how to use Zap logging in your Go projects. Whether you opt for default configurations or customize, Zap is an excellent choice for efficient logging in Go applications.

Subscribe to my newsletter

Get the latest posts delivered right to your inbox.