6.3 結構化日誌欄位設計
結構化日誌欄位的核心目標是讓 log 可查詢、可聚合、可追蹤。Message 給人讀,欄位給系統查;重要資訊應放在穩定欄位,不應只藏在自由文字裡。
本章目標
學完本章後,你將能夠:
- 設計穩定 log schema
- 用
layer、request_id、event_type、reason支援查詢 - 區分 message 與 structured fields 的責任
- 避免重複記錄同一個錯誤
- 避免把敏感資料寫進 log
【觀察】自由文字 log 很難查詢
Log 設計的核心問題是事故發生時需要快速查詢。若所有資訊都在 message 裡,查詢只能依賴模糊字串。
不穩定 log:
1logger.Info("event accepted for user 123 request abc")這行給人看可以,但系統很難穩定查 request_id=abc 或 user_id=123。不同工程師改字句後,查詢就可能失效。
結構化 log:
1logger.Info("event accepted",
2 "layer", "http",
3 "request_id", requestID,
4 "user_id", userID,
5 "event_type", event.Type,
6)Message 描述發生什麼事,欄位提供可查詢資料。這是 log schema 的基本分工。
【判讀】log schema 是查詢合約
Log schema 的核心規則是欄位名稱與值集合要穩定。request_id、requestID、rid 混用會讓查詢與儀表板變得困難。
常用欄位:
| 欄位 | 用途 |
|---|---|
layer | 問題發生在哪個系統層 |
request_id | 串起單次 HTTP request |
event_id | 串起事件處理流程 |
event_type | 聚合某類 domain event |
client_id | 查 WebSocket client 行為 |
topic | 查訂閱或推送範圍 |
reason | 聚合失敗原因 |
error | 保存錯誤文字 |
欄位不需要很多,但要一致。穩定欄位能讓除錯從「讀一堆文字」變成「查一組條件」。
【執行】layer 表示發生位置
layer 的核心用途是標示 log 來自哪個系統層,協助工程師快速縮小問題範圍。
1logger.Warn("queue full",
2 "layer", "worker",
3 "queue", "events",
4 "reason", "buffer_full",
5)常見 layer:
httpwebsocketworkerrepositoryruntimediagnostics
名稱不需要多,但應穩定。若 worker、background、job_runner 混用,查詢就會變麻煩。
【策略】correlation ID 串起一次流程
Correlation ID 的核心目標是把同一次請求或同一個事件流串起來。HTTP request 常用 request_id,背景事件可以用 event_id 或 trace_id。
1func WithRequestLog(r *http.Request, logger *slog.Logger) *slog.Logger {
2 requestID := r.Header.Get("X-Request-ID")
3 if requestID == "" {
4 requestID = uuid.NewString()
5 }
6
7 return logger.With("request_id", requestID)
8}後續 handler、service、repository 都使用帶有 request_id 的 logger。查詢單次流程時,不需要靠時間範圍猜哪些 log 相關。
Correlation ID 不應包含敏感資料。它是追蹤用識別碼,不是使用者資料容器。
【執行】reason 欄位讓失敗可統計
reason 的核心用途是把錯誤原因變成可聚合分類。Message 可以給人讀,reason 給查詢與統計使用。
1logger.Warn("reject event",
2 "layer", "http",
3 "reason", "invalid_payload",
4 "event_type", event.Type,
5)穩定 reason 可以回答「最近一小時最多的拒絕原因是什麼」。如果原因只寫在 message 中,查詢會依賴模糊字串比對。
Reason 值應像 enum 一樣維持小集合,例如:
invalid_payloadqueue_fullpermission_deniedtimeoutclient_disconnecteddependency_unavailable
reason 應維持小集合分類,完整錯誤應放在 error 欄位。這樣監控可以穩定聚合原因,工程師仍能從錯誤欄位取得診斷細節。
【判讀】錯誤只在負責處理的邊界記一次
錯誤日誌的核心風險是同一個錯誤被每一層都記一次。這會放大噪音,讓事故時很難看出真正的失敗點。
反模式:
1logger.Error("repository failed", "error", err)
2return fmt.Errorf("save notification: %w", err)上層又記一次:
1logger.Error("request failed", "error", err)較清楚的做法是底層 wrap error,上層在決定 response 或重試策略的邊界記錄一次:
1if err := service.Create(ctx, cmd); err != nil {
2 logger.Warn("create notification failed",
3 "layer", "http",
4 "reason", reasonOf(err),
5 "error", err,
6 )
7 writeError(w, err)
8 return
9}底層若有必要補充脈絡,優先透過 error wrapping 或 structured error,而不是每層都 Error log。
【策略】敏感資料不進 log
Log 欄位設計的核心安全邊界是只記錄診斷必要資料。token、密碼、完整 cookie、完整個資與機密 payload 都屬於應排除資料;結構化 log 很容易被集中保存與搜尋,敏感資料一旦進入 log,清理成本很高。
可以記錄:
1logger.Info("user login",
2 "user_id", user.ID,
3)應排除:
1logger.Info("user login",
2 "password", password,
3 "token", token,
4)若需要診斷 payload,可記錄長度、hash、欄位是否存在,而不是完整內容。
1logger.Debug("payload received",
2 "payload_bytes", len(body),
3 "payload_sha256", checksum(body),
4)所有會被收集或保存的 log 都應遵守同一套資料保護規則。Debug log 也會進入檔案、集中式 log 或診斷封包,因此不能把它當成敏感資料的例外通道。
【測試】log 欄位可以用 handler 驗證
Log schema 的測試核心是確認重要欄位存在,避免未來重構時消失。
1func TestLogAttrsForEvent(t *testing.T) {
2 event := DomainEvent{
3 ID: "evt_1",
4 Type: "notification.created",
5 SubjectID: "notification_1",
6 }
7
8 attrs := LogAttrsForEvent(event)
9
10 if !hasAttr(attrs, "event_id", "evt_1") {
11 t.Fatalf("event_id attr missing")
12 }
13 if !hasAttr(attrs, "event_type", "notification.created") {
14 t.Fatalf("event_type attr missing")
15 }
16}不需要測整行 log 字串。測穩定欄位即可,message 文字可以保留一定調整空間。
本章不處理
本章先處理 Go 服務內部的 structured log schema;集中式平台、欄位標準與隱私治理,會在下列章節再往外延伸:
和 Go 教材的關係
這一章承接的是 structured recording、event log 與 observability pipeline;如果你要先回看語言教材,可以讀:
- Go:如何新增結構化記錄欄位
- Go:結構化日誌
- Go:如何新增一種 domain event
- Go:Observability pipeline、metrics 與 tracing
- Backend:可觀測性平台
- Go 入門:log/slog
- Go 入門:如何新增結構化記錄欄位
小結
結構化日誌的價值在於穩定欄位:layer 定位層級,request_id 串起請求,event_id 串起事件,event_type 支援聚合,reason 支援失敗分類。Message 給人讀,欄位給系統查。好的 log schema 能讓除錯從猜測變成查詢,同時避免敏感資料外洩與錯誤重複記錄。