前一章已經說明如何在資料進入主要處理前檢查內容。不過,資料通過驗證後,仍可能因為目前狀態不允許操作、程式缺陷、執行資源不足或相依項目失敗而無法完成處理。
如果每個函式各自決定錯誤訊息與記錄內容,相同問題可能產生不同結果。程式也可能把內部例外直接顯示在輸出中,或留下大量無法互相關聯的文字。發生問題時,即使找到錯誤訊息,也不一定能確認它屬於哪一次操作、在哪個步驟發生,以及是否改變了保存狀態。
異常事件管理需要同時處理三項需求:對外提供穩定且安全的失敗結果、在內部保留足以追查原因的執行記錄,以及為重要操作留下可核對的稽核紀錄。本章會先建立錯誤分類與處理邊界,再說明結構化記錄(Structured Logging)、關聯識別碼(Correlation ID)、記錄工具與保存規則如何配合。
錯誤結果、例外與異常事件代表不同層次的資訊:
驗證失敗屬於正常契約的一部分時,可以直接回傳失敗結果,不必建立呼叫堆疊,也不必逐筆記為高層級事件。程式無法繼續執行時,則可以拋出例外,交由目前操作的處理邊界統一轉換與記錄。
目標系統可以先使用下列四類失敗建立共同判斷方式:
| 類型 | 常見情況 | 對外處理 | 內部處理 |
|---|---|---|---|
| 輸入驗證錯誤 | 必要欄位缺漏、格式錯誤或內容超出範圍。 | 回傳穩定錯誤碼與可修正欄位,不顯示原始程式資訊。 | 按照分析需求記錄規則代碼與欄位,不保存完整原始輸入。 |
| 功能規則錯誤 | 目前狀態不允許操作、項目重複或前置條件尚未完成。 | 說明目前無法完成的原因,以及可以採取的下一步。 | 記錄規則代碼、目標識別資訊與操作結果,通常不需要呼叫堆疊。 |
| 系統例外 | 程式缺陷、未處理狀態或執行資源超出預期。 | 回傳一般化訊息與操作識別碼,避免揭露內部結構。 | 以錯誤層級記錄例外類型、呼叫堆疊、處理位置與必要脈絡。 |
| 相依項目失敗 | 相依項目逾時、暫時無法使用或回傳不符合契約的內容。 | 回傳對本次操作有意義的結果,只有確認能安全重試時才標示可重試。 | 記錄相依項目、執行階段、失敗類型與耗時,保留原始例外作為內部原因。 |
分類要按照目標系統可以採取的處理方式決定,不能只按照例外類別名稱判斷。同一個逾時在可重新執行的查詢中可能屬於暫時失敗,在已經產生部分狀態變更的流程中,則需要先確認一致性才能決定後續動作。
錯誤契約讓不同輸入輸出方式使用一致概念。畫面、檔案匯入、批次工作或其他互動方式可以採用不同呈現形式,但應該保留相同錯誤碼與判斷語意。
一份對外錯誤結果通常需要下列欄位:
| 欄位 | 用途 | 注意事項 |
|---|---|---|
code |
讓程式穩定辨識錯誤種類。 | 不應該隨顯示文字調整,也不應該包含套件或類別名稱。 |
message |
說明本次操作結果。 | 使用輸入來源或操作人員可以理解的文字,不放入呼叫堆疊與保存結構。 |
operationId |
讓相關人員能找到同一次操作的內部記錄。 | 格式與產生方式要一致,避免直接沿用未檢查的外來值。 |
details |
表達欄位、項目或可修正條件。 | 只提供完成下一步所需的內容,不複製整份輸入。 |
retryable |
表示相同操作是否可以安全重新執行。 | 只有已確認重複執行結果與狀態一致性時才能設為 true。 |
例如,匯入內容未通過驗證時,可以使用下列結果:
{
"ok": false,
"error": {
"code": "IMPORT_ROW_INVALID",
"message": "匯入內容不符合格式",
"operationId": "01K2T9Q6J4N8M3R7V5X0Z1C2B3",
"retryable": false
}
}
錯誤碼應該描述穩定的功能語意,例如 IMPORT_ROW_INVALID、STATE_CONFLICT、DEPENDENCY_UNAVAILABLE 與 UNEXPECTED_FAILURE。顯示文字可以翻譯或調整,呼叫端的判斷仍以錯誤碼為準。如果系統包含 API,可以在介接邊界另外對映傳輸協定的狀態碼,不要讓內部規則直接依賴特定傳輸方式。
錯誤碼也需要負責人與修訂規則。新增代碼前要確認現有代碼是否已經表達相同情況,停用代碼時則要處理仍可能收到舊版本結果的組成項目。對外契約只保留安全資訊,完整原因與呼叫堆疊應該留在受限制的內部記錄中。
集中式例外處理是指在一項操作的明確邊界統一完成分類、記錄與結果轉換。邊界可能是畫面事件處理函式、API 程序、批次項目、訊息處理程式或命令列工作,應該按照系統實際構成決定。
內層函式只有在能執行下列動作時才需要捕捉例外:
只為了加入一行文字就反覆捕捉及拋出,會讓同一個失敗被記錄多次。完全忽略例外並繼續執行,則可能讓後續步驟使用不完整狀態。每項失敗應該由最清楚其責任的邊界記錄一次,內層如果需要增加脈絡,可以包裝例外並保留 cause。
下列 TypeScript 片段把可預期失敗轉為已知錯誤碼,其他例外則只回傳一般化結果:
type ErrorResult = {
ok: false
error: {
code: string
message: string
operationId: string
retryable: boolean
}
}
class KnownFailure extends Error {
constructor(
readonly code: string,
readonly publicMessage: string,
readonly retryable = false,
options?: ErrorOptions,
) {
super(code, options)
}
}
function toErrorResult(error: unknown, operationId: string): ErrorResult {
if (error instanceof KnownFailure) {
return {
ok: false,
error: {
code: error.code,
message: error.publicMessage,
operationId,
retryable: error.retryable,
},
}
}
return {
ok: false,
error: {
code: 'UNEXPECTED_FAILURE',
message: '系統暫時無法完成這項操作',
operationId,
retryable: false,
},
}
}
這個轉換函式沒有把 error.message 直接放入對外結果。處理邊界仍要把未預期例外記錄一次,再呼叫轉換函式產生結果。可預期錯誤是否需要記錄,則按照事件的重要程度與統計需求決定。
結構化記錄使用固定欄位名稱、型別與語意保存事件。只有把自由文字包成 JSON,仍不足以形成穩定結構。查詢條件依賴的欄位必須在不同組成項目中維持相同意義,欄位改名或型別變更時也要按照契約管理。
異常事件可以從下列欄位開始:
| 欄位 | 用途 |
|---|---|
timestamp |
記錄事件實際發生時間,並統一時區與格式。 |
level |
表達事件嚴重程度與預期處理方式。 |
eventName |
以穩定名稱分類事件,例如 import.operation.failed。 |
operationId |
串連同一次操作中的多筆記錄與對外錯誤結果。 |
component |
標示實際產生事件的組成項目或處理位置。 |
outcome |
使用固定值表示成功、拒絕、失敗或部分完成。 |
errorCode |
對應錯誤契約中的穩定代碼。 |
durationMs |
記錄已確認需要分析的處理耗時。 |
dependency |
系統具有相依項目時,標示發生互動失敗的對象。 |
traceId、spanId |
系統採用追蹤機制時,串連同一條追蹤路徑。 |
eventName、errorCode 與 outcome 適合用於篩選及統計,message 則提供人員閱讀所需的簡短說明。不要把時間、識別碼或錯誤類型只寫進 message,否則查詢工具必須解析自由文字才能使用。
來源不明的物件也不能直接展開成記錄的頂層欄位。外來欄位可能覆蓋 level、eventName 或 operationId,也可能意外帶入不應保存的內容。記錄前要明確選取允許欄位,必要內容則放在由程式控制名稱的巢狀物件中。
記錄器(Logger)應該集中設定最低記錄層級、基本欄位、例外序列化與敏感欄位遮蔽。個別功能只提供事件特有內容,避免各處自行組合不同格式。
下列 TypeScript 片段以 Pino 示範:先設定要遮蔽的欄位,再透過子記錄器讓同一次操作自動帶入 operationId。
import pino from 'pino'
const baseLogger = pino({
level: process.env.LOG_LEVEL ?? 'info',
redact: [
'credential',
'accessToken',
'personalData',
],
})
export function recordUnexpectedFailure(
operationId: string,
error: unknown,
): void {
const logger = baseLogger.child({ operationId })
logger.error({
err: error,
eventName: 'import.operation.failed',
errorCode: 'UNEXPECTED_FAILURE',
outcome: 'failed',
}, 'operation failed')
}
redact 是防止疏漏的保護層,欄位允許清單仍然要在資料進入記錄器前生效。實際欄位路徑要按照目標系統的資料結構設定,並以自動化檢查確認巢狀內容與例外物件不會繞過遮蔽規則。
TypeScript 或 JavaScript 專案可以比較下列四種記錄工具:
| 記錄工具 | 主要特性 | 適合評估的情況 | 導入前要確認的事項 |
|---|---|---|---|
| Pino | 輸出 JSON 記錄,提供記錄層級、子記錄器、例外序列化與欄位遮蔽。 | 需要一般用途記錄器,並且希望自行控制事件欄位與輸出流程。 | 確認外來物件處理、遮蔽路徑、輸出目的地及程序停止前的寫出行為。Pino API 文件提醒不要把外來物件直接作為頂層欄位。 |
| Winston | 使用格式(Format)與傳輸(Transport)分開處理事件內容及輸出位置,也能為不同輸出設定層級。 | 同一份程式需要多種輸出格式或輸出目的地,而且願意管理較多組態。 | 確認各 Transport 的失敗行為、結束前寫出方式、欄位格式與例外處理設定。 |
| LogTape | 支援結構化欄位、分類階層與可替換的輸出端(Sink),並提供獨立的資料遮蔽套件。 | 需要支援多種 JavaScript 執行環境,或希望程式庫與應用程式共用較中立的記錄介面。 | 確認所需 Sink、JSON Lines 格式、執行環境相容性與資料遮蔽設定。 |
| evlog | 提供簡單記錄、結構化錯誤與寬事件(Wide Event),可以在一次操作中累積脈絡後送出。 | TypeScript 專案希望減少分散記錄,並以一筆事件摘要一次操作結果。 | 確認框架整合、事件送出時機、欄位型別、遮蔽、匯出方式與版本維護狀態。evlog 快速入門說明了三種記錄方式的差異。 |
這四種工具的抽象層次與預設做法不同,不宜只比較輸出範例。應該用相同的小型情境產生一筆已知錯誤、一筆未預期例外及一次相依項目失敗,再比較事件欄位、關聯方式、遮蔽、寫出失敗行為與查詢結果。
時間相近的事件不一定屬於同一次操作。目標系統應該在操作進入處理邊界時建立 operationId,並將它傳遞至後續函式、相依項目互動、錯誤結果與稽核紀錄。相關人員取得錯誤結果中的識別碼後,就能直接查詢同一次操作。
如果系統接收外部提供的識別碼,要先限制格式與長度,再決定沿用或建立內部識別碼。未經檢查就把外來值寫入每筆記錄,可能造成欄位污染、過長內容或錯誤關聯。
關聯識別碼與分散式追蹤使用的 traceId 具有不同責任。關聯識別碼是目標系統自行定義的操作契約,可以出現在對外錯誤結果中。traceId 與 spanId 由追蹤脈絡(Trace Context)描述執行路徑,適合系統已經採用追蹤機制的情境。單一執行流程如果只需要串連記錄,可以先使用 operationId,不必為了產生一個識別碼就加入完整遙測架構。
記錄層級要按照事件的影響與預期處理方式選擇,不能把所有失敗都標示為 error。大量可預期的驗證失敗如果持續產生高層級記錄,真正需要處理的系統例外就容易被淹沒。
| 層級 | 使用條件 | 不適合的情況 |
|---|---|---|
fatal |
目前執行程序無法安全繼續,即將停止前保留最後資訊。 | 一般功能失敗、單次相依項目逾時或可恢復錯誤。 |
error |
未預期例外、重要操作失敗,或需要人員介入處理的異常。 | 每一筆可修正輸入錯誤。 |
warn |
已完成降級或恢復,但情況持續發生可能影響後續處理。 | 正常操作流程與不需關注的預期分支。 |
info |
重要操作開始、完成、拒絕或狀態變更的摘要。 | 迴圈內每個步驟或大量重複細節。 |
debug、trace |
開發與受控診斷期間需要的細節。 | 未限制資料內容與保存量的正式記錄。 |
同一個例外沿著呼叫路徑傳遞時,通常由最外層負責處理的邊界記錄一次。需要統計的可預期失敗可以記錄事件代碼與數量,不必為每次結果保存完整呼叫堆疊。抽樣或降低層級前,也要先確認不會讓重要錯誤與稽核事件消失。
執行記錄用來追查程式如何運作,稽核紀錄則用來回答重要操作由哪個來源在何時執行、作用於哪個對象,以及結果為何。兩者可能共用 operationId,但保存目的、欄位與存取規則不同。
| 比較項目 | 執行記錄 | 稽核紀錄 |
|---|---|---|
| 主要目的 | 診斷例外、效能與相依項目互動。 | 核對重要操作與資料異動。 |
| 常見內容 | 事件名稱、層級、錯誤碼、處理位置、耗時與例外資訊。 | 操作來源、動作、目標識別資訊、時間、結果與異動摘要。 |
| 保存方式 | 可以按照層級、容量與診斷需求採用不同期限。 | 應該按照稽核需求防止未經允許的修改,並保留必要查詢能力。 |
| 寫入時機 | 可在操作過程中記錄多個重要事件。 | 應該在重要操作結果確定時保存一筆語意完整的紀錄。 |
如果系統具有身分識別機制,稽核紀錄可以保存經允許的操作者識別資訊。無人操作的排程、匯入工具或其他自動工作,則應該記錄實際執行來源。異動內容應該保存欄位名稱、結果與必要摘要,不要直接複製修改前後的完整資料。
重要狀態變更與稽核紀錄之間也要定義一致性要求。如果稽核寫入失敗,系統要事先決定阻止操作、保存待補寫項目或採取其他可驗證方式,不能在失敗後悄悄略過。選擇方式要按照該紀錄的重要程度與保存要求決定。
執行記錄與稽核紀錄可能集中保存並供查詢,一旦寫入不必要的敏感內容,影響範圍通常比單次錯誤結果更大。保護順序應該是先避免收集,再以遮蔽作為額外防線。
記錄送出後,收集、傳送、保存、匯出與刪除流程都要套用相同保護要求。只在應用程式端遮蔽一部分欄位,無法處理其他來源產生的記錄,也無法限制後續查詢與匯出。
遙測(Telemetry)可以包含記錄、追蹤與指標。記錄器負責在程式內建立事件,遙測工具則可能負責插樁、收集、轉換、保存、查詢或告警。比較工具前要先確認需要補足哪一段流程。
下列四種遙測工具涵蓋不同層次:
| 遙測工具 | 主要定位 | 適合評估的情況 | 導入前要確認的事項 |
|---|---|---|---|
| OpenTelemetry | 提供記錄、追蹤與指標的共同資料模型、API、SDK、通訊協定及收集器。 | 希望統一插樁與傳送格式,並降低程式與特定保存工具的耦合。 | 它不包含完整的遙測保存與查詢介面。應確認所用語言的訊號支援狀態、既有記錄器橋接、脈絡傳遞與匯出端相容性。 |
| Sentry | 以例外事件分組、呼叫堆疊、事件脈絡與相關追蹤協助定位程式問題。 | 主要需求是找出未預期例外、重複問題與發生前後脈絡。 | 確認 SDK 支援、事件分組規則、追蹤抽樣、敏感資料處理、保存期限與使用量限制。Sentry Issue Details說明可查閱的事件內容。 |
| SigNoz | 以 OpenTelemetry 為資料來源,整合記錄、指標、追蹤、例外、查詢、儀表板與告警。 | 希望以同一套介面關聯多種遙測訊號,並評估自行管理或代管方式。 | 確認部署與維護成本、資料量、保存層級、查詢效能、告警規則及 OpenTelemetry 收集流程。SigNoz 文件列出目前支援的訊號與功能。 |
| Elastic Observability | 以搜尋與分析能力整合記錄、指標、應用程式追蹤及其他事件資料。 | 已經使用 Elastic Stack,或需要對大量結構化記錄進行彈性查詢與跨訊號分析。 | 確認資料對映、索引與保存策略、資源需求、授權方案、OpenTelemetry 相容性及管理責任。Elastic Observability 文件說明其整合範圍。 |
OpenTelemetry 的記錄說明指出,既有記錄器可以透過橋接與追蹤脈絡建立關聯。OpenTelemetry 收集器可以接收、處理及匯出資料,但保存、查詢與告警仍由目的端負責。Sentry、SigNoz 與 Elastic Observability 提供的功能範圍也不相同,不能只按照「都能接收遙測資料」就視為相同選項。
概念驗證時,可以讓同一筆未預期例外帶有 operationId、errorCode、traceId 與版本資訊,再確認工具能否完成接收、關聯、查詢、遮蔽、告警與到期刪除。選型結果要記錄資料流向、故障時的處理方式、維護責任與重新評估條件。
記錄成功產生後,還要能在需要時找到並安全刪除。執行記錄與稽核紀錄應該分別定義下列項目:
operationId、eventName、errorCode、outcome 與已確認的組成項目欄位。每筆可預期輸入錯誤都立即告警,通常只會產生大量無法採取行動的訊息。告警應該使用已定義事件欄位與時間範圍判斷,並附上 operationId、錯誤碼、影響範圍及查詢入口。監控指標與完整告警處理流程會由後續章節繼續說明,本章先確保異常事件具有可以可靠彙整的欄位。
完成一項功能的錯誤處理後,可以使用下列問題確認結果:
operationId 串連?error?operationId 建立關聯。traceId 與 spanId。