Windows 應用程式因程式錯誤的例外掉下也要確實留下日誌 - 不賭 in-process 的設計與 WER / 最終日誌 / 監視程序的最佳實踐

· · Windows 開發, 例外處理, 日誌, WER, 當機傾印, 缺陷調查

Windows 應用程式的缺陷調查中最痛苦的是 只知道掉了,但為什麼掉沒有留下 的狀態。

特別是以下的案件中這個問題會相當重。

  • 只在客戶環境掉
  • 只在長時間運轉的最後掉
  • WPF / WinForms / Windows 服務 / 常駐應用程式中重現率低
  • COM、P/Invoke、native DLL、vendor SDK 相關
  • 「例外訊息」能取得,但沒有之前的脈絡

但是最初老實說,只用掉下側的程序不能「一定」留下日誌。 包含堆疊破損、記憶體破壞、fast fail、強制終止、電源斷電,in-process 的最後日誌本質上是 best effort

實務上該目標的是 不期待掉下程序中 的構成。 也就是,

  1. 通常時的時序日誌
  2. 掉下瞬間的最終當機標記
  3. OS 或別程序側留下的當機證跡

這 3 層思考。

本文以 Windows 桌面應用程式、常駐應用程式、Windows 服務、裝置連動工具為前提,整理 因程式錯誤的例外掉下也不失去調查可能性的最佳實踐

1. 先講結論

先排結論。

  • 不把「最後的日誌」賭在 1 個 in-process 處理器 最重要。
  • 實務上最無難的是 通常日誌 + 最終當機標記 + WER LocalDumps 的組合。
  • 長時間運轉、裝置連動、外掛、native SDK 混在的話,加上監視程序(watchdog / launcher / service) 會相當強。
  • 當機處理器中 不做重處理 是鐵則。壓縮、HTTP 送信、DI 解析、UI 對話方塊、複雜的 JSON 產生要排除。
  • 當機時只往本地短短留下,壓縮・上傳・通知放到 下次啟動後或別程序
  • 用 WinForms 的 ThreadException 或 WPF 的 DispatcherUnhandledException 讓外觀延命的設計,對程式錯誤而言危險
  • .NET 和 native 中 懷疑損壞狀態的例外以「記錄後終止」而非「復原」 為基本比較安全。
  • 要取得傾印,必須同時保存 PDB 和發佈的二進位 否則之後讀不了。

簡言之最佳實踐是 「不要在掉下的瞬間做全部。把掉下前・掉下瞬間・掉下後分工」

2. 為什麼只有 in-process 不能「確實」

這裡曖昧設計會搖擺。

2.1 掉下的執行緒的脈絡本身可能已壞

未處理例外的鉤子或最上層例外過濾器 可能在壞掉那側的執行緒脈絡上運作。 這時,

  • 堆疊已經危險
  • 堆損壞額外配置危險
  • 例外發生時持有的鎖導致等待會停
  • logger 本身依賴的物件已壞

這些普通存在。

所以 把最後的處理器看作「什麼都能做的地方」不如看作「能做的事相當少的地方」 比較安全。

2.2 fast fail 或損壞狀態例外以「最小 in-process 動作」為前提

記憶體破壞或致命狀態中不要期待通常例外處理比較好。 特別 native 側的 __fastfail 系或懷疑損壞狀態的異常,以 「以盡可能少的額外開銷立即終止」 方向設計。

也就是說 最後的 in-process 日誌能寫算運氣好,主證跡在 OS / 別程序側 這思考方式自然。

2.3 .NET 的未處理例外事件也不是「重復原處理」的場合

.NETAppDomain.UnhandledException 雖方便, 這裡該做的看作到 短記錄 為止比較好。

  • 受例外發生時持有鎖的影響
  • 不是損壞狀態例外都能安全取得
  • 這裡勉強做持續方針容易以半壞狀態延命

簡言之 「未處理例外事件 = 最後的通知」「不是安全的復原點」

3. 推薦架構 - 分開 crash-time 和 after-restart

最容易整理的是分開 當機時做的重新啟動後做的

階段 目的 在哪運作 做的事
通常時 留下時序 應用程式內 結構化日誌、heartbeat、邊界事件
當機時 留下最低限證跡 應用程式內 + OS 最終當機標記、WER 傾印
終止直後 偵測 unexpected exit 別程序 記錄 exit code、判斷重新啟動、通知
下次啟動後 做重的後處理 新的健全程序 壓縮、上傳、使用者通知、整理舊日誌

