天天看點

zap 自定義日志格式_整潔架構(Clean Architecture)的Go微服務: 日志管理

點選上方藍色“Go語言中文網”關注我們,領全套Go資料,每天學習 Go 語言

作者:倚天碼農,授權釋出 

原文連結:https://segmentfault.com/a/1190000021479989

良好的日志記錄可以提供豐富的日志資料,便于在調試時發現問題,進而大大提高編碼效率。記錄器提供的自動化資訊越多越好,日志資訊也需要以簡潔的方式呈現,便于找到重要的資料。

日志需求

  1. 無需修改業務代碼即可切換到其他日志庫
  2. 不需直接依賴任何日志庫
  3. 整個應用程式隻有一個日志庫的全局執行個體,是以你可以在一個位置更改日志配置并将其應用于整個程式。
  4. 可以在不修改代碼的情況下輕松更改日志記錄選項,例如,日志級别
  5. 能夠在程式運作時動态更改日志級别

資源句柄:為什麼日志記錄與資料庫不同

當應用程式需要處理外部資源時,例如資料庫,檔案系統,網絡連接配接, SMTP伺服器時,它通常需要一個資源句柄(Resource Handler)。在依賴注入中,容器建立一個資源句柄并将其注入每個業務函數,是以它可以使用資源句柄來通路底層資源。在此應用程式中,資源句柄是一個接口,是以業務層不會直接依賴于資源句柄的任何具體實作。資料庫和gRPC連結都以這種方式處理。

但是,日志記錄器稍有不同,因為幾乎每個函數都需要它,但資料庫不是。在Java中,我們為每個Java類初始化一個記錄器(Logger)執行個體。Java日志記錄架構使用層次關系來管理不同的記錄器,是以它們從父日志記錄器繼承相同的日志配置。在Go中,不同的記錄器之間沒有層次關系,是以你要麼建立一個記錄器,要麼具有許多彼此不相關的不同記錄器。為了獲得一緻的日志記錄配置,最好建立一個全局記錄器并将其注入每個函數。但者将需要做很多工作,是以我決定在一個中心位置建立一個全局記錄器,每個函數可以直接引用它。

為了不将應用程式緊密綁定到特定的記錄器,我建立了一個通用的記錄器接口,是以應用程式對于具體的記錄器透明的。以下是記錄器(Logger)接口。

因為每個檔案都依賴于日志記錄,很容易産生循環依賴,是以我在“容器”包裡面建立了一個單獨的子包“logger”來避免這個問題。它隻有一個“Log”變量和“Logger”接口。每個檔案都通過這個變量和接口通路日志功能。

記錄器封裝

支援一個日志庫的标準方法(例如ZAP¹或Logrus²) 是建立一個封裝來實作已經建立的記錄器接口。這很簡單,以下是代碼。

但是日志記錄存在一個問題。日志記錄的一個功能是在日志消息中列印記錄者名字。在對接口封裝之後,方法的調用者不是列印日志的程式,而是封裝程式。要解決該問題,你可以直接更改日志庫的源代碼,但在更新日志庫時會導緻相容性問題。最終的解決方案是要求日志記錄庫建立一個新功能,該功能可以根據方法是否使用封裝來傳回合适的調用方。

為了讓代碼現在能正常工作,我走了捷徑。因為ZAP和Logrus之間的大多數函數簽名是相似的,是以我提取了常用的簽名并建立了一個共享接口,因為兩個日志庫都已經有了這些函數,它們自動實作這些接口。Go接口設計的優點在于,你可以先建立具體實作,然後再建立接口,如果函數簽名互相比對,則自動實作接口。這有點作弊,但非常有效。如果要用的記錄器不支援公共的接口,則還是要對它進行封裝, 這樣就隻能暫時先犧牲調用者功能或修改源代碼。

日志庫比較

不同的日志庫提供不同的功能,其中一些功能對于調試很重要。

需要記錄的重要資訊(需要以下資料):

  1. 檔案名和行号
  2. 方法名稱和調用檔案名
  3. 消息記錄級别
  4. 時間戳
  5. 錯誤堆棧跟蹤
  6. 自動記錄每個函數調用包括參數和結果

我希望日志庫自動提供這些資料,例如調用方法名稱,而不編寫顯式代碼來實作。對于上述6個功能,目前沒有日志庫提供#6,但它們都提供1到5個中的部分或全部。我嘗試了兩個非常流行的日志庫Logrus和ZAP。Logrus提供了所有功能,但是我的控制台上的格式不正确(它在我的Windows控制台上顯示“ n t”而不是新行)并且輸出格式不像ZAP那樣幹淨。ZAP不提供#2,但其他一切看起來都不錯,是以我決定暫時使用它。

令人驚訝的是,本程式被證明是一個非常好的工具來測試不同的日志庫,因為你可以切換到不同的日志庫來比較輸出結果,而隻需要更改配置檔案中的一行。這不是本程式的功能,而是一個好的副作用。

實際上,我最需要的功能是自動記錄每個函數調用包括參數和結果(#6),但是還沒有日志庫提供該功能提供。我希望将來能夠得到它。

錯誤(error)處理

錯誤處理與日志記錄直接相關,是以我也在這裡讨論一下。以下是我在處理錯誤時遵循的規則。

1.使用堆棧跟蹤建立錯誤

2.使用堆棧跟蹤列印錯誤

你需要使用“logger.Log.Errorf(”%+vn“,err)”或“fmt.Printf(”%+vn“,err)”以便列印堆棧跟蹤資訊,關鍵是“+v”選項(當然你必須已經使用#1)。

3.隻有頂級函數才能處理錯誤

“處理”表示記錄錯誤并将錯誤傳回給調用者。因為隻有頂級函數處理錯誤,是以錯誤隻在程式中記錄一次。頂層的調用者通常是面向使用者的程式,它是使用者界面程式(UI)或另一個微服務。你希望記錄錯誤消息(是以你的程式中具有記錄),然後将消息傳回到UI或其他微服務,以便他們可以重試或對錯誤執行某些操作。

4.所有其他級别函數應隻是将錯誤傳播到較進階别

底層或中間層函數不要記錄或處理錯誤,也不要丢棄錯誤。你可以向錯誤中添加更多資料,然後傳播它。當出現錯誤時,你不希望停止整個應用程式。

恐慌(Panic)

除了在本地的“main.go”之外,我從未使用過恐慌(Panic)。它更像是一個bug而不是一個功能。在讓我們談談日志⁴中,Dave Cheney寫道“人們普遍認為應用庫不應該使用恐慌”。另一個錯誤是log.Fatal,它具有與恐慌相同的效果,也應該被禁止。“log.Fatal”更糟糕,它看起來像一個日志,但是在輸出日志後它“恐慌”,這違反了單一責任規則。

恐慌有兩個問題。首先,它與錯誤的處理方式不同,但它實際上是一個錯誤,一個錯誤的子類型。現在,錯誤處理代碼需要處理錯誤和恐慌,例如事務處理代碼⁵中的錯誤處理代碼。其次,它會停止應用程式,這非常糟糕。隻有頂級主要制程式才能決定如何處理錯誤,所有其他被調用的函數應該隻将錯誤傳播到上層。特别是現在,服務網格層(Service Mesh)可以提供重試等功能,恐慌使其更加複雜。

如果你正在調用第三方庫并且它在代碼中産生恐慌,那麼為了防止代碼停止,你需要截獲恐慌并從中恢複。以下是代碼示例,你需要為每個可能發生恐慌的頂級函數執行此操作(在每個函數中放置“defer catchPanic()”)。在下面的代碼中,我們有一個函數“catchPanic”來捕獲并從恐慌中恢複。函數“RegisterUser”在代碼的第一行調用“defer catchPanic()”。有關恐慌的詳細讨論,請參閱此處⁶。

結論

良好的日志記錄可以使程式員更有效。你希望使用堆棧跟蹤記錄錯誤。隻有頂級函數才能處理錯誤,所有其他級别函數隻應将錯誤傳播到上一級。不要使用恐慌。

源程式

完整的源程式連結 https://github.com/jfeng45/servicetmpl

索引

[1] zap

[2] Logrus

[3] Stack traces and the errors package

[4] Let’s talk about logging

[5] database/sql Tx — detecting Commit or Rollback

[6] On the uses and misuses of panics in Go

不堆砌術語,不羅列架構,不迷信權威,不盲從流行,堅持獨立思考

推薦閱讀

  • 整潔架構(Clean Architecture)的Go微服務: 設計原則

喜歡本文的朋友,歡迎關注“Go語言中文網”:

zap 自定義日志格式_整潔架構(Clean Architecture)的Go微服務: 日志管理

Go語言中文網啟用微信學習交流群,歡迎加微信:274768166,投稿亦歡迎