譯|Let’s talk about logging

來源:cyningsun.github.io/09-27-2019/…html

本文受 Nate Finch started on the Go Forum 的一個話題啓發。本文專一於 Go,可是若是你能看透,我認爲這裏提出的想法是普遍適用的。git

Why no love ?

Go 的 log package 沒有日誌級別,你必須本身手動添加 debug、info、warn 和 error 等前綴。另外,Go 的 日誌類型沒法以包爲基礎打開或者關閉這些不一樣級別。經過比較,讓咱們看一些第三方的替代品:程序員

Google 的 glog 提供如下級別:github

  • Info
  • Warning
  • Error
  • Fatal (終止程序)

看看另一個庫, 咱們爲 Juju 開發的 loggo,提供如下級別:golang

  • Trace
  • Debug
  • Info
  • Warning
  • Error
  • Critical

Loggo 還提供了根據每一個包調整日誌的詳細程度的功能。app

所以,以上兩個示例顯然受到其餘語言的其餘日誌庫的影響。實際上,它們的命令行能夠追溯到 syslog(3),甚至更早。我認爲他們是錯的。函數

我站相反的立場。我認爲_全部_日誌庫都很差,由於它提供了_太多_的功能。一系列使人困惑的選擇,使程序員必須清楚地思考如何與將來的讀者溝通,而讀者將要使用他們的日誌。oop

我認爲成功的日誌包須要的功能要少得多,固然須要的級別也更少。ui

Let’s talk about warnings

咱們從最簡單的一個開始。沒有人須要 warning 的日誌級別。this

沒有人閱讀 warning,由於按照定義,沒有出錯。也許未來會出問題,但這聽起來像是別人的問題。

此外,若是你在使用某種分級日誌,那麼爲何將級別設置爲 warning? 你能夠將級別設置爲 info 或 error。將級別設置爲 warning 是認可你可能正在以 warninng 級別打印錯誤日誌

消除 warning 級別,由於它既多是信息性的消息,也多是錯誤的狀況。(潛臺詞:語義不明確)

Let’s talk about fatal

Fatal 級別,有效打印消息日誌,而後調用 os.Exit(1)。原則上,這意味着:

  • 其餘goroutines中的defer語句不會運行。
  • 緩衝區不刷新。
  • 臨時文件和目錄不會被刪除。

實際上,log.Fatal 的詳細程度要比 panic 少,但在語義上卻等同於它。

廣泛認爲,庫不該該使用 panic1,可是若是調用 log.Fatal2 具備相同的效果,那麼固然也應該將其定爲非法。

解決此清理問題的建議是,能夠經過在日誌系統中註冊 shutdown handler,但如此致使日誌系統與發生清理操做的每一個位置之間緊密耦合。它也違反了關注點分離。

不要以 Fatal 級別寫日誌,而要將錯誤返回給調用方。若是錯誤一直冒泡到 main.main, 那麼退出以前,是處理全部清理操做的正確位置。

Let’s talk about error

錯誤處理和日誌密切相關,所以從表面上看,以 error 級別進行日誌打印應該很容易解釋。但,我不一樣意。

在 Go 中,若是函數或方法調用返回錯誤值,實際上您有兩個選擇:

  • 處理錯誤
  • 將錯誤返回給您的呼叫者(您能夠選擇包裝錯誤,但這對於本次討論並不重要)

若是您選擇經過打印日誌來處理錯誤,那麼按照定義,你已經處理了它,錯誤就不存在了。打印錯誤處理錯誤的行爲,意味着再也不適合將其打印爲錯誤日誌。

讓我嘗試用如下代碼片斷說服您:

err := somethingHard()
if err != nil {
        log.Error("oops, something was too hard", err)
        return err // what is this, Java ?
}
複製代碼

您永遠不該該在 error 級別打印任何內容的日誌,由於您應該處理錯誤,或者將其傳遞迴調用方。

準確地說,我並非說您不該該將發生的狀況打印日誌

if err := planA(); err != nil {
        log.Infof("could't open the foo file, continuing with plan b: %v", err)
        planB()
}
複製代碼

但實際上 log.Infolog.Error 有殊途同歸之妙。

我並非說不要打印錯誤日誌!相反,問題是,最小的日誌API是什麼?當提到錯誤時,我相信絕大多數打印日誌爲 error 級別的項目,都是經過這種簡單地方式完成的,由於它們與錯誤相關。實際上,它們只是提供信息,意味着能夠從API中刪除以 error 級別打印日誌。

What’s left ?

咱們已經排除了 warning,認爲不該該以 error 級別打印任何內容的日誌,而且代表只有應用程序的頂層應該具備某種 log.Fatal 行爲。還剩什麼 ?

我認爲只有兩種東西您應該打印日誌:

  1. 開發人員在開發或調試軟件時會關心的東西。
  2. 用戶在使用您的軟件時關心的東西。

顯然,分別是 debug 和 info 級別。

log.Info 應該簡單地將該行寫入日誌輸出。不該有將其關閉的選項,由於僅應告知用戶對他們有用的事情。若是發生_沒法_處理的錯誤,它將冒泡到 main.main, 程序終止的地方。必須在最後一條日誌消息前面插入 FATAL 前綴,或直接用 fmt.Fprintf 寫入 os.Stderr 所帶來的小小不便,不足以解釋日誌包須要添加 log.Fatal 方法。

log.Debug,則徹底不一樣。由開發人員或支持工程師控制。在開發過程當中,調試語句應足夠多,而沒必要求助於 trace 或 debug2 級別。日誌包應支持細粒度的控制,以啓用或禁用調試,而且僅在該包或可能更精細的範圍內的語句調試。

Wrapping up

若是這是推特民意調查,請您選擇

  • 打印日誌很重要
  • 打印日誌很難

可是事實是,打印日誌是二者兼而有之。解決這個問題的方法_必須_是解構和殘酷地消除沒必要要的干擾。

你怎麼看?這僅僅是瘋狂到足以工做,仍是純粹是瘋狂的?

Notes

  1. 一些庫可能使用 panic/recover 做爲內部控制流機制,但最重要的原則是它們必定不能讓這些控制流操做泄漏到程序包邊界以外。
  2. 具備諷刺意味的是,儘管缺乏 debug 級別的輸出,但 Go 標準日誌包同時具備 FatalPanic 功能。在此程序包中,致使程序忽然退出的功能數量超過了沒有退出功能的數量。
相關文章
相關標籤/搜索