Zap是非常快的、结构化的,分日志级别的Go日志库。
根据Uber-go Zap的文档,它的性能比类似的结构化日志包更好,也比标准库更快。 以下是Zap发布的基准测试信息
记录一条消息和10个字段:
记录一个静态字符串,没有任何上下文或printf风格的模板:
Zap提供了两种类型的日志记录器:Sugared Logger和Logger。
SugaredLogger
。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。Logger
。它比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。什么是printf风格的日志记录?就是实现相同的日志描述,sugarLogger只要写更少的代码就能实现更清晰的描述功能。demo代码如下:
// 使用logger
logger.Info("Failed to fetch URL", zap.String("url", url))
logger.Debug("Retrying:", zap.Int("delay", delay))
// 使用sugarLogger
sugarLogger.Infof("Failed to fetch URL: %s", url)
sugarLogger.Debugf("Retrying in %d seconds...", delay)
zap.NewProduction()
: 创建一个适合在生产环境中使用的Logger。这个Logger会输出JSON格式的日志,包含时间戳和调用者信息。默认情况下,它会将InfoLevel及以上的日志写入标准输出。这个Logger的配置更注重性能和日志的机器解析性。zap.NewDevelopment()
: 创建一个适合在开发环境中使用的Logger。这个Logger的输出格式更适合人类阅读,而不是机器解析。它也包含了时间戳和调用者信息,但是默认情况下,它会将DebugLevel及以上的日志写入标准错误输出。这个Logger的配置更注重人类的可读性和错误的详细信息。zap.Example()
: 创建一个简单的Logger主要用于库的示例。这个Logger会将所有级别的JSON格式的日志写入标准输出,不包含时间戳和调用者信息。这个Logger配置主要用于演示库的基础功能,通常不会在生产或开发环境中使用。var logger *zap.Logger
func main() {
InitLogger()
defer func(logger *zap.Logger) {
//每个使用zap库的应用程序在结束前都应调用Logger.Sync(),来确保所有的日志都被写入目标设备。
//因为zap库为了提高性能,可能会缓存一些日志在内存中,而不是立即写入目标设备。
_ = logger.Sync()
}(logger)
simpleHttpGet("https://www.baidu.com")
simpleHttpGet("https://www.google.com")
}
func InitLogger() {
logger, _ = zap.NewProduction()
}
func simpleHttpGet(url string) {
resp, err := http.Get(url)
if err != nil {
logger.Error(
"Error fetching url..",
zap.String("url", url),
zap.Error(err))
} else {
logger.Info("Success..",
zap.String("statusCode", resp.Status),
zap.String("url", url))
_ = resp.Body.Close()
}
}
当发送请求错误时使用error级别的日志记录;当请求成功时使用info级别的日志记录。运行结果如下:
var sugarLogger *zap.SugaredLogger
func main() {
InitLogger()
defer func(sugarLogger *zap.SugaredLogger) {
_ = sugarLogger.Sync()
}(sugarLogger)
simpleHttpGet("https://www.baidu.com")
simpleHttpGet("https://www.google.com")
}
func InitLogger() {
logger, _ := zap.NewProduction()
sugarLogger = logger.Sugar()
}
func simpleHttpGet(url string) {
sugarLogger.Debugf("Trying to hit GET request for %s", url)
resp, err := http.Get(url)
if err != nil {
sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
} else {
sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
_ = resp.Body.Close()
}
}
运行结果如下:
我们将使用 zap.New() 方法来手动传递所有配置,而不是使用像 zap.NewProduction() 这样的预置方法来创建logger。logger常见完成要做的第一个更改是把日志写入文件,而不是打印到应用程序控制台。zap.New() 方法的原型如下:
func New(core zapcore.Core, options ...Option) *Logger
zapcore.Core 这个参数需要三个配置,分别是Encoder、WriteSyncer和LogLevel。
Encoder:编码器(如何写入日志)。这里使用开箱即用的 NewJSONEncoder(),zapcore.NewJSONEncoder()返回一个将日志消息编码为JSON格式的编码器。然后并使用预先设置的 NewProductionEncoderConfig(),这个NewProductionEncoderConfig()有如下配置可供选择:
MessageKey
: 默认为"msg",用于指定输出的消息的键名。LevelKey
: 默认为"level",用于指定输出的日志级别的键名。TimeKey
: 默认为"ts",用于指定输出的时间戳的键名。NameKey
: 默认为"logger",用于指定输出的日志记录器名称的键名。CallerKey
: 默认为"caller",用于指定输出的调用者信息的键名。StacktraceKey
: 默认为"stacktrace",用于指定输出的堆栈信息的键名。LineEnding
: 默认为"\n",用于指定行结束符。EncodeLevel
: 默认为zapcore.LowercaseLevelEncoder,用于指定日志级别的编码方式。EncodeTime
: 默认为zapcore.EpochTimeEncoder,用于指定时间戳的编码方式。EncodeDuration
: 默认为zapcore.SecondsDurationEncoder,用于指定时间持续期的编码方式。EncodeCaller
: 默认为zapcore.ShortCallerEncoder,用于指定调用者信息的编码方式。// 这里我们都使用默认的即可
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
WriterSyncer:指定日志将写到哪里去。我们使用 zapcore.AddSync() 函数并且将打开的文件句柄传进去。
file, _ := os.Create("./log/demo.log")
writeSyncer := zapcore.AddSync(file)
Log Level:哪种级别的日志将被写入。有以下几种日志级别:
DebugLevel
: 通常只在开发环境中使用,用于输出详细的调试信息。InfoLevel
: 适用于生产环境,用于记录关键的系统信息。WarnLevel
: 对可能存在问题的情况进行警告,但不会影响系统运行。ErrorLevel
: 在系统无法正常运行时进行记录,比如无法进行数据库连接、缺失必要的配置文件等。DPanicLevel
: 用于开发环境,当代码运行到绝不应该运行的部分时,记录Panic日志。在生产环境,不会引起Panic,只会记录错误。PanicLevel
: 与DPanicLevel相似,用于非开发环境,当代码运行到绝不应该运行的部分时,会引发Panic。FatalLevel
: 当系统无法运行时,记录致命错误,然后调用os.Exit。修改上述部分中的Logger代码:重写InitLogger()方法。
func InitLogger() {
encoder := getEncoder()
writeSyncer := getLogWriter()
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
logger := zap.New(core)
sugarLogger = logger.Sugar()
}
func getEncoder() zapcore.Encoder {
return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
func getLogWriter() zapcore.WriteSyncer {
file, _ := os.Create("./log/demo.log")
return zapcore.AddSync(file)
}
当使用这些修改过的logger配置调用上述部分的main()函数时,以下输出将打印在文件./log/demo.log中。
{"level":"debug","ts":1697091706.9003525,"msg":"Trying to hit GET request for https://www.baidu.com"}
{"level":"info","ts":1697091707.2923145,"msg":"Success! statusCode = 200 OK for URL https://www.baidu.com"}
{"level":"debug","ts":1697091707.2925751,"msg":"Trying to hit GET request for https://www.google.com"}
{"level":"error","ts":1697091728.6855457,"msg":"Error fetching URL https://www.google.com : Error = Get \"https://www.google.com\": dial tcp [2a03:2880:f126:83:face:b00c:0:25de]:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond."}
现在,我们希望将编码器从JSON Encoder更改为普通Encoder。其实只需要将NewJSONEncoder()更改为NewConsoleEncoder()即可。
func getEncoder() zapcore.Encoder {
return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}
最后的日志结果如下:
1.6970938827359314e+09 debug Trying to hit GET request for https://www.baidu.com
1.697093883124089e+09 info Success! statusCode = 200 OK for URL https://www.baidu.com
1.6970938831244597e+09 debug Trying to hit GET request for https://www.google.com
1.6970939045269628e+09 error Error fetching URL https://www.google.com : Error = Get "https://www.google.com": dial tcp 162.125.32.15:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
第一步:覆盖默认的ProductionConfig()
func getEncoder() zapcore.Encoder {
// 得到编码配置
encoderConfig := zap.NewProductionEncoderConfig()
// 通过配置修改时间编码规则
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// 通过配置添加调用者信息
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
第二步:修改zap logger代码,添加将调用函数信息记录到日志中的功能。也就是在zap.New()函数中添加一个Option。
logger := zap.New(core, zap.AddCaller())
最后的运行结果如下:
2023-10-12T15:11:29.098+0800 DEBUG zap/main.go:47 Trying to hit GET request for https://www.baidu.com
2023-10-12T15:11:29.624+0800 INFO zap/main.go:52 Success! statusCode = 200 OK for URL https://www.baidu.com
2023-10-12T15:11:29.624+0800 DEBUG zap/main.go:47 Trying to hit GET request for https://www.google.com
2023-10-12T15:11:51.053+0800 ERROR zap/main.go:50 Error fetching URL https://www.google.com : Error = Get "https://www.google.com": dial tcp [2a03:2880:f10d:83:face:b00c:0:25de]:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
当我们不是直接使用初始化好的logger实例记录日志,而是将其包装成一个函数等,此时日录日志的函数调用链会增加,想要获得准确的调用信息就需要通过AddCallerSkip函数来跳过。
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
有时候我们除了将全量日志输出到xxx.log文件中之外,还希望将ERROR级别的日志单独输出到一个名为xxx.err.log的日志文件中。我们可以通过以下方式实现。
func InitLogger() {
encoder := getEncoder()
// demo.log记录全量日志
logF, _ := os.Create("./log/demo.log")
c1 := zapcore.NewCore(encoder, zapcore.AddSync(logF), zapcore.DebugLevel)
// demo.err.log记录ERROR级别的日志
errF, _ := os.Create("./log/demo.err.log")
c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel)
// 使用NewTee将c1和c2合并到core
core := zapcore.NewTee(c1, c2)
logger := zap.New(core, zap.AddCaller())
sugarLogger = logger.Sugar()
}
demo.err.log的内容如下:
2023-10-12T15:21:09.108+0800 ERROR zap/main.go:55 Error fetching URL https://www.google.com : Error = Get "https://www.google.com": dial tcp [2a03:2880:f112:83:face:b00c:0:25de]:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
demo.log的内容如下:
2023-10-12T15:20:47.300+0800 DEBUG zap/main.go:52 Trying to hit GET request for https://www.baidu.com
2023-10-12T15:20:47.675+0800 INFO zap/main.go:57 Success! statusCode = 200 OK for URL https://www.baidu.com
2023-10-12T15:20:47.675+0800 DEBUG zap/main.go:52 Trying to hit GET request for https://www.google.com
2023-10-12T15:21:09.108+0800 ERROR zap/main.go:55 Error fetching URL https://www.google.com : Error = Get "https://www.google.com": dial tcp [2a03:2880:f112:83:face:b00c:0:25de]:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
Zap日志工具唯一缺少的就是日志切割归档功能。这里使用第三方库Lumberjack来实现,但是这个库目前只支持按文件大小切割,原因是按时间切割效率低且不能保证日志数据不被破坏。
要在zap中加入Lumberjack支持,我们需要修改WriteSyncer代码。我们将按照下面的代码修改getLogWriter()函数:
// MaxSize:定义了日志文件的最大大小,单位是MB。
// MaxBackups:定义了最多保留的备份文件数量。当备份文件数量超过MaxBackups后,lumberjack会自动删除最旧的备份文件。
// MaxAge:定义了备份文件的最大保存天数。当备份文件的保存天数超过MaxAge后,lumberjack会自动删除备份文件。
// Compress:定义了备份文件是否需要压缩。如果设置为true,备份的日志文件会被压缩为.gz格式。
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "./log/demo.log",
MaxSize: 1,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}
最后只要将getLogWriter返回就行。
writeSyncer := getLogWriter()
以上就是Zap库的基本使用。下面做一个总结: