Windows 事件記錄・ETW 入門 ── 讓業務應用程式的日誌搭上 OS 標準機制

· · CSharp, .NET, 事件記錄, ETW, EventSource, 日誌設計, 故障調查, Windows 開發

在用 C# 開發業務應用程式或 Windows 服務時,通常會用自製的檔案記錄器,或 Serilog、NLog 這類函式庫來輸出每天的日誌。那麼 Windows 事件記錄或 ETW(Event Tracing for Windows)是不是已經不需要了呢?並非如此。這兩者並不是「檔案日誌的替代品」,而是維運人員或 OS 標準工具能看到的另一個層級的記錄

本文將整理檔案日誌、事件記錄、ETW 的分工方式,.NET 上的實作方法,ETW 最基本的重點,收集與調查的實務,以及現場常踩到的陷阱。

1. 先下結論

  • 三種手段不是替代關係,而是分工。 檔案日誌是開發者事後追查細節用的記錄,事件記錄是讓維運人員或 OS 標準的事件檢視器、監控工具察覺異常用的記錄,ETW 則是為了效能調查或難以重現的缺陷分析,依需要啟用的高頻率追蹤。基本上三者會一起使用,只是各自寫入的資訊量不同。
  • 事件記錄只寫「啟動、停止、嚴重錯誤」。 如果把所有日誌都灌進事件記錄,維運人員真正該看到的異常反而會被埋沒。檔案日誌一如既往寫得詳細,事件記錄則要收斂範圍。
  • 註冊事件來源需要系統管理員權限。 EventLog.CreateEventSource 從 Windows Vista 之後,沒有系統管理員權限就無法呼叫。應避免設計成在執行時第一次存取才註冊,而是在安裝程式階段就先完成註冊。1
  • ETW 並不是「隨時都在收集」。 即使用 EventSource 定義好了事件,只要沒有訂閱者(dotnet-trace、PerfView、以 ETW 為基礎的工具),就什麼都不會被記錄下來。基本用法是只在做效能分析或特定缺陷調查時,才指定目標 provider 進行收集。2
  • 訊息要用範本格式撰寫,而不是字串串接或字串插值。logger.LogInformation("Order {OrderId} failed", orderId) 這樣寫,就能以結構化日誌的形式,用具名的預留位置搜尋 OrderId。用字串串接或插值組出來的訊息,會讓這個結構資訊消失。3
  • 日誌肥大化的問題,常常是因為預設值太小。 事件記錄的預設最大容量只有 512KB,如果業務應用程式會日常性地使用事件記錄,就應該明確擴大容量,並在設計階段就決定容量滿了之後的行為(要覆寫舊資料,還是捨棄新資料)。4

下面是判斷表。

觀點 檔案日誌 事件記錄 ETW
主要讀者 開發者、維護人員 維運人員、監控工具、OS 標準的事件檢視器 效能調查人員、支援工程師
輸出量參考 詳細(可以包含 Trace/Debug) 少(僅啟動、停止、嚴重錯誤) 啟用時才大量產生。預設情況下什麼都不會記錄
生命週期・保存 依輪替設定,容易長期保存 達到日誌容量上限後,會從舊的開始覆寫或捨棄 以工作階段為單位。收集完成後以 .etl/.nettrace 檔案儲存
與 OS 標準工具的整合 無(需要自製檢視器) 可用事件檢視器、wevtutilGet-WinEvent 等標準方式查看 用 dotnet-trace、PerfView、WPR/WPA 查看
權限 僅需應用程式執行使用者的寫入權限 註冊來源需要系統管理員權限(若來源已註冊,寫入本身標準使用者也可以) 開始收集多半需要系統管理員權限

2. Windows 事件記錄的基礎

Windows 預設有 Application、System、Security 三種日誌,其中 Security 為唯讀。業務應用程式或服務會寫入 Application 日誌,或應用程式自訂的日誌(頻道)。裝置驅動程式則慣例上寫入 System 日誌。5

寫入的單位是「事件來源(event source)」。來源是用來識別應用程式的名稱,要先用 EventLog.CreateEventSource 註冊。從 Windows Vista 開始,註冊來源需要系統管理員權限。 這是因為要確認來源名稱的唯一性,必須搜尋包含 Security 日誌的所有事件記錄,而標準使用者沒有存取 Security 日誌的權限。1

using System.Diagnostics;

// 在安裝程式或設定流程中,於擁有系統管理員權限期間執行
const string SourceName = "KomuraSoft.OrderService";
const string LogName = "Application";

if (!EventLog.SourceExists(SourceName))
{
    EventLog.CreateEventSource(SourceName, LogName);
}

如果想在執行時第一次存取才建立來源,在應用程式以標準使用者身分執行的環境中,這個呼叫就會失敗。關於何時需要系統管理員權限的一般性說明,整理在「Windows 什麼時候需要系統管理員權限」一文中,事件來源的註冊正好符合這個模式:在安裝程式中完成註冊,應用程式本體只寫入已註冊好的來源,才是安全的設計。

事件會附帶「層級(level)」與「事件識別碼(event ID)」兩種識別資訊。層級是 EventLogEntryType(Information、Warning、Error、SuccessAudit、FailureAudit),會用於事件檢視器的圖示與預設篩選條件。事件識別碼是應用程式自行定義的整數,用作日後搜尋、統計同類事件的鍵值。

3. 從 .NET 寫入事件記錄

.NET 寫入事件記錄的路徑大致有兩種。

3.1 直接使用 System.Diagnostics.EventLog

這是低階 API,適合需要精細控制(分類、附加二進位資料等)的場合。在 .NET(非 .NET Framework)中,需要參考 System.Diagnostics.EventLog 這個 NuGet 套件。

using System.Diagnostics;

public sealed class OrderServiceEventLog
{
    private const string SourceName = "KomuraSoft.OrderService";

    public void WriteServiceStarted()
    {
        EventLog.WriteEntry(SourceName, "已啟動 OrderService。",
            EventLogEntryType.Information, eventID: 1000);
    }

    public void WriteFatalError(Exception ex)
    {
        EventLog.WriteEntry(SourceName,
            $"發生嚴重錯誤,無法繼續處理。詳細內容請確認檔案日誌。{ex.GetType().Name}: {ex.Message}",
            EventLogEntryType.Error, eventID: 1999);
    }
}

3.2 使用 ILogger + AddEventLog

如果已經用 ILogger 建立了結構化日誌,使用 Microsoft.Extensions.Logging.EventLog 套件的 AddEventLog,會更順理成章地接上既有的日誌管線。以 ASP.NET CoreGeneric Host 為基礎的 Worker Service,在 Windows 上執行時預設就會啟用 EventLog provider。6

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddEventLog(settings =>
{
    settings.SourceName = "KomuraSoft.OrderService";
    settings.LogName = "Application";
    // 除了預設的分類篩選之外,事件記錄只讓
    // Warning 以上等級通過(詳細內容交給檔案日誌那邊)
    settings.Filter = (_, level) => level >= LogLevel.Warning;
});

using IHost host = builder.Build();
await host.RunAsync();

如果省略 EventLogSettingsLogName 預設會是 "Application"SourceName 預設會是 ".NET Runtime"7 如果維持 .NET Runtime 這種通用的來源名稱,在事件檢視器上會很難分辨究竟是自己應用程式的日誌,還是其他 .NET 應用程式的日誌,因此請務必把 SourceName 明確指定為自家應用程式專用的名稱。

在 Windows 服務或工作排程器上執行的批次處理中,只把「啟動」「停止」「嚴重錯誤」寫入事件記錄,其他詳細內容交給檔案日誌,是實務上比較均衡的做法。服務的建立方式與維運,整理在「Windows 服務的建立方式與維運」;透過工作排程器執行時的問題排查,則整理在「工作排程器的工作不執行、或以 0x1 結束」。這兩種情境下,異常發生時維運人員最先看到的多半是事件記錄,這裡是否留有重點,會直接影響初期調查的速度。

4. 什麼是 ETW ── 貫穿整個 OS 的追蹤基礎

Event Tracing for Windows(ETW)是一套能以共同基礎、從核心到使用者模式應用程式進行儀器化的追蹤機制。它最大的特色,是能把核心事件(磁碟 I/O、程序建立等)和應用程式自訂的事件放在同一條時間軸上,一併記錄與分析。8

.NET 可以透過建立繼承 System.Diagnostics.Tracing.EventSource 的類別,定義自己的 ETW provider。ETW 以發佈—訂閱(Publish-Subscribe)模式運作,沒有訂閱者的話事件就不會被記錄。也就是說,光是實作 EventSource 什麼事都不會發生,要等收集工具明確啟用之後,事件才會真正被記錄下來。2

using System.Diagnostics.Tracing;

[EventSource(Name = "KomuraSoft-OrderService")]
internal sealed class OrderServiceEventSource : EventSource
{
    public static readonly OrderServiceEventSource Log = new();

    [Event(1, Level = EventLevel.Informational, Message = "已開始處理訂單。OrderId={0}")]
    public void OrderProcessingStart(string orderId)
    {
        if (IsEnabled())
        {
            WriteEvent(1, orderId);
        }
    }

    [Event(2, Level = EventLevel.Informational, Message = "訂單處理已完成。OrderId={0}")]
    public void OrderProcessingStop(string orderId)
    {
        if (IsEnabled())
        {
            WriteEvent(2, orderId);
        }
    }
}

呼叫端的用法如下。在真正寫入事件之前,先用 IsEnabled() 確認是否已啟用,可以避免在沒有收集時產生多餘的成本。

OrderServiceEventSource.Log.OrderProcessingStart(order.Id);
try
{
    ProcessOrder(order);
}
finally
{
    OrderServiceEventSource.Log.OrderProcessingStop(order.Id);
}

要收集這個事件,最簡單的方式是使用跨平台工具 dotnet-trace9

dotnet-trace collect --providers KomuraSoft-OrderService -- OrderService.exe

收集到的 .nettrace 檔案,可以用 Visual Studio 或 PerfView 開啟查看內容。如果想做更深入的效能調查,例如連核心事件都要一併分析,就會變成使用 Windows Assessment and Deployment Kit(ADK)內含的 Windows Performance Recorder(WPR)收集,再用 Windows Performance Analyzer(WPA)查看的架構。10 不過,本文的範圍僅止於「ETW 能做什麼、什麼時候用」,不會深入 PerfView 或 WPA 的詳細分析步驟。就開發與維護業務應用程式的角度而言,只要掌握到能用 EventSource 定義自己的 provider、並能用 dotnet-trace 收集,就已經足夠。

5. 結構化日誌的實務

ILogger 的訊息範本,要寫成包含 {PlaceHolder} 這種具名預留位置的固定字串。這不只是表面上的寫法慣例,而是代表在不改變範本本身的前提下,只傳遞引數,就能讓日誌基礎架構那一側維持預留位置名稱與值之間的對應關係(結構化資料)。11

// 建議:範本固定,引數另外傳遞
logger.LogWarning("Order {OrderId} 保留庫存失敗。倉庫={WarehouseId}", orderId, warehouseId);

// 不建議:字串串接、插值會破壞範本與值的對應關係
logger.LogWarning("Order " + orderId + " 保留庫存失敗。倉庫=" + warehouseId);
logger.LogWarning($"Order {orderId} 保留庫存失敗。倉庫={warehouseId}");

程式碼分析器規則 CA2254,可以偵測到這種不建議的模式(範本會隨每次呼叫而改變的寫法)。3 在呼叫頻率高的熱路徑(hot path)上,進一步使用 LoggerMessageAttribute 做原始碼產生,可以避免 boxing 與範本解析的成本。12

using Microsoft.Extensions.Logging;

internal static partial class Log
{
    [LoggerMessage(
        EventId = 2001,
        Level = LogLevel.Warning,
        Message = "Order {OrderId} 保留庫存失敗。倉庫={WarehouseId}")]
    public static partial void StockReservationFailed(
        this ILogger logger, string orderId, string warehouseId);
}

// 呼叫端
logger.StockReservationFailed(orderId, warehouseId);

採用這種架構,同一次日誌呼叫就能分別把詳細內容送到檔案日誌(Serilog/NLog 等 sink)、把收斂後的內容送到事件記錄、把低成本的追蹤透過 EventSourceLoggerProvider 送到 ETW。基本作法是註冊多個 ILoggerProvider,再用 Filter 依 provider 分別調整輸出層級。自製記錄器的最低要求,整理在「自製記錄器的最低要求與整合測試檢查清單」;當機時該如何把日誌與傾印檔綁在一起保留,整理在「Windows 應用程式當機時保留日誌與傾印的設計」,請參考。

6. 收集與調查 ── 讀取已寫入日誌的一方

在事件檢視器中,可以從日誌清單使用「篩選目前的日誌」或「建立自訂檢視」,只取出特定來源、層級、事件識別碼的項目。經常使用的條件,建議存成自訂檢視,之後調查會更快。自訂檢視是用 XPath 查詢定義的,舉例來說,只取出特定事件名稱的查詢大致如下。13

<QueryList>
  <Query Id="0" Path="Application">
    <Select Path="Application">
      *[System[Provider[@Name='KomuraSoft.OrderService'] and (Level=2 or Level=3)]]
    </Select>
  </Query>
</QueryList>

在命令列上,可以用 wevtutil 匯出日誌或變更設定。作為障礙調查的一環,也可以把事件發生前後的事件記錄匯出,送給支援團隊,這種運維方式很實用。14

:: 將 Application 日誌整個匯出成 evtx 檔案
wevtutil epl Application C:\logs\application_20260707.evtx

:: 只顯示特定來源的事件(最近 10 筆)
wevtutil qe Application /q:"*[System[Provider[@Name='KomuraSoft.OrderService']]]" /c:10 /rd:true /f:text

在當機調查中,基本做法是把事件記錄、檔案日誌、當機傾印這三者依時間對齊。以事件記錄中留下的「嚴重錯誤」時間戳記為起點,比對同一時刻的檔案日誌細節,以及用 WER/ProcDump 採集到的傾印檔。傾印檔的採集方式整理在「Windows 當機傾印收集入門 - WER/ProcDump/WinDbg」,採集後實際的分析步驟整理在「用 WinDbg + SOS 讀取當機傾印」。

7. 常見陷阱

  • 沒註冊來源就想寫入,結果失敗。 用未註冊的來源名稱進行相當於 RegisterEventSource 的呼叫時,事件本身確實會寫進 Application 日誌,但因為沒有對應的訊息資源 DLL,事件檢視器無法顯示說明文字,會呈現錯誤狀態。15 更進一步,如果設計成在執行時用 CreateEventSource 自動註冊,很容易在非系統管理員使用者執行的程序第一次啟動時,因例外而當掉。來源註冊務必在安裝程式端就完成。
  • 「竟然可以用別的來源名稱寫入」的混亂。 事件記錄只要有寫入權限,就能用應用程式從未註冊過的其他來源名稱寫入。來源名稱理應在一台電腦上是唯一的,但 OS 並沒有機制去強制這一點,全靠應用程式端自律。5 如果選用過於通用的來源名稱(不含公司名或產品名的短名稱),就容易和其他廠商的應用程式衝突,或不小心沿用了別的應用程式的來源名稱。
  • 日誌肥大化與保存政策。 事件記錄的預設最大容量僅有 512KB。4 如果業務應用程式會日常使用事件記錄,就要用 wevtutil sl 或安裝程式明確擴大容量,並在設計時就意識到達到上限後,要選擇「從舊資料開始覆寫」還是「捨棄新資料」。也要注意 OverwriteOlder 已經被標記為棄用,即使指定了,實際上仍可能變成「不覆寫」的行為。16
  • 多語言環境下的訊息顯示。 如果採用本地化的訊息資源檔(MessageResourceFile),一旦在缺少對應資源 DLL、或版本不一致的環境中檢視日誌,就會顯示成「無法找到事件識別碼 ○○ 的說明」。像是把轉送過來的事件記錄拿到別的伺服器上看,或應用程式已經解除安裝、卻只留下日誌,都是容易出現這類問題的場景。如果採用直接用 WriteEntry 寫入字串(不使用資源檔)的架構,就可以從根本上避免這類問題。
  • 因權限不足而無法寫入的情況。 這裡常被誤解:註冊來源需要系統管理員權限,但寫入已註冊的來源,標準使用者也可以做到。請不要一看到「沒有系統管理員權限就跑不動」就急著把整個應用程式改成以系統管理員身分執行,先分清楚實際上是哪個操作需要權限。

相關文章

相關諮詢領域

合同會社小村軟體處理 Windows 業務應用程式的日誌、事件記錄、ETW 基礎架構設計,以及以障礙調查為前提的觀測點設計技術諮詢。

