slog 實戰:文件日誌、輪轉與 kafka 集成

《slog 正式版來了:Go 日誌記錄新選擇![1]》一文發佈後,收到了很多讀者的反饋,意見集中在以下幾點:

這篇文章就是對上述問題進行補充說明的,供大家參考,希望能給大家帶去幫助。

1. 輸出日誌到文件

之所以《slog 正式版來了:Go 日誌記錄新選擇![2]》一文中使用的例子 [3] 都以 os.Stdout(標準輸出) 爲 log 輸出目的地,主要是因爲基於雲原生微服務架構模式下,應用都跑在容器中 (k8s[4] 的 pod 中),基本都是將 log 輸出到 Stdout,而不會寫入某個具體的本地文件。但如果應用是基於虛擬機或裸機部署,那麼將日誌寫入文件仍然是第一選項。

其實,使用 slog 內置的 TextHandler 和 JSONHandler 可以非常方便的將結構化的日誌寫入文件,因爲 slog.NewXXXHandler 函數的第一個參數是一個 io.Writer,這樣通過將一個文件的描述符傳遞給 NewXXXHandler,即可創建一個向文件寫入日誌的 Logger。我們看下面示例代碼:

//slog-in-action/log2file/main.go

package main
  
import (
    "log/slog"
    "os"
)

func main() {
    f, err := os.Create("foo.log")
    if err != nil {
        panic(err)
    }
    defer f.Close()
    logger := slog.New(slog.NewJSONHandler(f, nil))
    slog.SetDefault(logger)
    slog.Info("greeting", "say", "hello")
}

在這個示例中,我們創建了目標日誌文件 foo.log,並將其描述符 (*os.File) 傳給了 NewJSONHandler 函數,通過這種方式創建出來的 Logger 輸出的日誌內容將會被寫入 foo.log 文件中:

$go run main.go
$cat foo.log
{"time":"2023-09-02T19:38:45.441782+08:00","level":"INFO","msg":"greeting","say":"hello"}

這種方式應該可以滿足大多數 gopher 的需求了。

2. 日誌文件的管理

一旦將日誌寫入文件,後續就要對日誌文件進行管理,比如:日誌文件的輪轉、壓縮、歸檔以及定期清理 (騰出磁盤空間) 等。

關於如何對日誌文件管理的方案大致有這麼幾種。

第一種是藉助外部工具,比如在主流的 Linux 發行版上都有一個 logrotate[5] 工具程序,應用程序可以藉助該工具對應用輸出的日誌進行 rotate、壓縮、歸檔和刪除歷史歸檔日誌,這樣可大幅簡化應用的日誌輸出邏輯,應用僅需要將日誌輸出到一個具名文件中即可,其餘都交給 logrotate 處理。關於如何使用 logrotate,我在《寫 Go 代碼時遇到的那些問題 [第 1 期]》[6] 中有詳細說明,感興趣的朋友可以移步閱讀一下,這裏就不贅述了。

第二種就是 log 包自身支持。大多數 log 包都沒有將日誌文件管理作爲自己的功能 feature,slog 包也是如此,沒有原生提供此功能。

第三種就是通過支持 log 包相關插件接口的一些擴展包來支持。lumberjack[7] 就是這樣的一個插件包,它支持與很多知名的 log 包集成在一起實現對 log 文件的管理,比如 logrus、zap 等。我曾在《寫 Go 代碼時遇到的那些問題 [第 3 期] 》[8] 和《一文告訴你如何用好 uber 開源的 zap 日誌庫》[9] 兩篇文章中分別講解了 logrus 和 zap 與 lumberjack 集成在一起對日誌文件進行管理的方法。如果你對 lumberjack 不是很熟悉,建議你在繼續閱讀下面內容之前,溫習一下這兩篇文章。

在這一篇文章中,我們用示例來簡單說說如何將 slog 與 lumberjack 集成以實現對 log 文件的管理功能。看下面示例:

//slog-in-action/lumberjack/main.go

package main
  
import (
    "log/slog"

    "gopkg.in/natefinch/lumberjack.v2"
)

