共用方式為


檢測程式碼以建立 EventSource 事件

本文適用於:✔️.NET Core 3.1 和更新版本 ✔️ .NET Framework 4.5 和更新版本

使用者入門指南說明如何建立最少的 EventSource,並在追蹤檔案中收集事件。 本教學課程詳細說明如何使用 System.Diagnostics.Tracing.EventSource 建立事件。

最小 EventSource

[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log { get; } = new DemoEventSource();

    [Event(1)]
    public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
}

衍生 EventSource 的基本結構一律相同。 特別是:

  • 類別繼承自 System.Diagnostics.Tracing.EventSource
  • 針對您想要產生的每個不同型別事件,必須定義方法。 此方法應該使用所建立事件的名稱來命名。 如果事件有其他資料,則應該使用引數傳遞這些資料。 這些事件引數必須序列化,因此只允許某些型別
  • 每個方法都有一個主體,會呼叫 WriteEvent 將識別碼 (代表事件的數值) 和事件方法的引數傳遞給它。 識別碼在 EventSource 內必須是唯一的。 識別碼是使用 System.Diagnostics.Tracing.EventAttribute 明確指派的
  • EventSources 是單一資料庫執行個體。 因此,藉由稱為 Log 的慣例定義靜態變數是方便的,這代表此單一資料庫。

定義事件方法的規則

  1. EventSource 類別中定義的任何執行個體、非虛擬、void 傳回方法,預設都是事件記錄方法。
  2. 只有在以 System.Diagnostics.Tracing.EventAttribute 標記時,才會包含虛擬或非 void 傳回方法
  3. 若要將限定方法標示為非記錄,您必須使用 System.Diagnostics.Tracing.NonEventAttribute 進行裝飾
  4. 事件記錄方法具有與其相關聯的事件識別碼。 這可以藉由以 System.Diagnostics.Tracing.EventAttribute 裝飾方法來明確完成,或使用類別中方法的序數來隱含完成。 例如,使用隱含編號,類別中第一個方法具有識別碼 1、第二個方法具有識別碼 2,依此類推。
  5. 事件記錄方法必須呼叫 WriteEventWriteEventCoreWriteEventWithRelatedActivityIdWriteEventWithRelatedActivityIdCore 多載。
  6. 不論隱含還是明確,事件識別碼都必須符合傳遞給其呼叫之 WriteEvent* API 的第一個引數。
  7. 傳遞至 EventSource 方法的引數數目、型別和順序,必須與傳遞至 WriteEvent* API 的方式一致。 針對 WriteEvent,引數接續在事件識別碼後面,針對 WriteEventWithRelatedActivityId,引數接續在 relatedActivityId 後面。 針對 WriteEvent*Core 方法,引數必須手動序列化為 data 參數。
  8. 事件名稱不能包含 <> 字元。 雖然使用者定義的方法也無法包含這些字元,但編譯器會重寫 async 方法以包含這些字元。 若要確定這些產生的方法不會變成事件,請使用 NonEventAttribute 標記 EventSource 上的所有非事件方法。

最佳作法

  1. 衍生自 EventSource 的型別通常沒有階層或實作介面中的中繼型別。 如需一些可能很有用的例外狀況,請參閱下方的進階自訂
  2. 一般而言,EventSource 類別的名稱是 EventSource 的不正確公用名稱。 公用名稱,記錄組態和記錄檢視器中顯示的名稱應該是全域唯一的。 因此,最好使用 System.Diagnostics.Tracing.EventSourceAttribute 為 EventSource 提供公用名稱。 上方使用的名稱 "Demo" 簡短且不太可能是唯一的,因此不適合用於生產環境。 常見的慣例是搭配 .- 作為分隔符號使用階層式名稱,例如 "MyCompany-Samples-Demo",或 EventSource 提供事件的組件或命名空間名稱。 不建議在公用名稱中包含 "EventSource"。
  3. 明確指派事件識別碼,如此一來就會對來源類別中的程式碼進行良性變更,例如重新排列或在中間新增方法,將不會變更與每個方法相關聯的事件識別碼。
  4. 撰寫代表工作單位開始和結束的事件時,依照慣例,這些方法的命名會有尾碼 'Start' 和 'Stop'。 例如,'RequestStart' 和 'RequestStop'。
  5. 除非您針對回溯相容性的理由有此需要,否則請勿指定 EventSourceAttribute 的 GUID 屬性明確值。 預設 GUID 值衍生自來源的名稱,其可讓工具接受人類更容易閱讀的名稱,並衍生相同的 GUID。
  6. 在執行與引發事件相關的任何資源密集工作之前呼叫 IsEnabled(),例如計算事件停用時不需要的昂貴事件引數。
  7. 嘗試讓 EventSource 物件保持回溯相容,並適當地設定版本。 事件的預設版本為 0。 您可以藉由設定 EventAttribute.Version 來變更版本。 每當變更以事件進行序列化的資料時,請變更事件的版本。 一律將新的序列化資料新增至事件宣告的結尾,也就是在方法參數清單的結尾。 如果無法這樣做,請建立具有新識別碼的新事件,以取代舊的事件。
  8. 宣告事件方法時,請先指定固定大小承載資料,再指定可變大小的資料。
  9. 請勿使用包含 Null 字元的字串。 產生 ETW EventSource 的資訊清單時,會將所有字串宣告為以 Null 結尾,即使 C# 字串中可能有 Null 字元也一樣。 如果字串包含 Null 字元,則會將整個字串寫入事件承載,但任何剖析器都會將第一個 Null 字元視為字串結尾。 如果字串後面有承載引數,則會剖析字串的其餘部分,而不是預期的值。

