EventSource イベントを作成するコードをインストルメント化する
この記事は、✔️.NET Framework 4.5 以降のバージョン ✔️ .NET Core 3.1 以降のバージョン 適用されます
の概要ガイド では、最小限の 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 を呼び出す本文があり、ID (イベントを表す数値) とイベント メソッドの引数を渡します。 ID は EventSource 内で一意である必要があります。 ID は、System.Diagnostics.Tracing.EventAttribute を使用して明示的に割り当てられます
- EventSources は、シングルトン インスタンスを対象としています。 したがって、このシングルトンを表す
Log
という規則によって、静的変数を定義すると便利です。
イベント メソッドを定義するための規則
- EventSource クラスで定義されている非仮想 void 戻りメソッドのインスタンスは、既定ではイベント ログ メソッドです。
- 仮想メソッドまたは void を返さないメソッドは、System.Diagnostics.Tracing.EventAttribute でマークされている場合にのみ含まれます。
- 特定の条件を満たすメソッドをログ記録しないようにするには、System.Diagnostics.Tracing.NonEventAttribute で装飾する必要があります。
- イベント ログ メソッドには、イベント ID が関連付けられています。 これは、メソッドを System.Diagnostics.Tracing.EventAttribute で修飾することによって明示的に行うか、クラス内のメソッドの序数で暗黙的に行うことができます。 たとえば、クラスの最初のメソッドに暗黙的な番号付けを使用すると、ID が 1 になり、2 つ目のメソッドは ID 2 になります。
- イベント ログ メソッドは、WriteEvent、WriteEventCore、WriteEventWithRelatedActivityId、または WriteEventWithRelatedActivityIdCore オーバーロードを呼び出す必要があります。
- イベント ID は、暗黙的か明示的かに関係なく、呼び出す WriteEvent* API に渡される最初の引数と一致する必要があります。
- EventSource メソッドに渡される引数の数、型、順序は、WriteEvent* API に渡される方法と一致している必要があります。 WriteEvent の場合、引数はイベント ID に従い、WriteEventWithRelatedActivityId の場合、引数は relatedActivityId に従います。 WriteEvent*Core メソッドの場合、引数は手動で
data
パラメーターにシリアル化する必要があります。 - イベント名には、
<
または>
文字を含めることはできません。 ユーザー定義メソッドにもこれらの文字を含めることはできませんが、async
メソッドはコンパイラによって書き換えられ、その文字が含まれます。 これらの生成されたメソッドがイベントにならないように、EventSource のすべての非イベント メソッドを NonEventAttributeでマークします。
ベスト プラクティス
- EventSource から派生する型は、通常、階層内に中間型を持たないか、インターフェイスを実装しません。 これが役立つ可能性があるいくつかの例外については、以下 高度なカスタマイズを参照してください。
- 一般に、EventSource クラスの名前は EventSource の不適切なパブリック名です。 パブリック名 (ログ構成とログ ビューアーに表示される名前) は、グローバルに一意である必要があります。 したがって、System.Diagnostics.Tracing.EventSourceAttributeを使用して EventSource にパブリック名を付けるのが良い方法です。 上記で使用した "Demo" という名前は短く、一意である可能性は低いので、運用環境での使用には適していません。 一般的な規則は、"MyCompany-Samples-Demo" などの区切り記号として
.
または-
を持つ階層名、または EventSource がイベントを提供するアセンブリまたは名前空間の名前を使用することです。 パブリック名の一部として "EventSource" を含めるのはお勧めしません。 - イベント ID を明示的に割り当てます。この方法では、ソース クラス内のコードに対して一見無害な変更を行います 。たとえば、配置を変更したり、途中でメソッドを追加したりしても、各メソッドに関連付けられているイベント ID は変更されません。
- 作業単位の開始と終了を表すイベントを作成する場合、規則により、これらのメソッドの名前にはサフィックス 'Start' と 'Stop' が付けられます。 たとえば、'RequestStart' や 'RequestStop' などです。
- 下位互換性のために必要な場合を除き、EventSourceAttribute の Guid プロパティには明示的な値を指定しないでください。 既定の Guid 値はソースの名前から派生します。これにより、ツールはより人間が判読できる名前を受け入れ、同じ Guid を派生できます。
- イベントの発生に関連するリソースを集中的に使用する処理を実行する前に、IsEnabled() を呼び出します。たとえば、イベントが無効になっている場合に不要なコストの高いイベント引数を計算する場合などです。
- EventSource オブジェクトの互換性を維持し、適切にバージョン管理を試みます。 イベントの既定のバージョンは 0 です。 バージョンは EventAttribute.Version設定することで変更できます。 イベントと共にシリアル化されるデータを変更するたびに、イベントのバージョンを変更します。 イベント宣言の最後、つまりメソッド パラメーターの一覧の末尾に、新しいシリアル化されたデータを常に追加します。 これが不可能な場合は、新しい ID を持つ新しいイベントを作成して、古いイベントを置き換えます。
- イベント メソッドを宣言する場合は、可変サイズのデータの前に固定サイズのペイロード データを指定します。
- null 文字を含む文字列は使用しないでください。 ETW EventSource のマニフェストを生成する際、C# の文字列には null 文字を含めることができるにもかかわらず、すべての文字列が null 終了として宣言されます。 文字列に null 文字が含まれている場合、文字列全体がイベント ペイロードに書き込まれますが、パーサーは最初の null 文字を文字列の末尾として扱います。 文字列の後にペイロード引数がある場合、文字列の残りの部分は、目的の値ではなく解析されます。
一般的なイベントのカスタマイズ
イベントの冗長性レベルの設定
各イベントには詳細レベルがあり、イベント サブスクライバーは多くの場合、EventSource 上のすべてのイベントを特定の詳細レベルまで有効にします。 イベントは、Level プロパティを使用して冗長性レベルを指定します。 たとえば、レベル Informational 以下のイベントを要求するサブスクライバーの下のこの EventSource では、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で属性付けされた構造体。 シリアル化可能な型を持つパブリック インスタンス プロパティのみがシリアル化されます。
- すべてのパブリック プロパティがシリアル化可能な型である匿名型
- シリアル化可能な型の配列
- Nullable<T> (T はシリアル化可能な型)
- KeyValuePair<T、U> ここで T と U は両方ともシリアル化可能な型です
- 厳密に 1 つの型 T に対して IEnumerable<T> を実装し、T がシリアル化可能な型である型
トラブルシューティング
EventSource クラスは、既定で例外をスローしない設計になっていました。 これは便利なプロパティです。ログ記録は省略可能として扱われることが多く、通常、アプリケーションが失敗する原因となるログ メッセージの書き込みエラーは必要ありません。 ただし、これにより、EventSource で間違いを見つけるのが困難になります。 トラブルシューティングに役立ついくつかの手法を次に示します。
- EventSource コンストラクターには、EventSourceSettingsを受け取るオーバーロードがあります。 ThrowOnEventWriteErrors フラグを一時的に有効にしてみてください。
- EventSource.ConstructionException プロパティには、イベント ログ メソッドの検証時に生成された例外が格納されます。 これにより、さまざまな作成エラーが表示される可能性があります。
- EventSource はイベント ID 0 を使用してエラーをログに記録し、このエラー イベントにはエラーを説明する文字列が含まれています。
- デバッグ時には、同じエラー文字列も Debug.WriteLine() を使用してログに記録され、デバッグ出力ウィンドウに表示されます。
- EventSource は内部的に例外をスローし、エラーが発生したときに例外をキャッチします。 これらの例外が発生していることを確認するには、デバッガーで初回例外を有効にするか、.NET ランタイムの 例外イベントを有効にしてイベント トレース 使用します。
高度なカスタマイズ
OpCode とタスクの設定
ETW には、イベントのタグ付けとフィルター処理のためのさらにメカニズムである、タスクと 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;
}
}
イベント ID が連続し、名前付けパターンが <EventName>Start と <EventName>Stop の 2 つのイベント メソッドを宣言することで、EventTask オブジェクトを暗黙的に作成できます。 これらのイベントはクラス定義内で相互に宣言する必要があり、<EventName>Start メソッドが先に来る必要があります。
自己記述 (トレース ログ) とマニフェスト イベントの形式
この概念は、ETW から EventSource をサブスクライブする場合にのみ重要です。 ETW には、イベントを記録するための 2 つの異なる方法があり、それはマニフェスト形式と自己記述型(トレースログとも呼ばれる)形式です。 マニフェスト ベースの 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 クラスから直接派生する型を記述できます。 ただし、カスタマイズされた WriteEvent オーバーロードなど、複数の派生 EventSource 型で共有される機能を定義すると便利な場合があります (後述 大量のイベントのパフォーマンスの最適化 参照)。
抽象基底クラスは、キーワード、タスク、オペコード、チャネル、またはイベントを定義しない限り使用できます。 UtilBaseEventSource クラスが、同じコンポーネント内の複数の派生 EventSource で必要な最適化された WriteEvent オーバーロードを定義する例を次に示します。 これらの派生型の 1 つを 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 クラスには、可変数の引数に対して 1 つを含む、WriteEvent のオーバーロードが多数用意されています。 他のオーバーロードが一致しない場合は、params メソッドが呼び出されます。 残念ながら、パラメーターのオーバーロードは比較的高価です。 具体的には次のようになります。
- 変数引数を保持する配列を割り当てます。
- 各パラメーターをオブジェクトにキャストします。これにより、値型の割り当てが発生します。
- これらのオブジェクトを配列に割り当てます。
- 関数を呼び出します。
- 各配列要素の型を調べて、それをシリアル化する方法を決定します。
これは、特殊な型の 10 ~ 20 倍のコストがかかる可能性があります。 これはボリュームの少ないケースでは重要ではありませんが、大量のイベントの場合は重要な場合があります。 params オーバーロードが使用されていないことを保証するために、次の 2 つの重要なケースがあります。
- 列挙型が高速オーバーロードの 1 つと一致するように、列挙型が 'int' にキャストされていることを確認します。
- 大量のペイロード用の新しい高速 WriteEvent オーバーロードを作成します。
4 つの整数引数を受け取る 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);
}
.NET