Go 学习笔记(54)— Go 第三方库之 uber-go/zap/lumberjack(记录日志到文件、支持自动分割日志、支持日志级别、打印调用文件、函数和行号)
1. 簡要說明
zap 是 uber 開源的 Go 高性能日志庫,支持不同的日志級別, 能夠打印基本信息等,但不支持日志的分割,這里我們可以使用 lumberjack 也是 zap 官方推薦用于日志分割,結(jié)合這兩個庫我們就可以實現(xiàn)以下功能的日志機制:
- 能夠?qū)⑹录涗浀轿募?#xff0c;而不是應用程序控制臺;
- 日志切割能夠根據(jù)文件大小、時間或間隔等來切割日志文件;
- 支持不同的日志級別,例如
DEBUG,INFO,WARN,ERROR等; - 能夠打印基本信息,如調(diào)用文件、函數(shù)名和行號,日志時間等;
官網(wǎng)地址:https://github.com/uber-go/zap
2. 下載安裝
使用下面命令安裝
go get -u go.uber.org/zap
如果下載失敗,則使用以下命令重新下載安裝
go get github.com/uber-go/zap
下載安裝成功后還有如下提示:
package github.com/uber-go/zap: code in directory
/home/wohu/GoCode/src/github.com/uber-go/zap expects import "go.uber.org/zap"
注意,不能通過 下面的語句導入該包,會有上面的錯誤提示
import ("github.com/uber-go/zap"
)
原因是作者開發(fā)它時的工程目錄本來就是 go.uber.org/zap ,只是它的代碼發(fā)布到 git 的目錄是 github.com/uber-go/zap 而已。
解決方法是將 zap 目錄復制到 GOPATH/src/go.uber.org 下(可能還會需要 go.uber.org/atomic 和 go.uber.org/multierr ,均可參考該方法 get 下來)。
go get -v github.com/uber-go/atomic
go get -v github.com/uber-go/multierr
同樣將 atomic 和 multierr 拷貝到 go.uber.org 目錄下。
3. 配置 zap Logger
zap 提供了兩種類型的日志記錄器—和 Logger 和 Sugared Logger 。兩者之間的區(qū)別是:
- 在每一微秒和每一次內(nèi)存分配都很重要的上下文中,使用
Logger。它甚至比SugaredLogger更快,內(nèi)存分配次數(shù)也更少,但它只支持強類型的結(jié)構(gòu)化日志記錄。 - 在性能很好但不是很關(guān)鍵的上下文中,使用
SugaredLogger。它比其他結(jié)構(gòu)化日志記錄包快 4-10 倍,并且支持結(jié)構(gòu)化和printf風格的日志記錄。
所以一般場景下我們使用 Sugared Logger 就足夠了。
3.1 Logger
- 通過調(diào)用
zap.NewProduction()/zap.NewDevelopment()或者zap.NewExample()創(chuàng)建一個Logger。 - 上面的每一個函數(shù)都將創(chuàng)建一個
logger。唯一的區(qū)別在于它將記錄的信息不同。例如production logger默認記錄調(diào)用函數(shù)信息、日期和時間等。 - 通過
Logger調(diào)用INFO、ERROR等。 - 默認情況下日志都會打印到應用程序的
console界面。
3.1.1 NewExample
代碼示例:
package mainimport ("go.uber.org/zap"
)func main() {log := zap.NewExample()log.Debug("this is debug message")log.Info("this is info message")log.Info("this is info message with fileds",zap.Int("age", 24), zap.String("agender", "man"))log.Warn("this is warn message")log.Error("this is error message")log.Panic("this is panic message")}
輸出結(jié)果:
{"level":"debug","msg":"this is debug message"}
{"level":"info","msg":"this is info message"}
{"level":"info","msg":"this is info message with fileds","age":24,"agender":"man"}
{"level":"warn","msg":"this is warn message"}
{"level":"error","msg":"this is error message"}
{"level":"panic","msg":"this is panic message"}
panic: this is panic message
3.1.2 NewDevelopment
代碼示例:
func main() {log, _ := zap.NewDevelopment()log.Debug("this is debug message")log.Info("this is info message")log.Info("this is info message with fileds",zap.Int("age", 24), zap.String("agender", "man"))log.Warn("this is warn message")log.Error("this is error message") // log.DPanic("This is a DPANIC message") // log.Panic("this is panic message")// log.Fatal("This is a FATAL message")}
輸出結(jié)果:
2020-06-12T18:51:11.457+0800 DEBUG task/main.go:9 this is debug message
2020-06-12T18:51:11.457+0800 INFO task/main.go:10 this is info message
2020-06-12T18:51:11.457+0800 INFO task/main.go:11 this is info message with fileds {"age": 24, "agender": "man"}
2020-06-12T18:51:11.457+0800 WARN task/main.go:13 this is warn message
main.main/home/wohu/GoCode/src/task/main.go:13
runtime.main/usr/local/go/src/runtime/proc.go:200
2020-06-12T18:51:11.457+0800 ERROR task/main.go:14 this is error message
main.main/home/wohu/GoCode/src/task/main.go:14
runtime.main/usr/local/go/src/runtime/proc.go:200
3.1.3 NewProduction
代碼示例:
func main() {log, _ := zap.NewProduction()log.Debug("this is debug message")log.Info("this is info message")log.Info("this is info message with fileds",zap.Int("age", 24), zap.String("agender", "man"))log.Warn("this is warn message")log.Error("this is error message") // log.DPanic("This is a DPANIC message") // log.Panic("this is panic message")// log.Fatal("This is a FATAL message")
}
輸出結(jié)果:
{"level":"info","ts":1591959367.316352,"caller":"task/main.go:10","msg":"this is info message"}
{"level":"info","ts":1591959367.3163702,"caller":"task/main.go:11","msg":"this is info message with fileds","age":24,"agender":"man"}
{"level":"warn","ts":1591959367.3163917,"caller":"task/main.go:13","msg":"this is warn message"}
{"level":"error","ts":1591959367.3163974,"caller":"task/main.go:14","msg":"this is error message","stacktrace":"main.main\n\t/home/wohu/GoCode/src/task/main.go:14\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:200"}
3.1.4 對比總結(jié)
Example和Production使用的是json格式輸出,Development使用行的形式輸出Development- 從警告級別向上打印到堆棧中來跟蹤
- 始終打印包/文件/行(方法)
- 在行尾添加任何額外字段作為
json字符串 - 以大寫形式打印級別名稱
- 以毫秒為單位打印 ISO8601 格式的時間戳
Production- 調(diào)試級別消息不記錄
Error,Dpanic級別的記錄,會在堆棧中跟蹤文件,Warn不會- 始終將調(diào)用者添加到文件中
- 以時間戳格式打印日期
- 以小寫形式打印級別名稱
在上面的代碼中,我們首先創(chuàng)建了一個 Logger ,然后使用 Info / Error 等 Logger 方法記錄消息。
日志記錄器方法的語法是這樣的:
func (log *Logger) MethodXXX(msg string, fields ...Field)
其中 MethodXXX 是一個可變參數(shù)函數(shù),可以是 Info / Error / Debug / Panic 等。每個方法都接受一個消息字符串和任意數(shù)量的 zapcore.Field 長參數(shù)。
每個 zapcore.Field 其實就是一組鍵值對參數(shù)。
3.2 Sugared Logger
默認的 zap 記錄器需要結(jié)構(gòu)化標簽,即對每個標簽,需要使用特定值類型的函數(shù)。
log.Info("this is info message with fileds",zap.Int("age", 24), zap.String("agender", "man"))
雖然會顯的很長,但是對性能要求較高的話,這是最快的選擇。也可以使用suger logger, 它基于 printf 分割的反射類型檢測,提供更簡單的語法來添加混合類型的標簽。
我們使用 Sugared Logger 來實現(xiàn)相同的功能。
- 大部分的實現(xiàn)基本都相同;
- 惟一的區(qū)別是,我們通過調(diào)用主
logger的.Sugar()方法來獲取一個SugaredLogger; - 然后使用
SugaredLogger以printf格式記錄語句;
func main() {logger, _ := zap.NewDevelopment()slogger := logger.Sugar()slogger.Debugf("debug message age is %d, agender is %s", 19, "man")slogger.Info("Info() uses sprint")slogger.Infof("Infof() uses %s", "sprintf")slogger.Infow("Infow() allows tags", "name", "Legolas", "type", 1)}
輸出結(jié)果:
2020-06-12T19:23:54.184+0800 DEBUG task/main.go:11 debug message age is 19, agender is man
2020-06-12T19:23:54.185+0800 INFO task/main.go:12 Info() uses sprint
2020-06-12T19:23:54.185+0800 INFO task/main.go:13 Infof() uses sprintf
2020-06-12T19:23:54.185+0800 INFO task/main.go:14 Infow() allows tags {"name": "Legolas", "type": 1}
如果需要,可以隨時使用記錄器上的 .Desugar() 方法從 sugar logger 切換到標準記錄器。
log := slogger.Desugar()log.Info("After Desugar; INFO message")
log.Warn("After Desugar; WARN message")
log.Error("After Desugar; ERROR message")
4. 將日志寫入文件
我們將使用zap.New(…)方法來手動傳遞所有配置,而不是使用像zap.NewProduction()這樣的預置方法來創(chuàng)建 logger 。
func New(core zapcore.Core, options ...Option) *Logger
zapcore.Core需要三個配置——Encoder,WriteSyncer,LogLevel。
Encoder:編碼器(如何寫入日志)。我們將使用開箱即用的NewConsoleEncoder(),并使用預先設(shè)置的ProductionEncoderConfig()。
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
WriterSyncer:指定日志將寫到哪里去。我們使用zapcore.AddSync()函數(shù)并且將打開的文件句柄傳進去。
file, _ := os.Create("./test.log")
writeSyncer := zapcore.AddSync(file)
Log Level:哪種級別的日志將被寫入。
代碼示例:
package mainimport ("os""go.uber.org/zap""go.uber.org/zap/zapcore"
)var sugarLogger *zap.SugaredLoggerfunc InitLogger() {encoder := getEncoder()writeSyncer := getLogWriter()core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)// zap.AddCaller() 添加將調(diào)用函數(shù)信息記錄到日志中的功能。logger := zap.New(core, zap.AddCaller())sugarLogger = logger.Sugar()
}func getEncoder() zapcore.Encoder {encoderConfig := zap.NewProductionEncoderConfig()encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 修改時間編碼器// 在日志文件中使用大寫字母記錄日志級別encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder// NewConsoleEncoder 打印更符合人們觀察的方式return zapcore.NewConsoleEncoder(encoderConfig)
}func getLogWriter() zapcore.WriteSyncer {file, _ := os.Create("./test.log")return zapcore.AddSync(file)
}func main() {InitLogger()sugarLogger.Info("this is info message")sugarLogger.Infof("this is %s, %d", "aaa", 1234)sugarLogger.Error("this is error message")sugarLogger.Info("this is info message")
}
輸出日志文件:
2020-06-16T09:01:06.192+0800 INFO task/main.go:40 this is info message
2020-06-16T09:01:06.192+0800 INFO task/main.go:41 this is aaa, 1234
2020-06-16T09:01:06.192+0800 ERROR task/main.go:42 this is error message
2020-06-16T09:01:06.192+0800 INFO task/main.go:43 this is info message
5. 使用 lumberjack 進行日志切割歸檔
因為 zap 本身不支持切割歸檔日志文件,為了添加日志切割歸檔功能,我們將使用第三方庫 lumberjack 來實現(xiàn)。
5.1 安裝 lumberjack
執(zhí)行下面的命令安裝 lumberjack
go get -uv github.com/natefinch/lumberjack
5.2 將 lumberjack 加入 zap logger
要在 zap 中加入 lumberjack 支持,我們需要修改 WriteSyncer 代碼。我們將按照下面的代碼修改 getLogWriter() 函數(shù):
func getLogWriter() zapcore.WriteSyncer {lumberJackLogger := &lumberjack.Logger{Filename: "./test.log",MaxSize: 10,MaxBackups: 5,MaxAge: 30,Compress: false,}return zapcore.AddSync(lumberJackLogger)
}
Lumberjack Logger 采用以下屬性作為輸入:
Filename: 日志文件的位置;MaxSize:在進行切割之前,日志文件的最大大小(以MB為單位);MaxBackups:保留舊文件的最大個數(shù);MaxAges:保留舊文件的最大天數(shù);Compress:是否壓縮/歸檔舊文件;
完整代碼:
package mainimport ("github.com/natefinch/lumberjack""go.uber.org/zap""go.uber.org/zap/zapcore"
)var sugarLogger *zap.SugaredLoggerfunc InitLogger() {encoder := getEncoder()writeSyncer := getLogWriter()core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)// zap.AddCaller() 添加將調(diào)用函數(shù)信息記錄到日志中的功能。logger := zap.New(core, zap.AddCaller())sugarLogger = logger.Sugar()
}func getEncoder() zapcore.Encoder {encoderConfig := zap.NewProductionEncoderConfig()encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 修改時間編碼器// 在日志文件中使用大寫字母記錄日志級別encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder// NewConsoleEncoder 打印更符合人們觀察的方式return zapcore.NewConsoleEncoder(encoderConfig)
}func getLogWriter() zapcore.WriteSyncer {lumberJackLogger := &lumberjack.Logger{Filename: "./test.log",MaxSize: 10,MaxBackups: 5,MaxAge: 30,Compress: false,}return zapcore.AddSync(lumberJackLogger)
}func main() {InitLogger()sugarLogger.Info("this is info message")sugarLogger.Infof("this is %s, %d", "aaa", 1234)sugarLogger.Error("this is error message")sugarLogger.Info("this is info message")
}
6. Log 第三方庫 uber-zap 使用
package main
import ("time""github.com/natefinch/lumberjack""go.uber.org/zap""go.uber.org/zap/zapcore"
)
var logger *zap.Logger
// logpath 日志文件路徑
// loglevel 日志級別
func InitLogger(logpath string, loglevel string) {// 日志分割hook := lumberjack.Logger{Filename: logpath, // 日志文件路徑,默認 os.TempDir()MaxSize: 10, // 每個日志文件保存10M,默認 100MMaxBackups: 30, // 保留30個備份,默認不限MaxAge: 7, // 保留7天,默認不限Compress: true, // 是否壓縮,默認不壓縮}write := zapcore.AddSync(&hook)// 設(shè)置日志級別// debug 可以打印出 info debug warn// info 級別可以打印 warn info// warn 只能打印 warn// debug->info->warn->errorvar level zapcore.Levelswitch loglevel {case "debug":level = zap.DebugLevelcase "info":level = zap.InfoLevelcase "error":level = zap.ErrorLeveldefault:level = zap.InfoLevel}encoderConfig := zapcore.EncoderConfig{TimeKey: "time",LevelKey: "level",NameKey: "logger",CallerKey: "linenum",MessageKey: "msg",StacktraceKey: "stacktrace",LineEnding: zapcore.DefaultLineEnding,EncodeLevel: zapcore.LowercaseLevelEncoder, // 小寫編碼器EncodeTime: zapcore.ISO8601TimeEncoder, // ISO8601 UTC 時間格式EncodeDuration: zapcore.SecondsDurationEncoder, //EncodeCaller: zapcore.FullCallerEncoder, // 全路徑編碼器EncodeName: zapcore.FullNameEncoder,}// 設(shè)置日志級別atomicLevel := zap.NewAtomicLevel()atomicLevel.SetLevel(level)core := zapcore.NewCore(// zapcore.NewConsoleEncoder(encoderConfig),zapcore.NewJSONEncoder(encoderConfig),// zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(&write)), // 打印到控制臺和文件write,level,)// 開啟開發(fā)模式,堆棧跟蹤caller := zap.AddCaller()// 開啟文件及行號development := zap.Development()// 設(shè)置初始化字段,如:添加一個服務器名稱filed := zap.Fields(zap.String("serviceName", "serviceName"))// 構(gòu)造日志logger = zap.New(core, caller, development, filed)logger.Info("DefaultLogger init success")
}
func main() {// 歷史記錄日志名字為:all.log,服務重新啟動,日志會追加,不會刪除InitLogger("./all.log", "debug")// 強結(jié)構(gòu)形式logger.Info("test",zap.String("string", "string"),zap.Int("int", 3),zap.Duration("time", time.Second),)// 必須 key-value 結(jié)構(gòu)形式 性能下降一點logger.Sugar().Infow("test-","string", "string","int", 1,"time", time.Second,)
}
從例子看出:
- 它同時提供了結(jié)構(gòu)化日志記錄和 printf 風格的日志記錄
- 先初始化 lumberjack 后初始化 zap
7. 在何處打印日志
- 在分支語句處打印日志。在分支語句處打印日志,可以判斷出代碼走了哪個分支,有助于判斷請求的下一跳,繼而繼續(xù)排查問題。
- 寫操作必須打印日志。寫操作最可能會引起比較嚴重的業(yè)務故障,寫操作打印日志,可以在出問題時找到關(guān)鍵信息。
- 在循環(huán)中打印日志要慎重。如果循環(huán)次數(shù)過多,會導致打印大量的日志,嚴重拖累代碼的性能,建議的辦法是在循環(huán)中記錄要點,在循環(huán)外面總結(jié)打印出來。
- 在錯誤產(chǎn)生的最原始位置打印日志。對于嵌套的 Error,可在 Error 產(chǎn)生的最初位置打印 Error 日志,上層如果不需要添加必要的信息,可以直接返回下層的 Error。
參考:
優(yōu)秀開源日志包使用教程
https://studygolang.com/articles/19387
https://blog.csdn.net/niyuelin1990/article/details/78340336
https://blog.csdn.net/feifeixiang2835/article/details/94207810
https://blog.csdn.net/qq_27068845/article/details/103480451
https://blog.csdn.net/NUCEMLS/article/details/86534444
https://zhuanlan.zhihu.com/p/88856378
https://gitbook.cn/books/5e7637996ba17a6d2c9a3352/index.html
總結(jié)
以上是生活随笔為你收集整理的Go 学习笔记(54)— Go 第三方库之 uber-go/zap/lumberjack(记录日志到文件、支持自动分割日志、支持日志级别、打印调用文件、函数和行号)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 东方人物立绘是谁画的啊?
- 下一篇: 开健身房大概需要多少资金