這樣分設計相當穩定。

3.1 最小構成

小型業務工具或公司內部用 WPF / WinForms 的話,先用以下通常就夠。

  • 通常日誌: 本地的 append-only 檔
  • 最終當機標記: 專用的短檔
  • 傾印: WER LocalDumps
  • 下次啟動時: 顯示「前次異常終止。有診斷資訊」

3.2 強化構成

以下要件的話加強 1 段比較好。

  • 24/7 運轉
  • 裝置控制、監視、常駐
  • 多 COM / P/Invoke / native SDK
  • 有子程序、外掛、腳本執行
  • 客戶環境不允許「停著」

這種情況,

  • worker 程序: 本體處理
  • launcher / watchdog / service: 啟動監視、記錄 exit、重新啟動
  • WER LocalDumps: worker 側
  • 下次啟動或 watchdog: 回收診斷資訊

這樣分相當實務向。

4. 通常日誌的最佳實踐

試圖只用當機時的最後 1 行戰鬥通常會輸。 真正有效的是 到之前為止的通常日誌

4.1 日誌比起「人類用文章」,是「之後能相關的資訊」

通常日誌至少放入以下。

  • UTC 時間戳記
  • 從程序開始經過時間
  • PID / TID
  • 應用程式名、版本、建置編號、提交識別碼
  • 工作階段 ID
  • 操作 ID / 工作 ID / 相關 ID
  • 模組名 / 畫面名 / worker 名
  • 之前的外部作用
    • 檔案寫入
    • DB 更新
    • 裝置命令傳送
    • 通訊要求
  • 例外類型、HRESULT / Win32 錯誤 / 例外代碼
  • 主要輸入參數的摘要
  • 不含機密範圍內的對象 ID

推薦 1 行 1 事件的 JSON Lines 或 key=value 格式

比起留下人類用長文,「之後能對照 3 個檔案」 更重要。

4.2 關鍵事件同步留下

通常日誌全部同步寫會重。 但全部交給非同步緩衝,掉下的瞬間會一起消失。

所以實務上以下分法好處理。

  • Information 的細事件: 可緩衝
  • Warning 以上: 早 flush
  • 重要的邊界事件: 同步留下
    • ProcessStart
    • ConfigLoaded
    • WorkerStarted
    • ExternalCommandSent
    • TransactionCommitted
    • RecoveryStarted
    • FatalPathEntered

重點是 業務上的邊界要好好落到地面

4.3 分開「現在寫的通常日誌」和「最後的當機標記」

這相當重要。

試圖把全部放入 1 個 rolling log,

  • 正在 rotation
  • 留在非同步佇列
  • 例外發生直後 logger 本身死了
  • 日誌行中途切斷

會發生這些事。

所以至少分成以下 2 個推薦。

  • app-<session>.jsonl 通常時序日誌
  • fatal-last.logfatal-<session>.log 最終當機標記專用

「最後 1 行留在哪」明確 在現場相當有幫助。

4.4 日誌儲存位置固定本地,不用網路處

當機時依賴 UNC 路徑、NAS、HTTP、雲 API 很危險。

  • 網路瞬斷
  • DNS 延遲
  • 憑證失效
  • UI 執行緒的等待
  • 服務帳戶權限不足

會相關。

當機時 先往本地固定路徑 落下。 發送放到 下次啟動後或別程序

4.5 檔名放入 session

光日期不夠。 因為同天會重啟很多次。

推薦例如以下。

Logs\
  MyApp_20260318_101530_pid1234_session-4f1c.jsonl
  MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
  MyApp_watchdog_20260318.jsonl

「是哪個啟動實例的話」 明確就讓解析速度相當不同。

5. 最終當機標記的最佳實踐

這裡不是做 全功能 logger 的地方。 是 只 1 次、短、偏確實地留下 的地方。

5.1 目的不是「原因的詳細」而是「入口的固定」

最終當機標記該放的資訊縮小比較強。

  • 發生 UTC
  • PID / TID
  • 工作階段 ID
  • 版本 / 建置編號
  • 從哪個鉤子來
    • AppDomain.UnhandledException
    • Application.ThreadException
    • DispatcherUnhandledException
    • SetUnhandledExceptionFilter
    • _set_invalid_parameter_handler
    • set_terminate
  • 例外類型或例外代碼
  • 可能的話簡單訊息
  • 之前的操作 ID
  • 通常日誌的檔名
  • dump 預期資料夾

