iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

如何管理系統異常事件?

前一章已經說明如何在資料進入主要處理前檢查內容。不過,資料通過驗證後,仍可能因為目前狀態不允許操作、程式缺陷、執行資源不足或相依項目失敗而無法完成處理。

如果每個函式各自決定錯誤訊息與記錄內容,相同問題可能產生不同結果。程式也可能把內部例外直接顯示在輸出中,或留下大量無法互相關聯的文字。發生問題時,即使找到錯誤訊息,也不一定能確認它屬於哪一次操作、在哪個步驟發生,以及是否改變了保存狀態。

異常事件管理需要同時處理三項需求:對外提供穩定且安全的失敗結果、在內部保留足以追查原因的執行記錄,以及為重要操作留下可核對的稽核紀錄。本章會先建立錯誤分類與處理邊界,再說明結構化記錄(Structured Logging)、關聯識別碼(Correlation ID)、記錄工具與保存規則如何配合。

先區分錯誤、例外與異常事件

錯誤結果、例外與異常事件代表不同層次的資訊:

  • 錯誤結果描述某次操作為何不能完成,內容屬於目標系統對輸入來源或操作人員提供的契約。
  • 例外是程式中斷目前執行路徑並向外傳遞失敗資訊的機制。可預期失敗與未預期缺陷都可能以例外表示。
  • 異常事件是為了後續查詢、統計或告警而保存的事件記錄。一次失敗可能產生錯誤結果與異常事件,也可能只需要其中一種。

驗證失敗屬於正常契約的一部分時,可以直接回傳失敗結果,不必建立呼叫堆疊,也不必逐筆記為高層級事件。程式無法繼續執行時,則可以拋出例外,交由目前操作的處理邊界統一轉換與記錄。

目標系統可以先使用下列四類失敗建立共同判斷方式:

類型 常見情況 對外處理 內部處理
輸入驗證錯誤 必要欄位缺漏、格式錯誤或內容超出範圍。 回傳穩定錯誤碼與可修正欄位,不顯示原始程式資訊。 按照分析需求記錄規則代碼與欄位,不保存完整原始輸入。
功能規則錯誤 目前狀態不允許操作、項目重複或前置條件尚未完成。 說明目前無法完成的原因,以及可以採取的下一步。 記錄規則代碼、目標識別資訊與操作結果,通常不需要呼叫堆疊。
系統例外 程式缺陷、未處理狀態或執行資源超出預期。 回傳一般化訊息與操作識別碼,避免揭露內部結構。 以錯誤層級記錄例外類型、呼叫堆疊、處理位置與必要脈絡。
相依項目失敗 相依項目逾時、暫時無法使用或回傳不符合契約的內容。 回傳對本次操作有意義的結果,只有確認能安全重試時才標示可重試。 記錄相依項目、執行階段、失敗類型與耗時,保留原始例外作為內部原因。

分類要按照目標系統可以採取的處理方式決定,不能只按照例外類別名稱判斷。同一個逾時在可重新執行的查詢中可能屬於暫時失敗,在已經產生部分狀態變更的流程中,則需要先確認一致性才能決定後續動作。

建立穩定的錯誤契約

錯誤契約讓不同輸入輸出方式使用一致概念。畫面、檔案匯入、批次工作或其他互動方式可以採用不同呈現形式,但應該保留相同錯誤碼與判斷語意。

一份對外錯誤結果通常需要下列欄位:

欄位 用途 注意事項
code 讓程式穩定辨識錯誤種類。 不應該隨顯示文字調整,也不應該包含套件或類別名稱。
message 說明本次操作結果。 使用輸入來源或操作人員可以理解的文字,不放入呼叫堆疊與保存結構。
operationId 讓相關人員能找到同一次操作的內部記錄。 格式與產生方式要一致,避免直接沿用未檢查的外來值。
details 表達欄位、項目或可修正條件。 只提供完成下一步所需的內容,不複製整份輸入。
retryable 表示相同操作是否可以安全重新執行。 只有已確認重複執行結果與狀態一致性時才能設為 true

