Zap 是一個由 Uber 公司開源的結構化、高效能日誌記錄庫,旨在為 Go 語言提供一種快速、簡單且高效的日誌解決方案。它起源於 Uber 內部使用的日誌系統,後來於 2016 年開源,迅速獲得了 Go 社群的廣泛關注和應用。
Zap 的主要特點如下:
- 高效能:Zap 在設計時就非常注重效能,比標準庫 log 包快幾個數量級,即使在高併發場景下也能保持出色的效能表現。
- 結構化日誌:Zap 支援結構化日誌記錄,可以方便地記錄任意型別的欄位,而不僅限於字串,這有利於後期日誌分析和處理。
- 級別控制:Zap 提供了豐富的日誌級別控制,可以動態修改日誌級別,從而只輸出關鍵日誌或除錯日誌。
- 編碼支援:Zap 內建支援 JSON 和控制檯的日誌編碼,並提供了鉤子機制來擴充套件其他編碼格式。
- 日誌分割:Zap 支援根據日期、大小等條件自動分割日誌檔案,方便日誌檔案管理和分析。
Zap 廣泛應用於各種 Go 專案中,尤其是那些對效能、日誌結構化和可觀測性有較高要求的場景,如微服務、分散式系統等。很多知名的 Go 專案和公司都在使用 Zap,例如 Kubernetes、Istio、InfluxData 等。透過 Zap,開發者可以獲得高效、靈活且易於管理的日誌解決方案,從而更好地監控和除錯應用程式。
下面我們來進行 zap 日誌庫的上手實踐。
依賴
我個人比較習慣配置在 go.mod
檔案當中,但是搜尋了幾頁居然都沒有發現,只好採用了官方給的命令安裝依賴方式:
go get -u go.uber.org/zap
然後我發現了 go.mod
檔案已經有了相對應的配置,如下:
go.uber.org/zap v1.27.0 // indirect
在後面實踐當中還會用到其他的依賴,這裡一起發一下配置:
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
小試牛刀
下面我們先來一個基礎的 Case 來熟悉一下 zap 日誌庫的的使用語法:
//
// TestLogZap
// @Description: 測試zap日誌
// @param t
//
func TestLogZap(t *testing.T) {
logger, _ := zap.NewProduction() // 建立一個新的 Logger 例項
defer logger.Sync() // 確保緩衝區中的日誌條目被重新整理
logger.Info("FunTester,例子", // 使用 logger 記錄日誌
zap.String("name", "FunTester"), // 結構化上下文
zap.Int("score", 100), // 結構化上下文
)
logger.Info("warn FunTester coming!!!") // 使用 logger 記錄警告日誌
logger.Warn("warn FunTester coming!!!") // 使用 logger 記錄警告日誌
logger.Error("error FunTester coming!!!") // 使用 logger 記錄錯誤日誌
}
下面是控制檯輸出:
=== RUN TestLogZap
{"level":"info","ts":1717310460.23924,"caller":"test/zap_test.go:16","msg":"This is an info message","category":"example","counter":1}
{"level":"info","ts":1717310460.2393,"caller":"test/zap_test.go:20","msg":"warn FunTester coming!!!"}
{"level":"warn","ts":1717310460.2393029,"caller":"test/zap_test.go:21","msg":"warn FunTester coming!!!"}
{"level":"error","ts":1717310460.239305,"caller":"test/zap_test.go:22","msg":"error FunTester coming!!!","stacktrace":"funtester/test.TestLogZap\n\t/Users/oker/GolandProjects/funtester/test/zap_test.go:22\ntesting.tRunner\n\t/opt/homebrew/opt/go/libexec/src/testing/testing.go:1689"}
--- PASS: TestLogZap (0.00s)
PASS
可以看到,這裡的輸出格式均是 JSON
格式的日誌資訊,對於不同的級別,輸出的日誌資訊中,都包含了 caller
資訊,但是 error
日誌多了一個 stacktrace
資訊。
這裡是我查到的 zap 預設的配置資訊:
Debug 級別日誌:包含呼叫者資訊,但不包含堆疊資訊。
Info 級別日誌:包含呼叫者資訊,但不包含堆疊資訊。
Warn 級別日誌:包含呼叫者資訊,但不包含堆疊資訊。
Error 級別日誌:包含呼叫者資訊,幷包含堆疊資訊。
DPanic 級別日誌:包含呼叫者資訊,幷包含堆疊資訊。
Panic 級別日誌:包含呼叫者資訊,幷包含堆疊資訊。
Fatal 級別日誌:包含呼叫者資訊,幷包含堆疊資訊。
sugar
在 zap 日誌庫中,除了提供高效能、結構化的日誌記錄功能外,還提供了一個簡化的日誌記錄介面,稱為 “Sugared Logger”。Sugared Logger 提供了一種更簡便的方式來記錄日誌,適合那些不需要嚴格結構化日誌的場景。
Sugared Logger(糖化日誌記錄器)是一種在使用上更靈活、語法更簡潔的日誌記錄器。與 zap 的原生結構化日誌記錄器相比,Sugared Logger 提供了類似於 fmt.Printf 風格的方法,這使得記錄日誌更為簡便,但在效能上略有損失。
下面是一個使用的例子:
func TestLogZapSugar(t *testing.T) {
logger, _ := zap.NewProduction() // 建立一個新的 Logger 例項
defer logger.Sync() // 確保緩衝區中的日誌條目被重新整理
sugar := logger.Sugar() // 使用 Sugar 方法建立一個新的 Logger 例項
sugar.Infow("呼叫失敗", // 使用 Sugar 方法記錄日誌
"方法", "FunTester",
"呼叫次數", 3,
"時間單位", time.Second,
)
sugar.Infof("呼叫方法失敗 %s", "FunTester") // 使用 Sugar 方法記錄日誌
}
下面是日誌輸出:
=== RUN TestLogLevel
2024-06-02T14:57:28.298+0800 INFO test/zap_test.go:62 This is a custom logger info message {"category": "custom", "counter": 1}
2024-06-02T14:57:28.299+0800 WARN test/zap_test.go:66 This is a custom logger warning message
2024-06-02T14:57:28.299+0800 ERROR test/zap_test.go:67 This is a custom logger error message
2024-06-02T14:57:28.299+0800 INFO test/zap_test.go:68 This is a structured log message {"key1": "value1", "key2": 42}
--- PASS: TestLogLevel (0.00s)
PASS
這樣看起來是不是就更加如何常見的日誌格式了條例清理,不同的資訊按列顯示。
日誌等級
下面我們來演示一下如何更加精細化使用日誌等級,將超過某個等級的日誌輸出到控制檯上。程式碼如下:
func TestLogLevel(t *testing.T) {
encoderConfig := zapcore.EncoderConfig{ // 建立編碼配置
TimeKey: "T", // 時間鍵
LevelKey: "L", // 日誌級別鍵
NameKey: "log", // 日誌名稱鍵
CallerKey: "C", // 日誌呼叫鍵
MessageKey: "msg", // 日誌訊息鍵
StacktraceKey: "stacktrace", // 堆疊跟蹤鍵
LineEnding: zapcore.DefaultLineEnding, // 行結束符,預設為 \n EncodeLevel: zapcore.CapitalLevelEncoder, // 日誌級別編碼器,將日誌級別轉換為大寫
EncodeTime: zapcore.ISO8601TimeEncoder, // 時間編碼器,將時間格式化為 ISO8601 格式
EncodeDuration: zapcore.StringDurationEncoder, // 持續時間編碼器,將持續時間編碼為字串
EncodeCaller: zapcore.ShortCallerEncoder, // 呼叫編碼器,顯示檔名和行號
}
encoder := zapcore.NewConsoleEncoder(encoderConfig) // 建立控制檯編碼器,使用編碼配置
atomicLevel := zap.NewAtomicLevel() // 建立原子級別,用於動態設定日誌級別
atomicLevel.SetLevel(zap.InfoLevel) // 設定日誌級別,只有 Info 級別及以上的日誌才會輸出
core := zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), atomicLevel) // 將日誌輸出到標準輸出
logger := zap.New(core, zap.AddCaller(), zap.Development()) // 建立 Logger,新增呼叫者和開發模式
defer logger.Sync()
logger.Warn("列印警告日誌")
logger.Error("列印錯誤日誌")
logger.Info("列印結構化日誌",
zap.String("key1", "FunTester"),
zap.Int("key2", 22),
)
}
控制檯輸出如下:
=== RUN TestLogLevel
2024-06-02T15:29:40.686+0800 WARN test/zap_test.go:61 列印警告日誌
2024-06-02T15:29:40.687+0800 ERROR test/zap_test.go:62 列印錯誤日誌
2024-06-02T15:29:40.687+0800 INFO test/zap_test.go:63 列印結構化日誌 {"key1": "FunTester", "key2": 22}
--- PASS: TestLogLevel (0.00s)
PASS
可以看到,info 以上的日誌輸出到控制檯了。
日誌檔案
之前我們案例中都沒有設定將日誌輸出到檔案,下面我們來學習將日誌輸入到日誌檔案中的應用。
func TestLogFile(t *testing.T) {
logDir := "logs" // 日誌目錄,不存在則建立
if err := os.MkdirAll(logDir, 0755); err != nil { // 建立日誌目錄
panic(err)
}
logFile := filepath.Join(logDir, "app.log") // 日誌檔案,不存在則建立
file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // 建立日誌檔案
if err != nil {
panic(err)
}
encoderConfig := zapcore.EncoderConfig{ // 建立編碼配置
TimeKey: "T", // 時間鍵
LevelKey: "L", // 日誌級別鍵
NameKey: "log", // 日誌名稱鍵
CallerKey: "C", // 日誌呼叫鍵
MessageKey: "msg", // 日誌訊息鍵
StacktraceKey: "stacktrace", // 堆疊跟蹤鍵
LineEnding: zapcore.DefaultLineEnding, // 行結束符,預設為 \n EncodeLevel: zapcore.CapitalLevelEncoder, // 日誌級別編碼器,將日誌級別轉換為大寫
EncodeTime: zapcore.ISO8601TimeEncoder, // 時間編碼器,將時間格式化為 ISO8601 格式
EncodeDuration: zapcore.StringDurationEncoder, // 持續時間編碼器,將持續時間編碼為字串
EncodeCaller: zapcore.ShortCallerEncoder, // 呼叫編碼器,顯示檔名和行號
}
encoder := zapcore.NewJSONEncoder(encoderConfig) // 建立 JSON 編碼器
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig) // 建立控制檯編碼器
writeSyncer := zapcore.AddSync(file) // 建立 WriteSyncer consoleWriteSyncer := zapcore.AddSync(os.Stdout) // 建立控制檯 WriteSyncer atomicLevel := zap.NewAtomicLevel() // 建立原子級別
atomicLevel.SetLevel(zap.InfoLevel) // 設定日誌級別
core := zapcore.NewCore(encoder, writeSyncer, atomicLevel) // 建立 Core,將日誌輸出到檔案
consoleCore := zapcore.NewCore(consoleEncoder, consoleWriteSyncer, atomicLevel)
combinedCore := zapcore.NewTee(core, consoleCore) // 建立多個 Core,將日誌同時輸出到檔案和控制檯
logger := zap.New(combinedCore, zap.AddCaller(), zap.Development()) // 建立 Logger,新增呼叫者和開發模式
defer logger.Sync() // 確保緩衝區中的日誌條目被重新整理
logger.Warn("列印警告日誌")
logger.Error("列印錯誤日誌")
logger.Info("列印結構化日誌",
zap.String("key1", "FunTester"),
zap.Int("key2", 22),
)
}
控制檯輸出:
=== RUN TestLogFile
2024-06-02T15:40:30.260+0800 WARN test/zap_test.go:103 列印警告日誌
2024-06-02T15:40:30.261+0800 ERROR test/zap_test.go:104 列印錯誤日誌
2024-06-02T15:40:30.261+0800 INFO test/zap_test.go:105 列印結構化日誌 {"key1": "FunTester", "key2": 22}
--- PASS: TestLogFile (0.01s)
PASS
日誌檔案內容:
{"L":"WARN","T":"2024-06-02T15:40:30.260+0800","C":"test/zap_test.go:103","msg":"列印警告日誌"}
{"L":"ERROR","T":"2024-06-02T15:40:30.261+0800","C":"test/zap_test.go:104","msg":"列印錯誤日誌"}
{"L":"INFO","T":"2024-06-02T15:40:30.261+0800","C":"test/zap_test.go:105","msg":"列印結構化日誌","key1":"FunTester","key2":22}
日誌分割
在實際的專案當中,我們通常會對日誌進行分割(比如按大小分割),下面我們來演示一下使用 zap 框架時,進行日誌分割的例子。
func TestLogFileLumberjack(t *testing.T) {
writeSyncer := zapcore.AddSync(&lumberjack.Logger{ // 建立 WriteSyncer,使用 lumberjack.Logger,支援日誌切割
Filename: "logs/app.log",
MaxSize: 10, // 每個日誌檔案最大 10 MB MaxBackups: 5, // 保留最近的 5 個日誌檔案
MaxAge: 30, // 保留最近 30 天的日誌
Compress: true, // 舊日誌檔案壓縮
})
encoderConfig := zapcore.EncoderConfig{ // 建立編碼配置
TimeKey: "T", // 時間鍵
LevelKey: "L", // 日誌級別鍵
NameKey: "log", // 日誌名稱鍵
CallerKey: "C", // 日誌呼叫鍵
MessageKey: "msg", // 日誌訊息鍵
StacktraceKey: "stacktrace", // 堆疊跟蹤鍵
LineEnding: zapcore.DefaultLineEnding, // 行結束符,預設為 \n EncodeLevel: zapcore.CapitalLevelEncoder, // 日誌級別編碼器,將日誌級別轉換為大寫
EncodeTime: zapcore.ISO8601TimeEncoder, // 時間編碼器,將時間格式化為 ISO8601 格式
EncodeDuration: zapcore.StringDurationEncoder, // 持續時間編碼器,將持續時間編碼為字串
EncodeCaller: zapcore.ShortCallerEncoder, // 呼叫編碼器,顯示檔名和行號
}
encoder := zapcore.NewJSONEncoder(encoderConfig) // 建立 JSON 編碼器
atomicLevel := zap.NewAtomicLevel() // 建立原子級別
atomicLevel.SetLevel(zap.InfoLevel) // 設定日誌級別
core := zapcore.NewCore(encoder, writeSyncer, atomicLevel) // 建立 Core,將日誌輸出到檔案
logger := zap.New(core, zap.AddCaller(), zap.Development()) // 建立 Logger,新增呼叫者和開發模式
defer logger.Sync() // 確保緩衝區中的日誌條目被重新整理
logger.Warn("列印警告日誌")
logger.Error("列印錯誤日誌")
logger.Info("列印結構化日誌",
zap.String("key1", "FunTester"),
zap.Int("key2", 22),
)
}
控制檯日誌列印和檔案分割效果這裡就不展示了。各位有興趣可以自測一波。
- 2021 年原創合集
- 2022 年原創合集
- 2023 年原創合集
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go、Python
- 單元&白盒&工具合集
- 測試方案&BUG&爬蟲&UI 自動化
- 測試理論雞湯
- 社群風采&影片合集