參考連結

  1. Microsoft Learn, EventLog.CreateEventSource Method. 說明從 Windows Vista 起,建立事件來源需要系統管理員權限的原因(必須搜尋包含 Security 日誌的所有日誌)。  2

  2. Microsoft Learn, Getting Started with EventSource. 說明 EventSource 以發佈—訂閱模式運作,沒有訂閱者(ETW 或 EventPipe)就不會記錄事件,以及用 dotnet-trace collect 收集的方法。  2

  3. Microsoft Learn, CA2254: Template should be a static expression. 說明在日誌訊息範本中使用字串串接或插值,會讓預留位置名稱與值的對應關係消失。  2

  4. Microsoft Learn, EventLog.MaximumKilobytes Property. 說明事件記錄的預設最大容量為 512 KB。  2

  5. Microsoft Learn, EventLog Class. 說明 Application/System/Security 三種預設日誌、來源必須在同一台電腦上保持唯一,以及只要有寫入權限就能用任何已註冊來源名稱寫入的機制。  2

  6. Microsoft Learn, Logging providers in .NET. 說明 Generic Host 預設的記錄提供者包含 Console、Debug、EventSource、EventLog(僅 Windows)。 

  7. Microsoft Learn, EventLogSettings Class. 說明 AddEventLog 的預設值(LogName 為 “Application”、SourceName 為 “.NET Runtime”)。 

  8. Microsoft Learn, Event Tracing. 說明 Event Tracing for Windows(ETW)是用來啟動、停止、儀器化與消費應用程式追蹤事件的機制。 

  9. Microsoft Learn, dotnet-trace performance analysis utility. 說明基於 EventPipe 的跨平台追蹤收集工具 dotnet-trace 的概要與 collect 指令。 

  10. Microsoft Learn, Introduction to WPR. 說明 Windows Performance Recorder(WPR)擴充了 ETW,是提供系統與應用程式詳細記錄的工具。 

  11. Microsoft Learn, Logging in C# and .NET. 說明日誌訊息範本的 {PlaceHolder} 語法,以及範本中的鍵名如何成為日誌的屬性名稱。 

  12. Microsoft Learn, High-performance logging in .NET. 說明透過 LoggerMessageAttribute 產生原始碼的記錄方式,可以避免 boxing 與範本解析的成本。 

  13. Microsoft Learn, Comparison of ETW and EventLog logger functionality. 說明在事件檢視器中使用 XPath 查詢,依事件名稱等條件建立自訂檢視的方法。 

  14. Microsoft Learn, wevtutil. 說明匯出事件記錄(epl)、查詢(qe)、變更設定(sl)等命令列操作。 

  15. Microsoft Learn, Event Sources. 說明用未註冊的來源名稱寫入時會回退到 Application 日誌,但因為沒有訊息檔而導致事件檢視器無法顯示說明文字、呈現錯誤的情況。 

  16. Microsoft Learn, OverflowAction Enum. 說明事件記錄達到容量上限時的行為(覆寫、捨棄),以及 OverwriteOlder 已被標記為棄用。 

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

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

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

常見問題

整理諮詢這個主題時常見的問題。

有檔案日誌的話,事件記錄就不需要了嗎?
如果只是為了做詳細調查,檔案日誌往往就已經足夠。但如果希望維運人員能透過 OS 標準的事件檢視器或監控工具(例如工作排程器的失敗通知、SCOM 等)察覺應用程式的異常,同時把重點寫入事件記錄會更實務。兩者不是替代關係,而是分屬不同層級的記錄。
事件識別碼(Event ID)的編號規則該怎麼決定?
沒有固定答案,但實務上遵守以下三點可以減少事故:『依功能領域分段(例如啟動類用 1000 系列、通訊類用 2000 系列)』、『一旦發出的識別碼不再改變其含義、也不重複使用』、『允許出現跳號』。ILogger 的 EventId 也會被當作結構化日誌的搜尋鍵,因此採用容易日後文件化的編號方式,對維運會很有幫助。
我們在用 Serilog 或 NLog,這篇文章的內容和它們有什麼關係?
Serilog 和 NLog 是在 ILogger 之下,把同一筆日誌分派到檔案、事件記錄、ETW、外部 SaaS 等多個 sink 的其中一種實作方式。本文提到的『事件記錄只寫重點』『不要破壞訊息範本』這些設計方針,無論是直接使用 ILogger,還是透過 Serilog/NLog,都同樣適用。至於要用事件記錄專用的 sink(例如 Serilog.Sinks.EventLog),還是用標準的 AddEventLog,屬於實作上的選擇。
ETW 學到什麼程度才算足夠?
如果主要目的是開發與維護業務應用程式,只要掌握到能用 EventSource 建立自己的 provider、能用 dotnet-trace 收集自己 provider 的事件,就已經足夠。用 PerfView 做呼叫堆疊分析,或用 WPR 收集核心事件,屬於專門做效能調查階段才需要學習的範疇,本文刻意將其排除在範圍之外。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