三個實用細節(jié),讓Zap在Go項目中變得更好用
一個項目日志功能夠不夠健全、記錄的日志內(nèi)容夠不夠有辨識度直接決定了一個項目維護的難度,你查日志是大海撈針一點點看,還是能夠靠一些有辨識度的索引篩選出用戶訪問程序期間留下的包含了完整上下文的日志直接決定了你搞明白“為什么會這樣”所耗費時間的多少。
從本節(jié)開始我們先用兩節(jié)為我們的Go項目定制日志組件,讓它足夠好用。
未來我們會用這個組件一步步完善項目的應(yīng)用日志規(guī)范,讓項目框架能為我們把關(guān)鍵的上下文信息記錄到日志中,保證我們即使自己忘記打日志的情況下框架依然能為我們記錄下一些關(guān)鍵日志。
圖片
本節(jié)項目的所有源碼和測試接口都單獨封存了Git版本, 方便大家在自己機器上快速調(diào)試和學(xué)習(xí)。
圖片
安裝 Zap 和相關(guān)配置信息準備
Zap是Uber開源的Go日志組件,它的優(yōu)勢什么的我就不過多介紹了,這兩節(jié)介紹的內(nèi)容更多地是關(guān)注怎么給自己的項目框架定制一個比較好用日志組件,其中介紹的方法思路換做其他的Go開源日志組件也同樣適用
我們首先來安裝一下 Zap ,這個時候可以打開你自己新建的項目來跟著操作
go get go.uber.org/zap@v1.21.0
把日志寫入文件,同時完成日志文件的切割歸檔需要借助另外一個開源庫 lumberjack,我們把它也安裝一下
go get gopkg.in/natefinch/lumberjack.v2@v2.0.0
安裝完成后我們先添加幾個與日志相關(guān)的配置,好能通過配置控制日志文件的路徑和文件大小等選項
打開項目開發(fā)環(huán)境的配置文件 config/application.dev.yaml, 我們在app原配置基礎(chǔ)上,加了log相關(guān)的三個配置。
app:
env: dev
name: go-mall
log:
path: "/tmp/applog/go-mall.log"
max_size: 100
max_age: 60
這里注意一下,開發(fā)環(huán)境日志文件放在/tmp目錄下主要是為了避免在電腦上很多目錄的權(quán)限限制比較嚴格程序沒辦法寫日志的問題。測試環(huán)境和生存環(huán)境的日志文件路徑建議設(shè)置成 /home/applog/go-mall/go-mall.log 這樣的路徑。
配置文件加好后,相應(yīng)的我們的配置對象也要根據(jù)新增配置進行調(diào)整。
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, 把它做為我們?nèi)罩窘M件的基礎(chǔ)Logger,配置完后我們會在其上封裝一個門面,讓Logger 變得更好用一些,通過這個門面除了能簡化我們使用Zap打日志的操作方式外,還會給日志自動追加一些追蹤和定位信息便于我們追蹤日志和定位程序問題,這個下個章節(jié)再講,本節(jié)先把基礎(chǔ)的東西做好。
接下里,先在項目中新建一個 common 目錄
.
|-- common
| |-- enum
| |-- logger
|-- main.go
|-- go.mod
|-- go.sum
logger目錄中先新建 zap.go 在文件中對Zap進行初始化相關(guān)的操作。
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我們只會把它當(dāng)作基礎(chǔ)Logger,所以把它的變量定義成了只能在 logger 包內(nèi)訪問的全局變量
var _logger *zap.Logger
我們都知道針對不同的運行環(huán)境,日志的最低級別不太一樣,比如說在開發(fā)環(huán)境中我們會打很多Debug日志,這個日志到生產(chǎn)環(huán)境上應(yīng)該被自動過濾掉,如果不支持這個功能的話就得每次在代碼里把自己寫過的Debug日志的代碼行刪掉,這個相信誰都辦不到。
所以我們從底層Logger下手讓程序運行在服務(wù)器上時不收集Debug日志。
var cores []zapcore.Core
switch config.App.Env {
case enum.ModeTest, enum.ModeProd:
// 測試環(huán)境和生產(chǎn)環(huán)境的日志輸出到文件中
cores = append(cores, zapcore.NewCore(encoder, fileWriteSyncer, zapcore.InfoLevel))
case enum.ModeDev:
// 開發(fā)環(huán)境同時向控制臺和文件輸出日志, Debug級別的日志也會被輸出
cores = append(
cores,
zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel),
zapcore.NewCore(encoder, fileWriteSyncer, zapcore.DebugLevel),
)
}
通過上面這幾行代碼的設(shè)置讓 Zap 在開發(fā)環(huán)境中可以寫Debug級別的日志,并且除了向文件里寫日志外,還同時向終端控制臺寫日志,這樣我們打的日志就能出現(xiàn)在程序運行的控制臺中,方便我們快速Debug。
日志文件的管理
Zap沒有自動管理和切割日志文件的功能,這個功能我們要借助 lumberjack 這個庫。
func getFileLogWriter() (writeSyncer zapcore.WriteSyncer) {
// 使用 lumberjack 實現(xiàn) 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)
}
創(chuàng)建 LumberJack 的 Logger 然后把它設(shè)置成 Zap 的 WriteSyncer ,這樣使用 Zap 打的日志就會寫到文件中
...
fileWriteSyncer := getFileLogWriter()
...
cores = append(cores, zapcore.NewCore(encoder, fileWriteSyncer, zapcore.InfoLevel))
創(chuàng)建LumberJack時可以定義日志文件的幾個選項
- FileName 日志的路徑,這個dev環(huán)境我們使用的是/tmp/applog/go-mall.log 。測試和生產(chǎn)環(huán)境建議設(shè)置成/home/applog/{項目}/{項目}.log,一來存放在/tmp中可能會被系統(tǒng)清理,二來通過固定的目錄可以讓ELK的日志收集組件去固定目錄抽取日志文件把日志收集到統(tǒng)一的日志平臺。
- MaxSize:單個日志文件的最大尺寸,上面配置里定義的是100 對應(yīng)的尺寸是100M,日志文件達到這個大小后Lumber Jack會自動切割日志文件,把原來的日志保存到備份文件中
- MaxAge:單位是天,設(shè)置60 就是備份文件最多保存60天
效果測試
日志文件Writer初始化并設(shè)置給Zap 后我們可以測試下是否有效果,我測試的時候是先把MaxSize 改成1 , 即最大1M,隨便寫個測試方法在里面寫日志,瘋狂刷了一會兒接口讓日志文件大小超過1M。