Skip to content

Zap

Posted on:July 9, 2024 at 04:32 PM
Share on

When we develop a project, maybe sometimes we need to code a log package ourselves, and we can use the popular open source log project such as the standard golang log package, glog, zap which is open source project development by uber, logrus etc. So let’s dive into these packages now!

Table of contents

Open Table of contents

Youtube Video

Watch My Youtube video

Zap

Zap is Uber’s open source log package, which is famous for its high performance. Many companies’ log packages are modified based on zap. In addition to the basic functions of logging, zap also has many powerful features:

Zap Usage

Basic Usage

The usage of zap is similar to that of other log packages. The following is a common usage:

package main

import (
	"time"

	"go.uber.org/zap"
)

func main() {
	logger, _ := zap.NewProduction()
	defer logger.Sync() // flush bugger , if any
	url := "https://maloong.com"
	logger.Info("failed to fetch URL", zap.String("url", url), zap.Int("attempt", 3), zap.Duration("backoff", time.Second))

	sugar := logger.Sugar()
	sugar.Infow("failed to fetch URL", "url", url, "attempt", 3, "backoff", time.Second)
	sugar.Infof("Failed to fetch URL: %s", url)
}

run this command in terminal,

go run main.go

It’s the output:

{"level":"info","ts":1720529047.9479308,"caller":"zap/main.go:13","msg":"failed to fetch URL","url":"https://maloong.com","attempt":3,"backoff":1}
{"level":"info","ts":1720529047.94805,"caller":"zap/main.go:16","msg":"failed to fetch URL","url":"https://maloong.com","attempt":3,"backoff":1}
{"level":"info","ts":1720529047.948066,"caller":"zap/main.go:17","msg":"Failed to fetch URL: https://maloong.com"}

The default log output format is JSON, and the file name and line number are recorded.

The above code creates a logger through zap.NewProduction(). Zap also provides zap.NewExample() and zap.NewDevelopment() to quickly create a logger. Loggers created by different methods have different settings. Example is suitable for test code, Development is used in development environment, and Production is used in production environment. If you want to customize the logger, you can call the zap.New() method to create it. The logger provides Debug, Info, Warn, Error, Panic, Fatal and other methods to record logs of different levels. When the program exits, be sure to call defer logger.Sync() to refresh the cached logs to the disk file.

When we have high requirements for log performance, we can use Logger instead of SugaredLogger. Logger has better performance and less memory allocation. In order to improve performance, Logger does not use interface and reflection, and Logger only supports structured logs, so when using Logger, you need to specify the specific type and key-value format of the log field, for example:

	logger.Info("failed to fetch URL", zap.String("url", url), zap.Int("attempt", 3), zap.Duration("backoff", time.Second))

If you think the log format of Logger is too complicated, you can use the more convenient SugaredLogger. Call logger.Sugar() to create SugaredLogger. SugaredLogger is simpler to use than Logger, but its performance is about 50% lower than Logger. It can be used in functions with low call counts. The calling method is as follows:

	sugar := logger.Sugar()
	sugar.Infow("failed to fetch URL", "url", url, "attempt", 3, "backoff", time.Second)
	sugar.Infof("Failed to fetch URL: %s", url)

Custom Logger

You can use the NexExample()/NewDevelopment()/NewProduction() functions to create a default Logger. Each method creates a different Logger configuration. You can also create a customized `Logger“ as follows:

package main

import (
	"encoding/json"

	"go.uber.org/zap"
)

func main() {
	rawJSON := []byte(`{
  "level":"debug",
  "encoding":"json",
  "outputPaths":["stdout","test.log"],
  "errorOutputPaths":["stderr"],
  "initialFields":{"name":"maloong"},
  "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("server start work successfully!")
}

The above example calls the Build method of zap.Config to create a Logger that outputs to standard output and the file test.log.

run this command in terminal,

go run main.go

It’s the output:

{"level":"info","message":"server start work successfully!","name":"maloong"}

and the test.log looks like this:

 {"level":"info","message":"server start work successfully!","name":"maloong"}

The zap.Config is defined as follows:

