Go日志库zap和lumberjack库的使用

日志库在每个项目中都是必不可少的一部分,Go语言中有很多优秀的日志库,比如logrus、zap等,这里我们介绍zap日志库的使用。

zap库

zap是uber公司开源的一款go语言的日志库。它支持多种日志级别,结构化日志,而且性能很好。性能对比图片这里就贴了,感兴趣的话可以访问仓库地址查看(github.com/uber-go/zap...

下面我们来更详细的介绍zap库的使用。

zap日志库的使用

NewExample()

zap.NewExample()构建的是一个简化的Logger,方便测试。它的输出中,省略了时间戳和调用函数。

示例:

go 复制代码
func TestZapExampleSugar(t *testing.T) {
	logger := zap.NewExample()

	logger.Info("info", zap.Uint8("age", 18), zap.String("name", "张三"))
}

输出:

txt 复制代码
=== RUN   TestZapExampleSugar
{"level":"info","msg":"info","age":18,"name":"张三"}
--- PASS: TestZapExampleSugar (0.00s)

NewDevelopment()

zap.NewDevelopment()构建了一个开发用的Logger,它将日志以友好(也没看出来有多友好)的格式写入标准错误。

使用方法:

go 复制代码
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", "张三"))
}

输出:

txt 复制代码
=== 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)

我们来看一下相关源码:

go 复制代码
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。使用方法:

go 复制代码
func TestZapProduct(t *testing.T) {
	logger, err := zap.NewProduction()

	require.Equal(t, nil, err)

	logger.Info("info", zap.Int32("age", 18), zap.String("name", "张三"))
}

输出:

txt 复制代码
=== 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级别的错误信息:

go 复制代码
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")
}

输出:

txt 复制代码
=== 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就是一款提供日志分割功能的库。

仓库地址:github.com/natefinch/l...

下面结合zap库,展示一下lumberjack库的使用方法。

在下面的例子中,我们融合了zap和lumberjack,设置如下:

  • 当日志文件大小超过1MB时,将日志文件分割。
  • 日志存储在logs/app.log文件中。
  • 不压缩。
go 复制代码
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文件,内容如下:

txt 复制代码
{"level":"info","ts":"2023-12-03T18:06:56.529+0800","caller":"zaplog/lumberjack_test.go:18","msg":"info","age":18,"name":"张三"}

日志分割功能的验证

我们来一段批量写入日志的代码,验证一下日志分割功能是否生效。

go 复制代码
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目录:

可以看到,除了当前使用的app.log,还多了两个大小为1MB的log文件。

日志压缩功能的验证

为了节省磁盘空间,在生产环境中,往往会对历史日志进行压缩。我们修改一下上面的代码,来启用压缩功能:

go 复制代码
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目录:

我们看到,logs目录下多了几个压缩文件(由于我们写入的是相同的字符串,所以压缩文件很小,在实际应用中,不会这么小)。

注:此文原载于本人个人网站,链接地址

本文由mdnice多平台发布

相关推荐
devlei7 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
努力的小郑9 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3569 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3569 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁10 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp10 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴11 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友12 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒12 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan13 小时前
Go 内存回收-GC 源码1-触发与阶段
后端