這些就夠。

5.2 當機處理器中不該做的

以下相當高機率是地雷。

  • 從 DI 容器解析 logger
  • 使用 async / await
  • 丟 Task
  • 鎖等待
  • 組複雜的 JSON
  • 碰 COM 物件
  • 顯示 UI 對話方塊
  • 壓縮
  • HTTP / SMTP / Slack / Teams 傳送
  • 解析 dump 並摘要
  • 吞下例外繼續

當機處理器 不是普通處理流程的延續。 往「只做最小本地寫入並結束」偏。

5.3 當機處理器該做的

相反該做的相當單純。

  1. 防止多重進入
  2. 只寫 1 行
  3. flush
  4. 終止

這個順序。

盡可能,

  • 事先建立的專用資料夾
  • 事先存在確認的路徑
  • ACL 確認過的儲存位置

使用。

通常日誌 flush 太多會重,但 fatal 標記件數極少,所以這裡可以強 flush。 .NET 是 FileStream.Flush(true),native 是 FlushFileBuffers 這類 「這 1 行立刻落到地面」 的處理容易設計。

5.4 不要試圖讓它繼續

程式錯誤起點的 unexpected 例外,最終處理器看作 不是復原裝置而是記錄裝置 比較安全。

特別以下「不繼續」為基本。

  • 即使 NullReferenceExceptionInvalidOperationException,共享狀態更新途中
  • UI 執行緒的 unexpected 例外
  • 監視迴圈或父迴圈漏的 unexpected 例外
  • AccessViolationException
  • StackOverflowException
  • native 邊界的異常
  • CRT 的 invalid parameter / purecall / terminate

「不想掉下來」的心情可理解,但 半壞狀態延命比較痛苦,診斷和運營都 很多。

要終止時,.NETEnvironment.FailFast,native 用 RaiseFailFastException__fastfail 這類 立即終止系 API,不期待 finally 或通常的後續處理的設計比較安全。

6. 各框架的注意點

6.1 .NET 通用: AppDomain.CurrentDomain.UnhandledException

這作為 最後通知 有用。 但這裡避免重的復原處理。

基本用法如下。

  • 寫最終當機標記
  • 必要時在 Windows Event Log 留最小訊息
  • 不繼續
  • 這裡不做等待或重試

UnhandledException 雖方便,但 不要以為這裡能把應用程式回到健康狀態 比較安全。

6.2 WinForms: Application.ThreadException

這難在 能接住 UI 執行緒的未處理例外並外觀上繼續

業務輸入的預期內錯誤做對話方塊化的用途還可以,但 不適合程式錯誤起點的 unexpected 例外繼續的用途

真的要優先原因調查的話,

  • ThreadException 中只做最小記錄
  • 或偏向 UnhandledExceptionMode.ThrowException
  • 在此基礎上終止程序,留下傾印和日誌

比較安全。

6.3 WPF: Application.DispatcherUnhandledException

WPF 也類似。

  • 只以 UI 執行緒的例外為主對象
  • Handled = true 能外觀上繼續
  • 但對程式錯誤做這個,畫面狀態和內部狀態容易偏差

所以 WPF 也是 不作為繼續用的延命裝置,作為記錄的入口使用 比較無難。

6.4 TaskScheduler.UnobservedTaskException 不作主路徑

這不是 「掉下前的最後防線」

偵測 Task 的例外漏的輔助可用,但 作為當機時的確實記錄路徑弱

所以,

  • 早期發現例外觀測漏
  • 開發中挖出 Task 的設計漏

用途用,但 不作為最終當機處理器的主角 比較好。

6.5 native Win32 / C++: 不過信 SetUnhandledExceptionFilter

native 側容易期待 SetUnhandledExceptionFilter

但這 在 faulting thread 的脈絡運作

  • 無效堆疊
  • 深遞迴
  • 已壞的堆
  • 例外發生時的鎖持有

會受影響。

因此 SetUnhandledExceptionFilter 看作 接最後通知的 best effort 的入口 剛好。

6.6 native C++ 也抓 CRT 的終止路徑

native C++ 中只看未處理 SEH 會漏。

想看的例如以下。

  • _set_invalid_parameter_handler
  • _set_purecall_handler
  • set_terminate

這系列是為了抓 C 執行環境或 C++ 執行環境起點的「終止路徑」

