結論先行
正式環境的 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 與
Authorizationheader。 - 使用者上傳的原始檔案內容。
必要時使用內部 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。