type Config struct {
    Level AtomicLevel
    Development bool
    DisableCaller bool
    DisableStacktrace bool
    Sampling *SamplingConfig
    Encoding string
    EncoderConfig zapcore.EncoderConfig
    OutputPaths []string
    ErrorOutputPaths []string
    InitialFields map[string]interface{}
}

Config structure, the fields are described as follows:

Call the Build() method of zap.Config to create a Logger using the zap.Config configuration.

EncoderConfig is the encoding configuration:

type EncoderConfig struct {
    MessageKey    string `json:"messageKey" yaml:"messageKey"`
    LevelKey      string `json:"levelKey" yaml:"levelKey"`
    TimeKey       string `json:"timeKey" yaml:"timeKey"`
    NameKey       string `json:"nameKey" yaml:"nameKey"`
    CallerKey     string `json:"callerKey" yaml:"callerKey"`
    FunctionKey   string `json:"functionKey" yaml:"functionKey"`
    StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
    LineEnding    string `json:"lineEnding" yaml:"lineEnding"`
    EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
    EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
    EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
    EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
    EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
    ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Commonly used settings are as follows:

Options

zap supports a variety of options, which can be used as follows:

package main

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

func main() {
	logger, _ := zap.NewProduction(zap.AddCaller())
	defer logger.Sync()

	logger.Info("Hi, I'm maloong")
}

run this command in terminal,

go run main.go

It’s the output:

{"level":"info","ts":1720530921.158103,"caller":"zap/main.go:11","msg":"Hi, I'm maloong"}

The above log outputs the call information of the log (file name: line number) “caller”: “zap/main.go:11”. Zap provides multiple options to choose from:

Preset Log Fields

If you want to add some common fields to each log, such as requestID, you can use the Fields(fs ...Field) option when creating a Logger. In the following code, the requestID and userID common fields are added to each log:

package main

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

func main() {
	logger := zap.NewExample(zap.Fields(zap.Int("userId", 10), zap.String("requestId", "maloon_request_123")))
	logger.Debug("This is a debug message")
	logger.Info("This is a info message")
}

run this command in terminal,

go run main.go

It’s the output:

{"level":"debug","msg":"This is a debug message","userId":10,"requestId":"maloon_request_123"}
{"level":"info","msg":"This is a info message","userId":10,"requestId":"maloon_request_123"}

Global Logger

Zap provides two global Loggers, which are convenient for us to call:

The default global Logger does not record any logs. It is a useless Logger. For example, zap.L() returns a Logger named _globalL, which is defined as:

_globalL  = NewNop()
func NewNop() *Logger {
    return &Logger{
        core:        zapcore.NewNopCore(),
        errorOutput: zapcore.AddSync(ioutil.Discard),
        addStack:    zapcore.FatalLevel + 1,
    }
}

The zapcore.NewNopCore() function is defined as:

type nopCore struct{}

// NewNopCore returns a no-op Core.
func NewNopCore() Core                                        { return nopCore{} }
func (nopCore) Enabled(Level) bool                            { return false }
func (n nopCore) With([]Field) Core                           { return n }
func (nopCore) Check(_ Entry, ce *CheckedEntry) *CheckedEntry { return ce }
func (nopCore) Write(Entry, []Field) error                    { return nil }
func (nopCore) Sync() error                                   { return nil }

// NewCore creates a Core that writes logs to a WriteSyncer.
func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core {
    return &ioCore{
        LevelEnabler: enab,
        enc:          enc,
        out:          ws,
    }
}

You can see that NewNop() creates a Logger that does not record any logs, any internal errors, and does not execute any hooks. You can use the ReplaceGlobals function to replace the global Logger with the Logger we created, for example:

package main

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

func main() {
	zap.L().Info("default global Logger")
	zap.S().Info("default global SugaredLogger")

	logger := zap.NewExample()
	defer logger.Sync()

	zap.ReplaceGlobals(logger)
	zap.L().Info("replaced global logger")
	zap.S().Info("replaced global SugaredLogger")
}

run this command in terminal,

go run main.go

It’s the output:

{"level":"info","msg":"replaced global logger"}
{"level":"info","msg":"replaced global SugaredLogger"}

You can see that the log before zap.ReplaceGlobals(logger) is not printed.

Share on