一般事件自訂

設定事件詳細程度層級

每個事件都有詳細程度層級,而事件訂閱者通常會在 EventSource 上啟用所有事件,最高為特定詳細程度層級。 事件會使用 Level 屬性宣告其詳細程度層級。 例如,在此 EventSource 的訂閱者下方,要求 Informational 和較低層級的事件不會記錄 Verbose DebugMessage 事件。

[EventSource(Name = "MyCompany-Samples-Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log { get; } = new DemoEventSource();

    [Event(1, Level = EventLevel.Informational)]
    public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
    [Event(2, Level = EventLevel.Verbose)]
    public void DebugMessage(string message) => WriteEvent(2, message);
}

如果未在 EventAttribute 中指定事件的詳細程度層級,則會預設為 Informational。

最佳做法

針對相對罕見的警告或錯誤,請使用低於 Informational 的層級。 不確定時,請遵守 Informational 的預設值,並針對頻率高於 1000 個事件/秒的事件使用 Verbose。

設定事件關鍵字

某些事件追蹤系統支援關鍵字作為額外的篩選機制。 不同於依詳細資料層級分類事件的詳細程度,關鍵字的目的是根據其他準則來分類事件,例如程式碼功能的區域,或有助於診斷特定問題。 關鍵字會命名為位元旗標,而且每個事件都可以套用任何關鍵字的組合。 例如,下列 EventSource 會定義一些與要求處理相關的事件,以及與啟動相關的其他事件。 如果開發人員想要分析啟動的效能,他們可能只啟用以 startup 關鍵字標示的事件記錄。

[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
    public static DemoEventSource Log { get; } = new DemoEventSource();

    [Event(1, Keywords = Keywords.Startup)]
    public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
    [Event(2, Keywords = Keywords.Requests)]
    public void RequestStart(int requestId) => WriteEvent(2, requestId);
    [Event(3, Keywords = Keywords.Requests)]
    public void RequestStop(int requestId) => WriteEvent(3, requestId);

    public class Keywords   // This is a bitvector
    {
        public const EventKeywords Startup = (EventKeywords)0x0001;
        public const EventKeywords Requests = (EventKeywords)0x0002;
    }
}

關鍵字必須使用名為 Keywords 的巢狀類別來定義,而且每個個別關鍵字都是由具型別 public const EventKeywords 的成員所定義。

最佳做法

區分大量事件時,關鍵字更為重要。 這可讓事件消費者將詳細程度提升為高階,但只啟用事件子集來管理效能額外負荷和記錄大小。 觸發超過 1000/秒的事件是唯一關鍵字的良好候選項目。

支援的參數類型

EventSource 要求所有事件參數都可以序列化,因此只接受一組有限的型別。 這些包括:

  • 基本型別:bool、byte、sbyte、char、short、ushort、int、uint、long、ulong、float、double、IntPtr 和 UIntPtr、Guid decimal、string、DateTime、DateTimeOffset、TimeSpan
  • 列舉
  • System.Diagnostics.Tracing.EventDataAttribute 屬性化的結構。 只有具有可序列化型別的公用執行個體屬性才會序列化。
  • 所有公用屬性都是可序列化型別的匿名型別
  • 可序列化型別的陣列
  • Nullable<T>,其中 T 是可序列化的型別
  • KeyValuePair<T, U>,其中 T 和 U 都是可序列化的型別
  • 針對確切一個型別 T 實作 IEnumerable<T> 的型別,其中 T 是可序列化的型別

疑難排解

EventSource 類別的設計目的是讓其預設永遠不會擲回例外狀況。 這是有用的屬性,因為記錄通常會被視為選擇性,而且您通常不想要讓錯誤寫入記錄訊息造成應用程式失敗。 不過,這會使得在 EventSource 中發現任何錯誤變得困難。 以下是數種可協助疑難排解的技術:

  1. EventSource 建構函式具有採用 EventSourceSettings 的多載。 請嘗試暫時啟用 ThrowOnEventWriteErrors 旗標。
  2. EventSource.ConstructionException 屬性會儲存驗證事件記錄方法時所產生的任何例外狀況。 這可能會顯示各種撰寫錯誤。
  3. EventSource 會使用事件識別碼 0 來記錄錯誤,而這個錯誤事件有描述錯誤的字串。
  4. 偵錯時,也會使用 Debug.WriteLine() 記錄相同的錯誤字串,並顯示在偵錯輸出視窗中。
  5. EventSource 會在內部擲回,然後在發生錯誤時攔截例外狀況。 若要觀察發生這些例外狀況的時間,請在偵錯工具中啟用初次發生例外狀況,或使用已啟用 .NET 執行階段例外狀況事件的事件追蹤。

進階的自訂設定

設定作業碼和工作

ETW 具有工作和作業碼的概念,這些是標記和篩選事件的進一步機制。 您可以使用 TaskOpcode 屬性,將事件與特定工作和作業碼產生關聯。 以下是範例:

[EventSource(Name = "Samples-EventSourceDemos-Customized")]
public sealed class CustomizedEventSource : EventSource
{
    static public CustomizedEventSource Log { get; } = new CustomizedEventSource();

    [Event(1, Task = Tasks.Request, Opcode=EventOpcode.Start)]
    public void RequestStart(int RequestID, string Url)
    {
        WriteEvent(1, RequestID, Url);
    }

    [Event(2, Task = Tasks.Request, Opcode=EventOpcode.Info)]
    public void RequestPhase(int RequestID, string PhaseName)
    {
        WriteEvent(2, RequestID, PhaseName);
    }

    [Event(3, Keywords = Keywords.Requests,
           Task = Tasks.Request, Opcode=EventOpcode.Stop)]
    public void RequestStop(int RequestID)
    {
        WriteEvent(3, RequestID);
    }

    public class Tasks
    {
        public const EventTask Request = (EventTask)0x1;
    }
}

您可以使用具有命名模式 <EventName>Start 和 <EventName>Stop 的後續事件識別碼來宣告兩個事件方法,以隱含方式建立 EventTask 物件。 這些事件必須在類別定義中彼此旁邊宣告,而且 <EventName>Start 方法必須先宣告。

自我描述 (追蹤記錄) 與資訊清單事件格式

只有在訂閱 ETW 的 EventSource 時,這個概念才重要。 ETW 有兩種不同的方式可以記錄事件、資訊清單格式和自我描述 (有時稱為追蹤記錄) 格式。 以資訊清單為基礎的 EventSource 物件會產生並記錄 XML 文件,代表初始化時在類別上定義的事件。 這需要 EventSource 自行反映,才能產生提供者和事件中繼資料。 在每個事件的自我描述格式中繼資料中,會與事件資料內嵌傳輸,而不是預先傳輸。 自我描述方法支援更有彈性的 Write 方法,這些方法可以傳送任意事件,而不需要建立預先定義的事件記錄方法。 啟動時也稍微快一點,因為會避免積極反映。 不過,隨著每個事件所發出的額外中繼資料會增加一些效能額外負荷,這在傳送大量事件時可能不是理想狀況。

若要使用自我描述事件格式,請使用 EventSource(String) 建構函式、EventSource(String, EventSourceSettings) 建構函式,或在 EventSourceSettings 上設定 EtwSelfDescribingEventFormat 旗標來建構 EventSource。

實作介面的 EventSource 型別

EventSource 型別可以實作介面,在各種進階記錄系統中順暢地整合,使用介面來定義通用記錄目標。 以下是可能的用法範例:

public interface IMyLogging
{
    void Error(int errorCode, string msg);
    void Warning(string msg);
}

[EventSource(Name = "Samples-EventSourceDemos-MyComponentLogging")]
public sealed class MyLoggingEventSource : EventSource, IMyLogging
{
    public static MyLoggingEventSource Log { get; } = new MyLoggingEventSource();

    [Event(1)]
    public void Error(int errorCode, string msg)
    { WriteEvent(1, errorCode, msg); }

    [Event(2)]
    public void Warning(string msg)
    { WriteEvent(2, msg); }
}

您必須在介面方法上指定 EventAttribute,否則 (因為相容性原因) 方法不會被視為記錄方法。 不允許明確介面方法實作,以避免命名衝突。

EventSource 類別階層

在大部分情況下,您能夠撰寫直接衍生自 EventSource 類別的型別。 不過,有時候定義多個衍生 EventSource 型別共用的功能會很有用,例如自訂 WriteEvent 多載 (請參閱下方的最佳化大量事件的效能)。

只要抽象基底類別未定義任何關鍵字、工作、作業碼、通道或事件,就可以使用。 以下是 UtilBaseEventSource 類別定義最佳化 WriteEvent 多載的範例,這是相同元件中多個衍生 EventSources 所需的項目。 下列其中一個衍生型別說明為 OptimizedEventSource。

public abstract class UtilBaseEventSource : EventSource
{
    protected UtilBaseEventSource()
        : base()
    { }
    protected UtilBaseEventSource(bool throwOnEventWriteErrors)
        : base(throwOnEventWriteErrors)
    { }

    protected unsafe void WriteEvent(int eventId, int arg1, short arg2, long arg3)
    {
        if (IsEnabled())
        {
            EventSource.EventData* descrs = stackalloc EventSource.EventData[2];
            descrs[0].DataPointer = (IntPtr)(&arg1);
            descrs[0].Size = 4;
            descrs[1].DataPointer = (IntPtr)(&arg2);
            descrs[1].Size = 2;
            descrs[2].DataPointer = (IntPtr)(&arg3);
            descrs[2].Size = 8;
            WriteEventCore(eventId, 3, descrs);
        }
    }
}

[EventSource(Name = "OptimizedEventSource")]
public sealed class OptimizedEventSource : UtilBaseEventSource
{
    public static OptimizedEventSource Log { get; } = new OptimizedEventSource();

    [Event(1, Keywords = Keywords.Kwd1, Level = EventLevel.Informational,
           Message = "LogElements called {0}/{1}/{2}.")]
    public void LogElements(int n, short sh, long l)
    {
        WriteEvent(1, n, sh, l); // Calls UtilBaseEventSource.WriteEvent
    }

    #region Keywords / Tasks /Opcodes / Channels
    public static class Keywords
    {
        public const EventKeywords Kwd1 = (EventKeywords)1;
    }
    #endregion
}

最佳化大量事件的效能

EventSource 類別有一些 WriteEvent 的多載,包括一個用於引數變數數目的多載。 當其他多載都不符合時,會呼叫 params 方法。 不幸的是,params 多載相對昂貴。 特別是:

  1. 配置陣列來保存變數引數。
  2. 將每個參數轉換成物件,這會導致實值型別的配置。
  3. 將這些物件指派給陣列。
  4. 呼叫函式。
  5. 找出每個陣列元素的型別,以判斷如何進行序列化。

這可能比特製化型別高 10 到 20 倍。 這對小量案例而言並不重要,但對於大量事件而言很重要。 有兩個重要案例可確保未使用 params 多載:

  1. 請確定列舉型別會轉換成 'int',使其符合其中一個快速多載。
  2. 為大量承載建立新的快速 WriteEvent 多載。

以下是新增 WriteEvent 多載 (採用四個整數引數) 的範例

[NonEvent]
public unsafe void WriteEvent(int eventId, int arg1, int arg2,
                              int arg3, int arg4)
{
    EventData* descrs = stackalloc EventProvider.EventData[4];

    descrs[0].DataPointer = (IntPtr)(&arg1);
    descrs[0].Size = 4;
    descrs[1].DataPointer = (IntPtr)(&arg2);
    descrs[1].Size = 4;
    descrs[2].DataPointer = (IntPtr)(&arg3);
    descrs[2].Size = 4;
    descrs[3].DataPointer = (IntPtr)(&arg4);
    descrs[3].Size = 4;

    WriteEventCore(eventId, 4, (IntPtr)descrs);
}