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 を呼び出してそれに 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 プロパティを使用して宣言します。 たとえば、下にあるこの 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 のままにして、毎秒 1,000 イベントより頻繁に発生するイベントに対しては 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 ランタイムの例外イベントを有効にしてイベント トレースを使います。
高度なカスタマイズ
オペコードとタスクの設定
ETW には、イベントのタグ付けやフィルター処理を行うためのメカズムであるタスクとオペコードという概念があります。 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 クラスで、最適化された WriteEvent オーバーロードを定義する例を示します。これは、同じコンポーネント内の複数の派生 EventSources によって必要になります。 これらの派生型の 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 クラスには、引数の数が変化するものを含めて、WriteEvent のオーバーロードがいくつかあります。 他のオーバーロードのどれとも一致しなければ、params メソッドが呼び出されます。 残念ながら、params オーバーロードは比較的高コストです。 特に次の場合です。
- 可変個引数を保持する配列を割り当てます。
- 各パラメーターをオブジェクトにキャストします。これにより、値の型の割り当てが発生します。
- これらのオブジェクトを配列に割り当てます。
- 関数を呼び出します。
- 各配列要素の型を特定して、シリアル化する方法を決定します。
これはおそらく、特殊な型と同様にコストが 10 倍から 20 倍になります。 これは、イベントの量が少ない場合にはあまり問題になりませんが、大量の場合は重大な影響が生じることがあります。 params オーバーロードが使用されないようにする有力な事例が 2 つあります。
- 列挙型を "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