實務上,

  • 這些處理器也寫最終當機標記
  • 但不做重的復原處理
  • 確實終止
  • 主證跡交給 WER / dump

無難。

7. 以 WER LocalDumps 為基礎

這在實務上相當強。

7.1 首推是 WER LocalDumps

「掉下後偏確實地留下最低限證跡」 的意義上, 先是 WER LocalDumps 最好處理。

理由單純。

  • 能在 OS 側留傾印
  • 沒有追加工具容易放入
  • 能以應用程式單位設定
  • 能把當機時的主證跡逃到 in-process 以外

光靠日誌不知道的

  • 哪個執行緒掉
  • 在哪個堆疊掉
  • 哪個模組邊界
  • managed / native / COM / SDK 哪個可疑

之後能看是強的。

7.2 典型設定

例如對 MyApp.exeC:\CrashDumps\MyApp 留傾印的話可以如下。

reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f

最初以下思考方式就好。

首推
DumpFolder 專用資料夾
DumpCount 5~10
DumpType 開發機 2,現場看容量和機密要件 1 or 2

7.3 一定要確認 dump 儲存位置的 ACL

日誌和傾印都一樣,設定到無法寫的資料夾沒意義

特別,

  • Windows 服務
  • 權限分離的子程序
  • 現場機的限制帳戶
  • 與 UAC 相關

儲存位置 ACL 是落空的主因

儲存位置,

  • 事先建立
  • 寫入測試
  • 保留數限制
  • 運營擔當能去看的位置

確認到這裡。

7.4 想在 WER 報告附加目前日誌時

對 Microsoft 的 WER 報告或自訂 WER 運營,也有用 WerRegisterFile 為了把目前日誌檔加到錯誤報告進行註冊 的方法。

但這裡看作 不是本地儲存的替代而是追加導線 比較安全。 當機時真正想要的先是 手邊終端偏確實地留下

順序是,

  1. 本地通常日誌
  2. 本地 fatal 標記
  3. 本地 dump
  4. 必要時也在 WER 傳送路徑註冊相關檔

比較實務向。

7.5 不只傾印也留版本管理

取得傾印後之後,

  • 那時的 EXE / DLL 沒了
  • 沒 PDB
  • 不知道哪個提交的建置

會相當弱。

至少留以下。

  • 已發佈的二進位
  • 對應的 PDB
  • 版本
  • 建置日期時間
  • 提交識別碼
  • 安裝程式版本

傾印收集和 PDB 保存是 套組

8. 使用 MiniDumpWriteDump 或獨自當機回報器時的思考方式

也有需要獨自實作的場面。

  • 想從 UI 顯示「儲存診斷資訊」按鈕
  • 想把日誌或設定檔也打包
  • 想把子程序群一起處理
  • 想在自動上傳前放入獨自遮罩

但這裡最重要的是 不要讓取 dump 的處理也由掉下側背負太多

8.1 比起 self-dump 用別程序

MiniDumpWriteDump 強大但 比起從當機的那個程序中呼叫,從別程序呼叫更安全

典型構成如下。

  • worker 本體偵測異常
  • 可能的話用事件或命名管道通知 helper
  • helper 取 worker 的 dump
  • helper 打包 tail 日誌或設定檔
  • helper 終止後放到上傳佇列

這樣 worker 壞了 helper 側還健全。

8.2 真的要 in-process 就偏向專用執行緒

不能別程序化的情況也, 把專用執行緒作為 dump 專用 比較好。

但即使如此本質是 best effort。 「放入獨自 dump 實作所以 100% 安心」不成立。

8.3 重的事放到下次啟動後

獨自回報器容易想做的事。

  • zip 壓縮
  • 與 symbol 資訊對照
  • 伺服器上傳
  • 畫面截圖
  • 從 DB 取得追加資訊

這些放到 不是當機時而是重啟後或 helper 側

9. 加入監視程序會改變什麼

長時間運轉系中監視程序相當有效。

9.1 監視程序留的東西

watchdog / launcher / 父服務,例如能留以下。

  • 子程序開始時刻
  • 啟動引數
  • PID
  • 監視對象版本
  • heartbeat 的最終接收時刻
  • 終止時刻
  • exit code
  • restart 次數
  • dump 有無
  • 是否重啟

光這些就,

  • 真的當機
  • OS 關機
  • 使用者關
  • hang 被 kill
  • 重啟迴圈幾次

相當看得到。

9.2 特別適合的情況

