← 所有文章2026.10.21 · 軟體設計

正式環境的 Log 該怎麼設計?從字串輸出到可追蹤事件

好 log 不是印得多,而是能沿著 request、job 與業務事件還原發生了什麼,同時控制敏感資料、儲存成本與告警噪音。

結論先行

正式環境的 log 不是給工程師臨時 console.log 的文字檔,而是系統產生的事件資料。它至少應該回答:

  • 什麼時間發生了什麼事件?
  • 發生在哪個服務、版本與環境?
  • 屬於哪個 request、job 或 message?
  • 影響哪個業務物件?
  • 結果是成功、失敗、重試還是延後?
  • 失敗發生在哪個依賴,下一步該由誰處理?

真正的 best practice 可以濃縮成四件事:結構化、可關聯、不洩密、能採取行動。

Log、Metric 和 Trace 各自回答不同問題

三種 observability signal 不應互相替代:

Signal 適合回答
Metric 問題有沒有發生、影響多大、趨勢如何
Trace 一次 request 經過哪些服務,時間花在哪裡
Log 某個時間點發生了什麼離散事件,以及當時的上下文

例如錯誤率 metric 告訴我們系統正在異常;trace 找出延遲集中在付款供應商;log 則說明某次呼叫使用哪個 provider、得到哪一類結果,以及系統採取了什麼動作。

使用 Structured Logging

不要把所有資訊拼成一句只適合人看的字串:

Payment failed for order 123 because provider timeout

應該輸出固定欄位的 JSON:

{
  "timestamp": "2026-10-04T10:30:12.431Z",
  "severity": "ERROR",
  "event": "payment.provider_request_failed",
  "service": "payment-api",
  "serviceVersion": "2026.10.04.2",
  "environment": "production",
  "traceId": "4f91...",
  "spanId": "0ab3...",
  "requestId": "req_...",
  "orderId": "ord_...",
  "provider": "provider-a",
  "operation": "capture",
  "outcome": "retry_scheduled",
  "errorType": "UPSTREAM_TIMEOUT",
  "durationMs": 1850
}

這樣才能依 event、service、errorType 或 orderId 查詢、統計和告警,而不必靠全文搜尋猜字串。Google Cloud 也將 JSON payload 定義為 structured log,欄位可以被查詢與索引;純文字只能搜尋整段內容。Google Cloud Structured Logging

OpenTelemetry 的 log data model 同樣把 timestamp、severity、body、resource、attributes、TraceId 與 SpanId 定義為標準欄位。OpenTelemetry Logs Data Model

讓整條流程可以串起來

在單體系統裡,request ID 可能就夠;進入 microservice、queue 和非同步 job 後,需要傳遞更完整的 context:

  • traceId:同一次端到端流程共用。
  • spanId:流程中的單一步驟。
  • requestId:一次 HTTP request。
  • messageId:一則 queue message。
  • jobId:一次背景工作。
  • correlationId:把不同協定與系統串在一起。
  • causationId:這個事件是由哪個前置事件造成。
flowchart LR
  A[API<br/>traceId: T1] --> Q[Queue message<br/>traceId: T1]
  Q --> W[Worker<br/>traceId: T1]
  W --> P[Provider call<br/>traceId: T1]

這些 ID 應放在結構化欄位,而不是埋在 message 裡。OpenTelemetry 建議把 TraceId 與 SpanId 帶入 log,才能將不同服務的紀錄直接關聯到同一條 trace。OpenTelemetry Log Correlation

應該記錄哪些事件?

不是每一行程式都要留下 log。優先記錄能還原系統行為的邊界事件:

Request 與外部整合

  • Request 完成結果、status code 和 duration。
  • 外部服務名稱、operation、回應分類與 latency。
  • Timeout、retry、circuit breaker 與 fallback。
  • 不要直接記錄完整 request/response body。

業務狀態變更

  • 訂單、付款、會員或文件從哪個狀態轉到哪個狀態。
  • 由誰或哪個系統觸發。
  • 使用哪個規則版本或決策原因。
  • 業務 ID,而不是整份物件內容。

非同步工作

  • Job/message 開始、完成、重試和進入 dead-letter queue。
  • Attempt number、queue delay 和 processing duration。
  • Idempotency 判定結果。

安全與稽核

  • 登入成功與失敗、權限拒絕。
  • 高風險資料存取、設定或權限變更。
  • Key rotation、secret access 與管理操作。
  • Audit log 應和一般 debug log 分開管理。

OWASP 建議依風險記錄 authentication、authorization、validation、系統錯誤、高風險操作與資料變更等事件,而不是盲目蒐集所有資料。OWASP Logging Cheat Sheet

絕對不要直接寫入 Log 的資料

Log 會被複製到 collector、搜尋平台、備份和分析系統,一旦寫入,比主資料庫更難完全刪除。因此應採 allow-list:先定義可以記錄的欄位,而不是輸出整個 object 後再期待遮罩全部秘密。

