插裝程式代碼以建立 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
,用來表示這個單一實例,這樣做相當方便。
定義事件方法的規則
- EventSource 類別中定義的任何實例、非虛擬、void 傳回方法預設都是事件記錄方法。
- 只有在以 System.Diagnostics.Tracing.EventAttribute 標示虛擬或非 void 傳回方法時,才會包含這些方法
- 若要將限定方法標示為非記錄,您必須使用 System.Diagnostics.Tracing.NonEventAttribute 裝飾
- 事件記錄方法具有與其相關聯的事件標識符。 這可以藉由使用 System.Diagnostics.Tracing.EventAttribute 裝飾這個方法,或在類別中透過方法的序數隱式完成。 例如,使用隱含編號類別中的第一個方法具有標識碼 1、第二個方法具有標識碼 2 等等。
- 事件記錄方法必須呼叫 WriteEvent、WriteEventCore、WriteEventWithRelatedActivityId 或 WriteEventWithRelatedActivityIdCore 多載。
- 無論隱含還是明確,事件標識碼都必須符合傳遞給它所呼叫之 WriteEvent* API 的第一個自變數。
- 傳遞至 EventSource 方法的自變數數目、類型和順序必須與傳遞至 WriteEvent* API 的方式一致。 針對 WriteEvent,自變數會遵循事件標識符,針對 WriteEventWithRelatedActivityId,自變數會遵循 relatedActivityId。 針對 WriteEvent*Core 方法,自變數必須手動串行化為
data
參數。 - 事件名稱不能包含
<
或>
個字元。 雖然使用者定義的方法也無法包含這些字元,但編譯程式會重寫async
方法以包含這些字元。 若要確定這些產生的方法不會成為事件,請使用 NonEventAttribute標記 EventSource 上的所有非事件方法。
最佳做法
- 衍生自 EventSource 的類型通常沒有階層中的中繼類型或實作介面。 如需某些可能很有用的例外狀況,請參閱下方 進階自定義。
- 一般而言,EventSource 類別的名稱不適合作為公用的 EventSource 名稱。 公用名稱,記錄組態和記錄查看器中顯示的名稱應該是全域唯一的。 因此,最好使用 System.Diagnostics.Tracing.EventSourceAttribute為 EventSource 提供公用名稱。 上述使用的名稱「Demo」過於簡短,也不具唯一性,因此不適合用於生產運作環境。 常見的慣例是使用階層式名稱搭配
.
或-
做為分隔符,例如 “MyCompany-Samples-Demo”,或是 EventSource 提供事件的元件或命名空間名稱。 不建議在公用名稱中包含 「EventSource」。 - 明確指派事件標識碼,如此一來,對來源類別中的程式代碼進行看似良性變更,例如重新排列或新增中間的方法不會變更與每個方法相關聯的事件標識符。
- 撰寫代表工作單位開始和結束的事件時,依照慣例,這些方法會以後綴 『Start』 和 『Stop』 命名。 例如,『RequestStart』 和 『RequestStop』。
- 除非需要為相容性考量,否則請勿明確指定 EventSourceAttribute 的 Guid 屬性值。 默認 Guid 值衍生自來源的名稱,這可讓工具接受更容易閱讀的名稱,並衍生相同的 Guid。
- 在執行與觸發事件相關的任何資源密集型工作之前,先呼叫 IsEnabled(),例如計算在事件已停用時不必要的高計算成本的事件參數。
- 嘗試讓 EventSource 物件保持相容,並適當地加以版本設定。 事件的預設版本為 0。 您可以藉由設定 EventAttribute.Version來變更版本。 每當您變更與事件序列化相關的資料時,請更新事件的版本。 一律將新的串行化數據新增至事件宣告的結尾,也就是在方法參數列表的結尾。 如果無法這樣做,請使用新的標識碼建立新的事件,以取代舊的事件。
- 宣告事件方法時,請在可變大小的數據之前指定固定大小的承載數據。
- 請勿使用包含 Null 字元的字串。 產生 ETW EventSource 的指令清單時,會將所有字串宣告為以空結尾,即使在 C# 字串中有可能包含空字元。 如果字串包含 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
的成員所定義。
最佳做法
關鍵詞在區分大量事件時更為重要。 這可讓事件取用者將事件的詳細程度提升至高階,但只啟用事件的窄子集來控制效能負擔和日誌大小。 每秒觸發超過 1,000 次的事件是很適合用作唯一關鍵詞的對象。
支援的參數類型
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的結構。 只有具有可串行化類型的公用實例屬性才會串行化。
- 匿名型別,其中所有公用屬性都是可序列化的型別
- 可串行化類型的陣列
- 可為 Null 的<T>,其中 T 是可串行化的型別
- KeyValuePair<T、U>,其中 T 和 U 都是可以序列化的類型
- 針對一個類型 T 實作 IEnumerable<T> 的類型,其中 T 是可串行化的型別
故障排除
EventSource 類別是設計來讓它預設永遠不會擲回例外狀況。 這是一個有用的屬性,因為記錄通常被視為選擇性,而且您通常不希望寫入記錄訊息的錯誤導致您的應用程式失敗。 不過,這會使您在 EventSource 中發現任何錯誤變得困難。 以下是數種可協助進行疑難解答的技術:
- EventSource 建構函式具有採用 EventSourceSettings的多載。 請嘗試暫時啟用 ThrowOnEventWriteErrors 旗標。
- EventSource.ConstructionException 屬性會儲存驗證事件記錄方法時所產生的任何例外狀況。 這可顯示各種撰寫錯誤。
- EventSource 會使用事件標識碼 0 來記錄錯誤,而且此錯誤事件具有描述錯誤的字串。
- 偵錯時,也會使用 Debug.WriteLine() 記錄相同的錯誤字串,並顯示在偵錯輸出視窗中。
- EventSource 會在內部拋出例外狀況,並在發生錯誤時加以攔截。 若要觀察發生這些例外狀況的時間,請在調試程式中啟用第一次機會例外狀況,或使用事件追蹤搭配 .NET 運行時間的 例外狀況事件 啟用。
進階自定義
設定 OpCode 和任務
ETW 具有 Tasks 和 OpCodes的概念,這是標記和篩選事件進一步的機制。 您可以使用 Task 和 Opcode 屬性,將事件與特定工作和作業碼產生關聯。 以下是範例:
[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;
}
}
您可以通過宣告兩個事件方法來隱含地建立 EventTask 物件,這些方法的事件標識符須具備依序命名模式,分別為 <EventName>Start 和 <EventName>Stop。 這些事件必須在類別定義中相鄰宣告,且 <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 多載(請參閱以下 優化高容量事件的效能)。
只要抽象基類未定義任何關鍵詞、工作、opcode、通道或事件,就可以使用。 以下是 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 方法。 不幸的是,參數多載相對昂貴。 特別是:
- 分配一個陣列用來保存可變參數。
- 將每個參數轉換成 物件,這會導致實值型別的配置。
- 將這些物件指派給陣列。
- 呼叫函式。
- 找出每個陣列元素的類型,以判斷如何序列化它。
這可能比特殊類型高 10 到 20 倍。 對於低量案例而言,這並不重要,但對於大量事件而言,這很重要。 有兩個重要情況可保證不會使用參數重載:
- 請確保將列舉型別轉換為 'int',以便與其中一個快速過載方法匹配。
- 為高容量的有效載荷建立新的快速 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);
}