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
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:
- Supports common log levels, such as
Debug
,Info
,Warn
,Error
,DPanic
,Panic
,Fatal
. - Very high performance, zap has very high performance, suitable for scenarios with high performance requirements.
- Like logrus, supports structured logging.
- Supports preset log fields.
- Supports output of call stack for specific log levels.
- Supports hooks.
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:
Level
: Log level.Development
: Set the Logger mode to development mode.DisableCaller
: Disabled call information. When the value of this field is true, the function call information of the log will no longer be displayed in the log.DisableStacktrace
: Disabled automatic stack trace capture.Sampling
: Flow control configuration, also known as sampling. The unit is per second. The function is to limit the number of log outputs per second to prevent CPU and IO from being over-occupied.Encoding
: Specify the log encoder. Currently only two encoders are supported: console and json. The default is json.EncoderConfig
: Encoding configuration.OutputPaths
: Configure the log standard output. You can configure multiple log output paths. Generally, you can only configure the standard output or output to a file. If necessary, you can also configure both at the same time.ErrorOutputPaths
: Error output path, which can be multiple.InitialFields
: Initialization field configuration. The fields of this configuration will be printed in each log output in a structured form.
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:
MessageKey
: The key name of the message in the log, the default is msg.LevelKey
: The key name of the level in the log, the default is level.EncodeLevel
: The format of the level in the log, the default is lowercase, such as debug/info.
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:
AddStacktrace(lvl zapcore.LevelEnabler)
: Used to output the call stack at the specified level and above.zap.WithCaller(enabled bool)
: Specifies whether to add the file name and line number to the log output content.zap.AddCaller()
: Equivalent tozap.WithCaller(true)
, specifies to add the line number and file name to the log output content.zap.AddCallerSkip(skip int)
: Specifies the call depth to skip in the call stack, otherwise the line number obtained through the call stack may always be the line number in the log component.zap.IncreaseLevel(lvl zapcore.LevelEnabler)
: Increase the log level. If the lvl passed in is lower than the current logger level, the log level will not be changed.ErrorOutput(w zapcore.WriteSyncer)
: Specifies the output location when an exception occurs in the log component.Fields(fs ...Field)
: Add public fields.Hooks(hooks ...func(zapcore.Entry) error)
: Register a hook function to call the hook method when printing logs.WrapCore(f func(zapcore.Core) zapcore.Core)
: Replacezapcore.Core
ofLogger
.Development()
: Change Logger to Development mode.
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:
zap.Logger
: Available throughzap.L()
, providesDebug()
,Info()
,Warn()
,Error()
,Panic()
,DPanic()
,Fatal()
methods for logging.zap.SugaredLogger
: Available throughzap.S()
, providesDebugf()
,Debugw()
,Infof()
,Infow()
,Warnf()
,Warnw()
,Errorf()
,Errorw()
,Panicf()
,Panicw()
,DPanicf()
,DPanicw()
,Fatalf()
,Fatalw()
methods for logging.
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.