以下的話相當積極考慮分離。

  • 抱 vendor SDK 的 worker
  • 影像處理 / 影片處理 / device I/O
  • 監視或輪詢的父迴圈
  • 腳本或外掛執行
  • COM / ActiveX 既有資產的主機
  • 64bit / 32bit 橋接或互通

把危險處理關進 1 個 worker 日誌設計和復原設計都變輕鬆。

10. 常見的 NG

10.1 catch (Exception) 只輸出日誌後繼續

最常見也最危險。

  • 途中變更留
  • 共享狀態壞
  • 後續障礙增
  • 真正原因點模糊

日誌增 1 行的代價是 事故拖長 多。

10.2 只信 async logger 的佇列

非同步日誌本身不壞。 問題是 fatal path 也往同一佇列堆積就結束

掉下瞬間 worker 停,那整個佇列會消失。

只 fatal path 直接寫 留逃避路徑比較安全。

10.3 在當機處理器 HTTP 傳送

想實作,但相當危險。

  • DNS
  • TLS
  • proxy
  • 認證
  • 逾時
  • 重送等待

全部搭在掉下的脈絡。

傳送放到 重啟後

10.4 有 dump 但和通常日誌不結合

這多。

  • 傾印檔名沒 session
  • 日誌側沒 PID / session
  • watchdog 側沒 PID
  • build 編號不一致

結果 3 個證跡看起來像不同的話

10.5 用 WinForms / WPF 的未處理例外事件延命

外觀「不掉了」最初會被喜歡。 但實際上

  • 只剩畫面
  • worker 死
  • 只剩按鈕啟用
  • 不知是否保存

會造成殭屍狀態。

10.6 沒看 native 側的終止路徑

只用 SetUnhandledExceptionFilter 放心會漏,

  • invalid parameter
  • purecall
  • terminate
  • fast fail

這側。

native C++ 中 不只 SEH,也意識 CRT / C++ 執行環境側的終止路徑 比較好。

11. 最低限的導入檢核表

滿足以下會相當實戰。

  • 通常日誌以 1 行 1 事件留
  • 所有日誌有 UTC、PID、TID、version、session
  • ProcessStartProcessExit
  • 重要邊界事件同步 flush
  • 有最終當機標記專用檔
  • fatal path 不經過 async logger
  • WER LocalDumps 以應用程式單位設定
  • 驗證 dump 儲存位置的 ACL
  • 保存 PDB 和發佈的二進位
  • 下次啟動時能偵測前次異常終止
  • 壓縮 / 上傳 / 通知在重啟後或別程序做
  • native C++ 中也整理 invalid parameter / purecall / terminate
  • 在驗證機意圖地讓它掉,確認 真的留下

最後 1 行特別重要。 光設計沒意義,一定要做「取得到試驗」

12. 試驗到哪

推薦的確認項目如下。

試驗 確認什麼
managed 的未處理例外 通常日誌、fatal 標記、dump 是否都齊
UI 執行緒例外 WinForms / WPF 的事件路徑是否如預期
worker 執行緒例外 是否到 AppDomain.UnhandledException,watchdog 能否偵測
native 例外 WER dump 是否真的取得
invalid parameter / terminate CRT / C++ 執行環境路徑也是否留最小記錄
強制 kill in-process 不可能但 watchdog 側能否記錄 unexpected exit
重啟 下次啟動後的通知、回收、上傳是否動作

不是「例外飛就該有日誌」,而是「在這條件這個檔案會留」確認 重要。

13. 總結

Windows 應用程式即使因程式錯誤的例外掉也想留調查必要資訊,思考方式的軸相當單純。

  • 不期待只靠掉下側的程序
  • 分為通常日誌、最終當機標記、OS / 別程序側的證跡
  • 當機時只往本地短短留
  • 重處理放到重啟後或別程序
  • 以 WER LocalDumps 為基礎
  • 比起繼續,以記錄後終止為基本

簡言之, 「努力最後 1 行」不如「做就算沒最後 1 行也能追的構成」 更強。

即使如此還想要最後 1 行,所以 最終當機標記在別檔短短留。 然後真正的主證跡給 WER 的 dump 和到之前為止的通常日誌。 這是 Windows 應用程式實務上相當穩定的做法。

相關文章

參考資料

  • Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
  • Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
  • Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
  • Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
  • Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
  • Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
  • Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
  • Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
  • Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
  • Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
  • Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
  • Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
  • Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
  • Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
  • Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
  • Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