func main() {
    r := &lumberjack.Logger{
        Filename:   "./foo.log",
        LocalTime:  true,
        MaxSize:    1,
        MaxAge:     3,
        MaxBackups: 5,
        Compress:   true,
    }
    logger := slog.New(slog.NewJSONHandler(r, nil))
    slog.SetDefault(logger)

    for i := 0; i < 100000; i++ {
        slog.Info("greeting", "say", "hello")
    }
}

在這個示例中,我們看到:*lumberjack.Logger 實現了 io.Writer 接口,因爲只要將實例化後的 * lumberjack.Logger 以參數形式傳入 NewXXXHandler 即可完成 slog 與 lumberjack 的集成。至於日誌文件的管理行爲則是通過 lumberjack.Logger 實例化過程的字段賦值來定製的。比如這裏我們指定了目標日誌文件名 (Filename) 爲 "./foo.log",指定當文件達到 1M 字節時 (MaxSize) 進行 rotate,對 rotate 後的文件進行壓縮 (Compress),最多保留 5 個歸檔文件(MaxBackups) 以及歸檔文件最多保存 3 天 (MaxAge) 等。

運行上述示例程序後,我們將在當前目錄想得到如下文件:

$go run main.go
$ls
foo-2023-09-02T08-24-20.854.log.gz  foo-2023-09-02T08-24-20.979.log.gz foo-2023-09-02T08-24-21.098.log.gz  go.mod  main.go
foo-2023-09-02T08-24-20.918.log.gz  foo-2023-09-02T08-24-21.041.log.gz foo.log        go.sum

foo.log 是當前正在寫入的日誌文件,而其他帶有時間戳、以 gz 爲後綴的文件則是歸檔文件。由於有了 lumberjack 對日誌文件的管理,我們就不用再擔心日誌文件 size 過大、歸檔文件過多沒有清理而導致的磁盤被佔滿的問題了。

注:lumberjack.Logger 的各個屬性字段的配置要根據你的應用實際輸出日誌的情況、本地磁盤可用空間來確定。

3. 與 kafka 集成

在我們團隊的一個生產項目中,日誌是不落盤而直接寫入 kafka 的,關於這個事情,我在《Go 社區主流 Kafka 客戶端簡要對比》[10] 一文中也曾提到過,並給出了基於 zap 和不同 kafka 客戶端實現向 kafka 寫入日誌的方案。

slog 與 kafka 集成的思路也是類似的,不同的是定製 KafkaHandler 的方法,基於 slog,我們要讓 KafkaHandler 實現 slog.Handler 接口。在《slog 正式版來了:Go 日誌記錄新選擇![11]》一文中,我們給出了一個向 channel 寫入結構化日誌的示例 [12],KakfaHandler 完全可以借鑑其中的 ChanHandler,也是通過字節切片來承接 JSONHandler 寫出的日誌,不同的是將寫入 Channel 改爲通過 kafka client 寫入 Kafka! 在這裏我就不給出 KakfaHandler 的實現了,這個作業留給大家,記得實現 KafkaHandler 後,使用 slog/slogtest 對其正確性做一個測試!

注:注意在實現 KakfaHandler 時,考慮 goroutine 併發使用同一個基於 KafkaHandler 創建的 slog.Logger 的情況,也就是字節切片的併發訪問和共享的問題。

4. 日誌輸出的實踐建議

在《聊聊 Go 應用輸出日誌的工程實踐 [13]》一文中,我聊了一些在日常使用 log 時遇到的問題、解決方法以及 Go 團隊對 log 支持上的問題。log/slog 的正式發佈,一定程度上解決或改善了那篇文章中提到的部分問題。

此外,在讀者關心的日誌輸出內容方面有哪些實踐建議,我也總結了以下幾點:

1). 選擇合適的日誌級別。常見的日誌級別包括 DEBUG、INFO、WARNING 和 ERROR。在生產環境中,我們通常將日誌級別設置爲 WARNING 或 ERROR,最低是 info,不能再低了,避免打印過多日誌以影響應用性能。

2). 日誌級別要支持熱更新。在系統出現異常時,如果要做在線調試,支持熱更新的日誌級別就特別重要,我們可以在一個調試時間窗口將日誌級別下調至 info 或 debug,這樣可以抓取到一段時間的詳細日誌,以供調試和診斷參考。

