前言

很多人一開始寫 log,都是在程式壞掉時才想到:

1
Console.WriteLine("error");

或是:

1
Console.WriteLine(ex.Message);

這當然比完全沒有 log 好,但如果系統真的在線上環境出問題,這種資訊通常還是不夠。

log 的價值不只是「看到錯誤」,更重要的是幫助我們還原當時發生了什麼事。

Log 要能串起流程

假設使用者回報:「我按送出之後畫面一直失敗。」

如果 log 只有一行:

1
System error

那幾乎沒有辦法判斷問題在哪。

比較有幫助的 log,至少要能讓我們串起流程:

1
2
3
4
5
Start creating order. userId=123
Validate order request completed. userId=123
Call payment service. userId=123, orderId=456
Payment service timeout. userId=123, orderId=456
Create order failed. userId=123, orderId=456

這樣就能看出問題大概發生在付款服務逾時,而不是訂單驗證或資料庫寫入。

Log Level 要分清楚

常見 log level 大概可以這樣理解:

Level 使用情境
Debug 開發或排查細節,正式環境通常不會大量開啟
Info 系統正常流程中的重要事件
Warning 發生異常狀況,但系統仍可繼續運作
Error 功能失敗或需要工程師追查的錯誤
Critical 系統嚴重異常,可能影響大範圍服務

如果全部都用 Error,真正重要的錯誤就會被淹沒。

如果全部都用 Info,正式環境的 log 量可能會太大。

所以 log level 的重點是:讓訊息的重要程度可以被快速判斷。

Log 要放有意義的上下文

只記錄錯誤訊息通常不夠。

例如:

1
Payment failed

這句話知道付款失敗,但不知道是哪一筆訂單、哪個使用者、哪個付款管道。

比較好的方式是加上上下文:

1
Payment failed. userId=123, orderId=456, provider=ExamplePay

常見可以放進 log 的資訊有:

  • user id
  • order id
  • request id 或 trace id
  • 外部服務名稱
  • 重要參數,但不包含敏感資訊

這些資訊可以讓我們更快定位問題。

不要把敏感資料寫進 Log

log 很有用,但也很危險。

下面這些內容不應該直接寫進 log:

  • 密碼
  • token
  • 信用卡資訊
  • 身分證字號
  • 完整個資
  • 任何不該被工程師隨意看到的敏感資料

有時候只是為了 debug 方便,順手把整個 request body 印出來,但如果裡面有敏感欄位,就會變成資安風險。

比較好的做法是只記錄必要資訊,敏感欄位遮蔽或完全不記。

錯誤 Log 要保留 Exception

發生 exception 時,不要只記錄 ex.Message

因為 ex.Message 通常只有一句簡短描述,缺少 stack trace。

比較好的做法是把完整 exception 一起交給 logging 工具處理。

概念上像這樣:

1
logger.LogError(ex, "Create order failed. userId={UserId}, orderId={OrderId}", userId, orderId);

這樣 log 裡會同時有錯誤訊息、stack trace,以及我們補上的上下文。

未來排查問題時會省非常多時間。

結語

好的 log 不是越多越好,而是要在出事時提供足夠線索。

我會用這幾個原則檢查:

  • 是否能看出流程走到哪一步?
  • 是否有足夠上下文定位問題?
  • log level 是否合理?
  • 是否避免記錄敏感資訊?
  • exception 是否保留完整 stack trace?

平常寫 log 多花一點心思,線上出問題時就少一點痛苦。