Zap 高效能日誌庫實踐

FunTester發表於2024-06-11

Zap 是一個由 Uber 公司開源的結構化、高效能日誌記錄庫,旨在為 Go 語言提供一種快速、簡單且高效的日誌解決方案。它起源於 Uber 內部使用的日誌系統,後來於 2016 年開源,迅速獲得了 Go 社群的廣泛關注和應用。

Zap 的主要特點如下:

  1. 高效能:Zap 在設計時就非常注重效能,比標準庫 log 包快幾個數量級,即使在高併發場景下也能保持出色的效能表現。
  2. 結構化日誌:Zap 支援結構化日誌記錄,可以方便地記錄任意型別的欄位,而不僅限於字串,這有利於後期日誌分析和處理。
  3. 級別控制:Zap 提供了豐富的日誌級別控制,可以動態修改日誌級別,從而只輸出關鍵日誌或除錯日誌。
  4. 編碼支援:Zap 內建支援 JSON 和控制檯的日誌編碼,並提供了鉤子機制來擴充套件其他編碼格式。
  5. 日誌分割: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 自動化
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章