3). 優先選結構化日誌。相對於文本日誌更適合人類閱讀,結構化日誌更適於機器解析、索引和查詢。大多數正常情況下,我們是不會去看日誌的,日誌都會被彙集到集中日誌中心存儲、管理並索引 (比如常見的 ELK 方案、近來的 grafana 的 PLG 方案 (Promtail, Loki and Grafana)[14] 等),以便於後續做查詢和展示。針對這樣的情況,顯然結構化日誌更適合。

4). 無論使用結構化還是文本形式日誌,日誌格式都要清晰易讀。每條日誌至少要打印時間、日誌級別、事件源、事件詳情等信息,對於固定的字段,要用屬性 (attribute) 來設置,以提高輸出性能。

5). 考慮到排查和診斷業務問題,通常會爲日誌添加上下文信息。比如:在日誌中增加關於當前用戶、請求 ID 等上下文信息等。但不應該在日誌中輸出用戶的隱私數據等敏感信息,要麼移除,要麼做打碼處理。

6). 考慮到監控和告警的需要,有些時候我們會對錯誤日誌進行監控,可能會在日誌中放置一些具有監控意義的特徵字段。

7). 對於日誌寫入文件的情況,就如本文前面提到的,要考慮日誌文件的管理:設置合理的分割輪轉日誌文件策略以及日誌文件的歸檔管理,避免日誌文件的無限增長對磁盤帶來的影響。

日誌輸出內容沒有 “固定標準”,需根據大家實際所處的業務環境以及相關要求確定。

5. 小結

本文是《slog 正式版來了:Go 日誌記錄新選擇![15]》一文的 “補充篇”,主要對將 slog 日誌如何寫入文件以及對文件的管理(輪轉、歸檔、清理等方案) 做了說明。對於將 slog 與外部系統 (如 kafka) 進行集成的思路做了點撥,最後還給出了一些關於日誌輸出實踐方面的參考意見,希望能幫助到大家!

本文涉及的示例代碼可以在這裏 [16] 下載。

Gopher Daily(Gopher 每日新聞) - https://gopherdaily.tonybai.com

我的聯繫方式:

參考資料

[1]  slog 正式版來了:Go 日誌記錄新選擇!: https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go

[2]  slog 正式版來了:Go 日誌記錄新選擇!: https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go

[3]  使用的例子: https://github.com/bigwhite/experiments/tree/master/slog-examples-go121

[4]  k8s: https://tonybai.com/tag/k8s

[5]  logrotate: https://github.com/logrotate/logrotate

[6]  《寫 Go 代碼時遇到的那些問題 [第 1 期]》: https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st

[7]  lumberjack: https://github.com/natefinch/lumberjack

[8]  《寫 Go 代碼時遇到的那些問題 [第 3 期] 》: https://tonybai.com/2018/04/06/the-problems-i-encountered-when-writing-go-code-issue-3rd

[9]  《一文告訴你如何用好 uber 開源的 zap 日誌庫》: https://tonybai.com/2021/07/14/uber-zap-advanced-usage

[10]  《Go 社區主流 Kafka 客戶端簡要對比》: https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients

[11]  slog 正式版來了:Go 日誌記錄新選擇!: https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go

[12]  向 channel 寫入結構化日誌的示例: https://github.com/bigwhite/experiments/tree/master/slog-examples-go121/demo4

[13]  聊聊 Go 應用輸出日誌的工程實踐: https://tonybai.com/2022/03/05/go-logging-practice

[14]  grafana 的 PLG 方案 (Promtail, Loki and Grafana): https://www.cncf.io/blog/2020/07/27/logging-in-kubernetes-efk-vs-plg-stack/

[15]  slog 正式版來了:Go 日誌記錄新選擇!: https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go

[16]  這裏: https://github.com/bigwhite/experiments/tree/master/slog-in-action

[17]  “Gopher 部落” 知識星球: https://public.zsxq.com/groups/51284458844544

[18]  鏈接地址: https://m.do.co/c/bff6eed92687

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7jS6E6d578QQIxvWKSux1Q