通常不應記錄:

  • Password、OTP、recovery code。
  • Access token、refresh token、session cookie、API key。
  • Encryption key、plaintext DEK、database connection string。
  • 完整信用卡、銀行帳戶或身分證件資料。
  • 不必要的 PII、醫療或其他受管制資訊。
  • 完整 request/response body 與 Authorization header。
  • 使用者上傳的原始檔案內容。

必要時使用內部 ID、tokenization 或 HMAC 後的穩定識別值進行關聯。不要只遮中間幾碼後就假設資料沒有風險。OWASP 明確建議移除、遮罩、hash 或加密 session identifier、token、密碼、key、付款資料及敏感個資。

同時要處理 log injection:使用結構化 logger、限制使用者輸入長度,並清理 CR、LF 等控制字元,避免攻擊者偽造下一行 log。

Log Level 要有固定語意

Level 建議語意
DEBUG 開發或短期診斷所需的細節;production 預設不大量開啟
INFO 正常但有意義的生命週期或業務事件
WARN 操作仍完成,但發生降級、重試或值得觀察的異常
ERROR 一項操作失敗,需要調查或已進入補償流程
FATAL Process 無法繼續,準備終止或重啟

同一個 exception 不要在 repository、service 和 controller 各記一次完整 stack trace。通常由最了解業務結果的邊界記錄一次,底層把 error 往上傳並保留 cause。否則一個錯誤會製造多個告警,讓事件數量失真。

另外,ERROR 不等於「程式拋過 exception」。如果 retry 已成功且不需人工處理,可能只是 WARN 或 metric;相反地,一項沒有 exception 的業務對帳差異也可能需要 ERROR 或 audit event。

集中收集,但不要讓 Logging 拖垮服務

常見做法是應用程式輸出 JSON 到 stdout,由 execution environment、agent 或 OpenTelemetry Collector 負責批次送往集中式平台:

flowchart LR
  A[Applications<br/>JSON to stdout] --> C[Agent / OTel Collector]
  C --> L[Central log store]
  L --> S[Search]
  L --> R[Rules / Alerts]
  L --> X[Archive]

應用程式的主要 request 不應同步等待遠端 log 平台寫入完成。Collector 中斷時,需要明確的 buffer、backpressure 和 drop policy;也要限制檔案大小與磁碟使用量,避免 logging failure 變成 production outage。

集中式 log store 需要:

  • 傳輸與靜態加密。
  • 最小讀取權限,以及查閱 audit。
  • 防止未授權修改或刪除。
  • 開發、production 與不同租戶的隔離。
  • 偵測 collector 停止、log pipeline backlog 與 ingestion failure。

Retention 和成本要依用途分層

所有 log 永久保存既昂貴,也增加資安與個資風險。Retention 應由用途決定:

  • 高頻 operational log:短期可搜尋,之後降到便宜 archive 或刪除。
  • Error 與 incident evidence:保存到足以支援調查與事故回顧。
  • Security/audit log:依風險、合約與法規使用較長 retention,並提高防竄改能力。
  • Debug log:限定時間開啟,自動到期。

可以 sample 高流量的正常 request,但不要任意 sample error、security event 或必要 audit trail。對高 cardinality 欄位也要小心:把完整 URL、任意使用者輸入或每次都不同的內容拿去建立 index,成本可能快速上升。

OWASP 建議 log 不得早於必要 retention 被刪除,也不應超過必要期限無限保存;實際期間應依法律、合約和業務目的制定。

告警應該對應行動

不要每出現一筆 ERROR 就通知所有人。比較好的做法是對可採取行動的條件告警:

  • 某類錯誤率在一段時間內超過 baseline。
  • Payment、login 或 checkout 的成功率下降。
  • Queue lag、retry 或 dead-letter 數量增加。
  • 某個版本部署後才開始出現的新 error type。
  • Log ingestion 突然停止。
  • 權限、key 或重要設定發生非預期變更。

每個 alert 最好附上 owner、影響、dashboard、查詢連結與 runbook。沒有負責人、無法採取行動的告警,只會累積 alert fatigue。

上線前檢查清單

  • Log 是否為 JSON 等結構化格式?
  • Event name 和欄位命名是否跨服務一致?
  • Trace ID 是否能跨 HTTP、queue 和 worker 傳遞?
  • 是否以 allow-list 避免 token、密碼、key 與 PII?
  • 是否能依業務 ID 還原一次狀態變更?
  • 同一個錯誤是否只在正確邊界記錄一次?
  • Logger 或 collector 失敗時,主服務是否仍能安全運行?
  • Log 權限、retention、archive 與刪除政策是否明確?
  • 是否監控 ingestion failure、drop rate 與儲存成本?
  • 每一項 alert 是否有人負責且有 runbook?

好的 logging 不追求「什麼都留下」。它保留剛好足以理解系統行為、追蹤責任與處理事件的證據,同時避免把秘密、噪音和無上限的儲存成本一起送進 log platform。