例如,匯入內容未通過驗證時,可以使用下列結果:

{
    "ok": false,
    "error": {
        "code": "IMPORT_ROW_INVALID",
        "message": "匯入內容不符合格式",
        "operationId": "01K2T9Q6J4N8M3R7V5X0Z1C2B3",
        "retryable": false
    }
}

錯誤碼應該描述穩定的功能語意,例如 IMPORT_ROW_INVALIDSTATE_CONFLICTDEPENDENCY_UNAVAILABLEUNEXPECTED_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 系統具有相依項目時,標示發生互動失敗的對象。
traceIdspanId 系統採用追蹤機制時,串連同一條追蹤路徑。

eventNameerrorCodeoutcome 適合用於篩選及統計,message 則提供人員閱讀所需的簡短說明。不要把時間、識別碼或錯誤類型只寫進 message,否則查詢工具必須解析自由文字才能使用。

來源不明的物件也不能直接展開成記錄的頂層欄位。外來欄位可能覆蓋 leveleventNameoperationId,也可能意外帶入不應保存的內容。記錄前要明確選取允許欄位,必要內容則放在由程式控制名稱的巢狀物件中。

使用記錄器統一欄位與遮蔽規則

記錄器(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 具有不同責任。關聯識別碼是目標系統自行定義的操作契約,可以出現在對外錯誤結果中。traceIdspanId 由追蹤脈絡(Trace Context)描述執行路徑,適合系統已經採用追蹤機制的情境。單一執行流程如果只需要串連記錄,可以先使用 operationId,不必為了產生一個識別碼就加入完整遙測架構。

讓記錄層級代表處理方式

記錄層級要按照事件的影響與預期處理方式選擇,不能把所有失敗都標示為 error。大量可預期的驗證失敗如果持續產生高層級記錄,真正需要處理的系統例外就容易被淹沒。

層級 使用條件 不適合的情況
fatal 目前執行程序無法安全繼續,即將停止前保留最後資訊。 一般功能失敗、單次相依項目逾時或可恢復錯誤。
error 未預期例外、重要操作失敗,或需要人員介入處理的異常。 每一筆可修正輸入錯誤。
warn 已完成降級或恢復,但情況持續發生可能影響後續處理。 正常操作流程與不需關注的預期分支。
info 重要操作開始、完成、拒絕或狀態變更的摘要。 迴圈內每個步驟或大量重複細節。
debugtrace 開發與受控診斷期間需要的細節。 未限制資料內容與保存量的正式記錄。

同一個例外沿著呼叫路徑傳遞時,通常由最外層負責處理的邊界記錄一次。需要統計的可預期失敗可以記錄事件代碼與數量,不必為每次結果保存完整呼叫堆疊。抽樣或降低層級前,也要先確認不會讓重要錯誤與稽核事件消失。

分開執行記錄與稽核紀錄

執行記錄用來追查程式如何運作,稽核紀錄則用來回答重要操作由哪個來源在何時執行、作用於哪個對象,以及結果為何。兩者可能共用 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 提供的功能範圍也不相同,不能只按照「都能接收遙測資料」就視為相同選項。

概念驗證時,可以讓同一筆未預期例外帶有 operationIderrorCodetraceId 與版本資訊,再確認工具能否完成接收、關聯、查詢、遮蔽、告警與到期刪除。選型結果要記錄資料流向、故障時的處理方式、維護責任與重新評估條件。

定義保存、查詢、存取與告警規則

記錄成功產生後,還要能在需要時找到並安全刪除。執行記錄與稽核紀錄應該分別定義下列項目:

  • 保存位置要符合資料量、查詢速度與可用性需求,並明確處理寫入端暫時無法使用時的緩衝、捨棄或停止規則。
  • 保存期限要按照記錄目的、層級、資料敏感程度與規範要求決定,除錯細節不應該因為容易產生就永久保留。
  • 查詢方式至少要支援時間範圍、operationIdeventNameerrorCodeoutcome 與已確認的組成項目欄位。
  • 存取規則要限制可查詢、匯出、修改與刪除的範圍,並記錄重要管理操作。
  • 刪除流程要涵蓋主要保存位置、備份與匯出副本,並且能確認過期資料已按照政策處理。
  • 告警條件要對應可採取的處理動作,例如特定錯誤碼持續增加、重要操作連續失敗或相依項目失敗超過門檻。

每筆可預期輸入錯誤都立即告警,通常只會產生大量無法採取行動的訊息。告警應該使用已定義事件欄位與時間範圍判斷,並附上 operationId、錯誤碼、影響範圍及查詢入口。監控指標與完整告警處理流程會由後續章節繼續說明,本章先確保異常事件具有可以可靠彙整的欄位。

完成異常事件管理的檢查

完成一項功能的錯誤處理後,可以使用下列問題確認結果:

  • 輸入驗證錯誤、功能規則錯誤、系統例外與相依項目失敗是否具有明確處理方式?
  • 錯誤碼是否穩定,而且對外訊息沒有揭露呼叫堆疊、保存結構或執行設定?
  • 未預期例外是否只在負責處理的邊界記錄一次,並且保留原始原因?
  • 結構化記錄是否使用固定欄位名稱、型別與事件名稱,而非只把自由文字輸出成 JSON?
  • 對外結果、執行記錄與稽核紀錄是否能透過 operationId 串連?
  • 記錄層級是否對應事件影響與處理方式,並避免把所有可預期錯誤標為 error
  • 執行記錄與稽核紀錄是否分開定義用途、欄位、一致性與保存方式?
  • 密碼、權杖、私密金鑰、完整身分驗證資料與不必要的個人資料是否確實不會進入記錄?
  • 記錄工具或遙測工具失敗時,系統是否有明確的緩衝、捨棄、補寫或停止條件?
  • 保存期限、查詢欄位、存取範圍、刪除流程與告警條件是否都有可檢查結果?

重點整理

  • 異常事件管理要分開處理對外錯誤結果、內部執行記錄與稽核紀錄,三者可以透過相同 operationId 建立關聯。
  • 輸入驗證錯誤、功能規則錯誤、系統例外與相依項目失敗具有不同責任,分類要按照目標系統能採取的處理方式決定。
  • 錯誤契約應該提供穩定錯誤碼、安全訊息與操作識別碼。呼叫堆疊、執行設定及原始相依項目回應只能留在受限制的內部記錄。
  • 集中式例外處理應該在明確操作邊界完成分類、記錄與結果轉換,避免同一例外被重複記錄或遭到忽略。
  • 結構化記錄需要固定欄位名稱、型別與語意。關聯識別碼可以串連一次操作,系統採用追蹤機制時再加入 traceIdspanId
  • 記錄層級應該對應事件影響與預期處理方式。可預期失敗不一定需要高層級記錄,重要錯誤與稽核事件則不能在沒有明確規則時被抽樣捨棄。
  • Pino、Winston、LogTape 與 evlog 適合不同的程式內記錄需求,選擇時要比較事件結構、輸出流程、遮蔽與失敗行為。
  • OpenTelemetry、Sentry、SigNoz 與 Elastic Observability 涵蓋不同的遙測層次,選擇時要分開確認插樁、收集、保存、查詢與告警責任。
  • 敏感內容應該在進入記錄器前排除,遮蔽只作為額外防線。保存、查詢、存取、匯出、告警與刪除流程也要遵守相同保護要求。

上一篇
[Day 19] 如何檢查來源不明的資料型別?
系列文
遠古聖遺物改造工程:遺留系統全面重構實務指南20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言