一个项目日志功能够不够健全、记录的日志内容够不够有辨识度直接决定了一个项目维护的难度,你查日志是大海捞针一点点看,还是能够靠一些有辨识度的索引筛选出用户访问程序期间留下的包含了完整上下文的日志直接决定了你搞明白“为什么会这样”所耗费时间的多少。
从本节开始我们先用两节为我们的Go项目定制日志组件,让它足够好用。
未来我们会用这个组件一步步完善项目的应用日志规范,让项目框架能为我们把关键的上下文信息记录到日志中,保证我们即使自己忘记打日志的情况下框架依然能为我们记录下一些关键日志。
图片
本节项目的所有源码和测试接口都单独封存了Git版本, 方便大家在自己机器上快速调试和学习。
图片
安装 Zap 和相关配置信息准备
Zap是Uber开源的Go日志组件,它的优势什么的我就不过多介绍了,这两节介绍的内容更多地是关注怎么给自己的项目框架定制一个比较好用日志组件,其中介绍的方法思路换做其他的Go开源日志组件也同样适用
我们首先来安装一下 Zap ,这个时候可以打开你自己新建的项目来跟着操作
go get go.uber.org/zap@v1.21.0
把日志写入文件,同时完成日志文件的切割归档需要借助另外一个开源库 lumberjack,我们把它也安装一下
go get gopkg.in/natefinch/lumberjack.v2@v2.0.0
安装完成后我们先添加几个与日志相关的配置,好能通过配置控制日志文件的路径和文件大小等选项
打开项目开发环境的配置文件 config/application.dev.yaml, 我们在app原配置基础上,加了log相关的三个配置。
app:
env: dev
name: go-mall
log:
path: "/tmp/applog/go-mall.log"
max_size: 100
max_age: 60
这里注意一下,开发环境日志文件放在/tmp目录下主要是为了避免在电脑上很多目录的权限限制比较严格程序没办法写日志的问题。测试环境和生存环境的日志文件路径建议设置成 /home/applog/go-mall/go-mall.log 这样的路径。
配置文件加好后,相应的我们的配置对象也要根据新增配置进行调整。
type appConfig struct {
Name string `mapstructure:"name"`
Env string `mapstructure:"env"`
Log struct {
FilePath string `mapstructure:"path"`
FileMaxSize int `mapstructure:"max_size"`
BackUpFileMaxAge int `mapstructure:"max_age"`
}
}
初始化日志组件
接下来我们先初始化Zap, 把它做为我们日志组件的基础Logger,配置完后我们会在其上封装一个门面,让Logger 变得更好用一些,通过这个门面除了能简化我们使用Zap打日志的操作方式外,还会给日志自动追加一些追踪和定位信息便于我们追踪日志和定位程序问题,这个下个章节再讲,本节先把基础的东西做好。
接下里,先在项目中新建一个 common 目录
.
|-- common
| |-- enum
| |-- logger
|-- main.go
|-- go.mod
|-- go.sum
logger目录中先新建 zap.go 在文件中对Zap进行初始化相关的操作。
func init() {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoder := zapcore.NewJSONEncoder(encoderConfig)
fileWriteSyncer := getFileLogWriter()
var cores []zapcore.Core
......
core := zapcore.NewTee(cores...)
_logger = zap.New(core)
}
因为Zap我们只会把它当作基础Logger,所以把它的变量定义成了只能在 logger 包内访问的全局变量
var _logger *zap.Logger
我们都知道针对不同的运行环境,日志的最低级别不太一样,比如说在开发环境中我们会打很多Debug日志,这个日志到生产环境上应该被自动过滤掉,如果不支持这个功能的话就得每次在代码里把自己写过的Debug日志的代码行删掉,这个相信谁都办不到。
所以我们从底层Logger下手让程序运行在服务器上时不收集Debug日志。
var cores []zapcore.Core
switch config.App.Env {
case enum.ModeTest, enum.ModeProd:
// 测试环境和生产环境的日志输出到文件中
cores = append(cores, zapcore.NewCore(encoder, fileWriteSyncer, zapcore.InfoLevel))
case enum.ModeDev:
// 开发环境同时向控制台和文件输出日志, Debug级别的日志也会被输出
cores = append(
cores,
zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel),
zapcore.NewCore(encoder, fileWriteSyncer, zapcore.DebugLevel),
)
}
通过上面这几行代码的设置让 Zap 在开发环境中可以写Debug级别的日志,并且除了向文件里写日志外,还同时向终端控制台写日志,这样我们打的日志就能出现在程序运行的控制台中,方便我们快速Debug。
日志文件的管理
Zap没有自动管理和切割日志文件的功能,这个功能我们要借助 lumberjack 这个库。
func getFileLogWriter() (writeSyncer zapcore.WriteSyncer) {
// 使用 lumberjack 实现 logger rotate
lumberJackLogger := &lumberjack.Logger{
Filename: config.App.Log.FilePath,
MaxSize: config.App.Log.FileMaxSize, // 文件最大 100 M
MaxAge: config.App.Log.BackUpFileMaxAge, // 旧文件最多保留90天
Compress: false,
LocalTime: true,
}
return zapcore.AddSync(lumberJackLogger)
}
创建 LumberJack 的 Logger 然后把它设置成 Zap 的 WriteSyncer ,这样使用 Zap 打的日志就会写到文件中
...
fileWriteSyncer := getFileLogWriter()
...
cores = append(cores, zapcore.NewCore(encoder, fileWriteSyncer, zapcore.InfoLevel))
创建LumberJack时可以定义日志文件的几个选项
- FileName 日志的路径,这个dev环境我们使用的是/tmp/applog/go-mall.log 。测试和生产环境建议设置成/home/applog/{项目}/{项目}.log,一来存放在/tmp中可能会被系统清理,二来通过固定的目录可以让ELK的日志收集组件去固定目录抽取日志文件把日志收集到统一的日志平台。
- MaxSize:单个日志文件的最大尺寸,上面配置里定义的是100 对应的尺寸是100M,日志文件达到这个大小后Lumber Jack会自动切割日志文件,把原来的日志保存到备份文件中
- MaxAge:单位是天,设置60 就是备份文件最多保存60天
效果测试
日志文件Writer初始化并设置给Zap 后我们可以测试下是否有效果,我测试的时候是先把MaxSize 改成1 , 即最大1M,随便写个测试方法在里面写日志,疯狂刷了一会儿接口让日志文件大小超过1M。