日志库在每个项目中都是必不可少的一部分,Go语言中有很多优秀的日志库,比如logrus、zap等,这里我们介绍zap日志库的使用。
zap库
zap是uber公司开源的一款go语言的日志库。它支持多种日志级别,结构化日志,而且性能很好。性能对比图片这里就贴了,感兴趣的话可以访问仓库地址查看(https://github.com/uber-go/zap)。有一些日志库使用了基于反射的序列化和字符串格式化,这写都是CPU密集型的操作,会影响日志库的性能,这些都是。而zap之所以能性能好,是因为使用了一个无反射、零分配的JSON编码器,尽可能的降低了序列化开销和分配。
下面我们来更详细的介绍zap库的使用。
zap日志库的使用
NewExample()
zap.NewExample()构建的是一个简化的Logger,方便测试。它的输出中,省略了时间戳和调用函数。
示例:
func TestZapExampleSugar(t *testing.T) {
logger := zap.NewExample()
logger.Info("info", zap.Uint8("age", 18), zap.String("name", "张三"))
}
输出:
=== RUN TestZapExampleSugar
{"level":"info","msg":"info","age":18,"name":"张三"}
--- PASS: TestZapExampleSugar (0.00s)
NewDevelopment()
zap.NewDevelopment()构建了一个开发用的Logger,它将日志以友好(也没看出来有多友好)的格式写入标准错误。
使用方法:
func TestZapDevelopment(t *testing.T) {
logger, err := zap.NewDevelopment()
require.Equal(t, nil, err)
logger.Debug("debug", zap.Int32("age", 18), zap.String("name", "张三"))
logger.Info("info", zap.Int32("age", 18), zap.String("name", "张三"))
logger.Error("error", zap.Int32("age", 18), zap.String("name", "张三"))
}
输出:
=== RUN TestZapDevelopment
2023-12-03T16:55:39.731+0800 DEBUG zaplog/zap_test.go:26 debug {"age": 18, "name": "张三"}
2023-12-03T16:55:39.732+0800 INFO zaplog/zap_test.go:27 info {"age": 18, "name": "张三"}
2023-12-03T16:55:39.732+0800 ERROR zaplog/zap_test.go:28 error {"age": 18, "name": "张三"}
git.gqnotes.com/guoqiang/grpcexercises/zaplog.TestZapDevelopment
/Users/gq/Documents/grpcexercises/zaplog/zap_test.go:28
testing.tRunner
/opt/homebrew/opt/go/libexec/src/testing/testing.go:1595
--- PASS: TestZapDevelopment (0.00s)
我们来看一下相关源码:
func NewDevelopment(options ...Option) (*Logger, error) {
return NewDevelopmentConfig().Build(options...)
}
// NewDevelopmentConfig builds a reasonable default development logging
// NewDevelopmentConfig构建了一个合理的开发用默认日志配置。
// configuration.
// Logging is enabled at DebugLevel and above, and uses a console encoder.
// 日志级别在Debug及以上级别启用,使用的encoder是console。
// Logs are written to standard error.
// 日志被写入到标准错误。
// Stacktraces are included on logs of WarnLevel and above.
// 在Warn及以上级别的日志中,包含堆栈信息。
// DPanicLevel logs will panic.
// panic级别的错误会导致panic。
//
// See [NewDevelopmentEncoderConfig] for information
// on the default encoder configuration.
func NewDevelopmentConfig() Config {
return Config{
Level: NewAtomicLevelAt(DebugLevel),
Development: true,
Encoding: "console",
EncoderConfig: NewDevelopmentEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
}
NewProduction()
zap.NewProduction()返回生产环境用的logger。使用方法:
func TestZapProduct(t *testing.T) {
logger, err := zap.NewProduction()
require.Equal(t, nil, err)
logger.Info("info", zap.Int32("age", 18), zap.String("name", "张三"))
}
输出:
=== RUN TestZapProduct
{"level":"info","ts":1701594526.765737,"caller":"zaplog/zap_test.go:36","msg":"info","age":18,"name":"张三"}
--- PASS: TestZapProduct (0.00s)
打印调用栈信息
当记录日志信息时,我们可能需要记录调用栈信息(比如a->b->c......)。我们可以在初始化时,设置记录哪种级别的日志的调用栈信息。如下面的代码,记录Error级别的错误信息:
func TestZapProductV1(t *testing.T) {
logger, err := zap.NewProduction(zap.AddStacktrace(zap.ErrorLevel))
require.Equal(t, nil, err)
b1(logger)
}
func b1(logger *zap.Logger) {
logger.Info("b1 info")
logger.Error("b1 failed")
}
输出:
=== RUN TestZapProductV1
{"level":"info","ts":1701595209.2724411,"caller":"zaplog/zap_test.go:48","msg":"b1 info"}
{"level":"error","ts":1701595209.272565,"caller":"zaplog/zap_test.go:49","msg":"b1 failed","stacktrace":"git.gqnotes.com/guoqiang/grpcexercises/zaplog.b1\n\t/Users/gq/Documents/grpcexercises/zaplog/zap_test.go:49\ngit.gqnotes.com/guoqiang/grpcexercises/zaplog.TestZapProductV1\n\t/Users/gq/Documents/grpcexercises/zaplog/zap_test.go:44\ntesting.tRunner\n\t/opt/homebrew/opt/go/libexec/src/testing/testing.go:1595"}
--- PASS: TestZapProductV1 (0.00s)
可以看到,info级别的日志没有打印调用栈信息,而error级别的日志记录了调用栈信息。
lumberjack库
日志文件的大小会随着时间的增长而变大。为了防止日志文件过大,我们需要对其进行分割。lumberjack就是一款提供日志分割功能的库。
仓库地址:https://github.com/natefinch/lumberjack。
下面结合zap库,展示一下lumberjack库的使用方法。
在下面的例子中,我们融合了zap和lumberjack,设置如下:
-
当日志文件大小超过1MB时,将日志文件分割。 -
日志存储在logs/app.log文件中。 -
不压缩。
func TestLumberJack(t *testing.T) {
logger, err := zap.NewProduction(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return getZapCoreWithWriter()
}), zap.AddStacktrace(zap.ErrorLevel))
require.Equal(t, nil, err)
logger.Info("info", zap.Uint8("age", 18), zap.String("name", "张三"))
}
func getZapCoreWithWriter() zapcore.Core {
writer := lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 1, // 当日志文件大小超过此值时,将被分割,单位为MB,此处设置的是1MB。
MaxAge: 0, // 历史日志的保留天数
MaxBackups: 0,
LocalTime: true,
Compress: true, // 在实际生产环境中,往往需要压缩
}
cfg := zap.NewProductionEncoderConfig()
cfg.EncodeTime = zapcore.ISO8601TimeEncoder
return zapcore.NewTee(zapcore.NewCore(zapcore.NewJSONEncoder(cfg), zapcore.AddSync(&writer), zap.InfoLevel))
}
执行上面的代码后,会在logs目录下生成app.log文件,内容如下:
{"level":"info","ts":"2023-12-03T18:06:56.529+0800","caller":"zaplog/lumberjack_test.go:18","msg":"info","age":18,"name":"张三"}
日志分割功能的验证
我们来一段批量写入日志的代码,验证一下日志分割功能是否生效。
func TestLumberJackBatch(t *testing.T) {
logger, err := zap.NewProduction(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return getZapCoreWithWriter()
}), zap.AddStacktrace(zap.ErrorLevel))
require.Equal(t, nil, err)
for i := 0; i < 1000; i++ {
logger.Info("批量写入", zap.String("content", "庆历四年的春天,滕子京被降职到巴陵郡做太守。隔了一年,政治清明通达,人民安居和顺,各种荒废的事业都兴办起来了。于是重新修建岳阳楼,扩大它原有的规模,把唐代名家和当代人的诗赋刻在它上面。嘱托我写一篇文章来记述这件事情。我观看那巴陵郡的美好景色,全在洞庭湖上。衔接远山,吞没长江,流水浩浩荡荡,无边无际,一天里阴晴多变,气象千变万化。这就是岳阳楼的雄伟景象。前人的记述(已经)很详尽了。那么向北面通到巫峡,向南面直到潇水和湘水,降职的官吏和来往的诗人,大多在这里聚会,(他们)观赏自然景物而触发的感情大概会有所不同吧?像那阴雨连绵,接连几个月不放晴,寒风怒吼,浑浊的浪冲向天空;太阳和星星隐藏起光辉,山岳隐没了形体;商人和旅客(一译:行商和客商)不能通行,船桅倒下,船桨折断;傍晚天色昏暗,虎在长啸,猿在悲啼,(这时)登上这座楼,就会有一种离开国都、怀念家乡,担心人家说坏话、惧怕人家批评指责,满眼都是萧条的景象,感慨到了极点而悲伤的心情。到了春风和煦,阳光明媚的时候,湖面平静,没有惊涛骇浪,天色湖光相连,一片碧绿,广阔无际;沙洲上的鸥鸟,时而飞翔,时而停歇,美丽的鱼游来游去,岸上的香草和小洲上的兰花,草木茂盛,青翠欲滴。有时大片烟雾完全消散,皎洁的月光一泻千里,波动的光闪着金色,静静的月影像沉入水中的玉璧,渔夫的歌声在你唱我和地响起来,这种乐趣(真是)无穷无尽啊!(这时)登上这座楼,就会感到心胸开阔、心情愉快,光荣和屈辱一并忘了,端着酒杯,吹着微风,觉得喜气洋洋了。哎呀!我曾探求过古时仁人的心境,或者和这些人的行为两样的,为什么呢?(是由于)不因外物好坏,自己得失而或喜或悲。在朝廷上做官时,就为百姓担忧;不在朝廷做官而处在僻远的江湖中间就为国君忧虑。他进也忧虑,退也忧愁。既然这样,那么他们什么时候才会感到快乐呢?古仁人必定说:“先于天下人的忧去忧,晚于天下人的乐去乐。”呀。唉!如果没有这种人,我与谁一道归去呢?写于为庆历六年九月十五日。"))
}
}
执行之后,我们再看一下logs目录:
![alt](https://img-blog.csdnimg.cn/img_convert/1499bfab9db978c965224b246768a3de.png)
可以看到,除了当前使用的app.log,还多了两个大小为1MB的log文件。
日志压缩功能的验证
为了节省磁盘空间,在生产环境中,往往会对历史日志进行压缩。我们修改一下上面的代码,来启用压缩功能:
func getZapCoreWithWriter() zapcore.Core {
writer := lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 1, // 当日志文件大小超过此值时,将被分割,单位为MB,此处设置的是1MB。
MaxAge: 0, // 历史日志的保留天数
MaxBackups: 0,
LocalTime: true,
Compress: true, // 在实际生产环境中,往往需要压缩
}
cfg := zap.NewProductionEncoderConfig()
cfg.EncodeTime = zapcore.ISO8601TimeEncoder
return zapcore.NewTee(zapcore.NewCore(zapcore.NewJSONEncoder(cfg), zapcore.AddSync(&writer), zap.InfoLevel))
}
重新执行TestLumberJackBatch代码,再查看logs目录:
![alt](https://img-blog.csdnimg.cn/img_convert/74cc2ce522f1860fa8bdd2f5333d502f.png)
我们看到,logs目录下多了几个压缩文件(由于我们写入的是相同的字符串,所以压缩文件很小,在实际应用中,不会这么小)。
注:此文原载于本人个人网站,链接地址。
本文由 mdnice 多平台发布