始めに
前回の記事で、DiagnosticSourceからEventSourceへ出力する記事 を書いたが、今回は、そのEventSourceのデータを処理するためのライブラリの使い方を書く。
これによって、コマンドラインで来たデータを加工したり、あるいはWPFで流す等の処理も書けるようになる。
前提条件
- dotnet sdk-3.0以降が必要
- 情報収集対象のアプリが、netcoreapp3.0以降
準備(イベント出力側の用意)
前回の記事でも作成したソースを使用する。
using System.Diagnostics;
using System;
using System.Threading.Tasks;
class C1
{
public DiagnosticSource _D = new DiagnosticListener("Diag1");
public void A()
{
if(_D.IsEnabled("ev1"))
{
_D.Write("ev1", new { X = "str" });
}
}
}
class Program
{
static void Main(string[] args)
{
while(true)
{
// 1秒に一回"ev1"イベントが発生する
// 停止機能はないので、Ctrl+C等を使用して止めること
new C1().A();
Task.Delay(1000).Wait();
}
}
}
必要なライブラリ
Microsoft.Diagnostics.NETCore.Client
とMicrosoft.Diagnostics.Tracing.TraceEvent
が必要。
Microsoft.Diagnostics.NETCore.Client
について
EventPipeプロトコルでdotnetプロセスにアタッチするためのライブラリ。dotnetプロセスが用意するEventPipe読み込みの口へ、PIDをキーにしてアクセスして、データを受け取る。
主に使用するクラスは以下(名前空間はMicrosoft.Diagnostics.NETCore.Client
からの相対位置)
- EventPipeProvider
- EventSourceの出力元の情報を保持するクラス
- 捕捉するイベントの種類、レベル、各種フラグ等を設定する
- DiagnosticClient
- EventProviderとPIDを元に、EventPipe接続を行う
StartEventPipeSession
-
public static IEnumerable<int> DiagnosticClient.GetPublishedProcesses()
で、アタッチ可能なPIDの一覧が取得可能
- EventPipeSession
-
DiagnosticClient.StartEventPipeSession
から生成される、セッションオブジェクト -
EventPipeSession.EventStream
で生のバイトデータが取得できる- フォーマットは perfviewリポジトリのドキュメントに記載がある
- 使用後は、必ず
Stop()
メソッドで動作を停止すること
-
さて、これだけではEventPipeの生データが取得できるだけで、中身はバイナリデータなので人に読めるものではない(いや、もしかしたら読める人もいるかもしれないが)。
これを解析してくれるライブラリが、Microsoft.Diagnostics.Tracing.TraceEvent
となる
Microsoft.Diagnostics.Tracing.TraceEvent
について
前述のEventPipeSessionから得たデータを解釈して、プログラム的に解析するためのライブラリ。起点はStreamから読み込むので、先のEventPipeSession.EventStreamをの内容をそのまま出力したファイルを使っても良い
EventStreamの解析に使用する場合、主に使用するクラスはMicrosoft.Diagnostics.Tracing.EventPipeEventSource
である。
EventPipeEventSourceについて
System.IO.Stream
をコンストラクタにとる。Streamには、EventPipeSession.EventStream
から得たデータが入っている想定。
データ解析手順には非同期イベントパターン(EAP)を採用しているので、EventPipeEventSourceに付随するイベントを購読する。
また、IObservable<T>
を返すインターフェイスも提供している。
イベントは、共通のヘッダ部分以外はイベント固有のデータが流れてくるため、そのイベント用の解析クラスを用意する必要がある。
このイベント解析用ベースクラスがTraceEventParser
で、解析クラスはこれを継承して各イベントの解析処理を実装している。
例えば、CLRのイベント(GCの回収イベントとか)は、専用のパーサーが用意されており、EventPipeEventSource.Clrで専用のイベントが用意されている。
なお、汎用的な用途に使えるDynamicTraceEventParser
も存在しており、ライブラリ側で用意されていない場合はこちらを使用することになるだろう。
そして、一通りのイベント設定が終わったら、EventPipeEventSource.Process
で解析を開始する。
注意点として、Process
を実行した時点でスレッドがロックされるため、途中キャンセル等をしたい場合は、
別スレッドを展開して、別のスレッドから、EventPipeEventSource.StopProcessing
と、元のストリームのクローズ(EventPipeSessionからStreamを取得している場合は、EventPipeSession.Stop
を実行する
具体的には以下
// EventPipeSession session;
// 汎用的なイベントを購読
evsrc.Dynamic.All += (TraceEvent evt) =>
{
// 解析処理
};
await Task.WhenAll(
// 解析スレッド
Task.Run(() => evsrc.Process()),
// 停止スレッド
// エンターキーを押したら終了する
Task.Run(() =>
{
Console.ReadLine();
evsrc.StopProcessing();
session.Stop();
}
);
なお、これを通さないAction<TraceEvent> EventPipeEventSource.AllEvents
なるイベントもあるが、これを通すとPayloadNames(イベントデータプロパティの名前)など、主にPayload関連について、セットされないプロパティが出てくる。その場合は、TraceEvent.EventData()
でバイトデータを取得して、自力でパースする必要がある。
EventPipeEventSource.Dynamic.All
の中の解析処理について
EventPipeEventSource.Dynamic.All
の中では、TraceEvent.PayloadNames
とTraceEvent.PayloadByName(string name)
の組み合わせでデータを取得する。具体的には以下のようになる。
// TracEvent traceevt;
foreach(var pname in traceevt.PayloadNames)
{
object pobj = traceevt.PayloadByName(pname);
Console.WriteLine($"{pname} = {pobj}");
}
pobjが何になるか、というところは、プリミティブ型(数値、文字列)ならばそのままキャストすればOK。しかし、それ以外のオブジェクト等がイベント発生元で渡された場合は少々工夫が必要。
ペイロードデータにオブジェクトで来た場合
この場合、実際の型はinternalで隠蔽されているが、取り出す時にはIDictionary<string, object>[]
で取り出すことができる([]
なことに注意)。
IDictionary<string, object>
には二つ要素が格納されており、それぞれのキーはKey
とValue
で、値の方にそれぞれプロパティ名と、実際の値が格納されている
具体的には以下のように値を取り出す。
// object pobj = traceevt.PayloadByName(pname);
if(pobj is IDictionary<string, object>[] pdicar)
{
foreach(var pdic in pdicar)
{
string key = (string)pdic["Key"];
object value = pdic["Value"];
// 解析処理
}
}
より複雑な型の場合はネストして処理が必要になるだろうが、そもそもEventSourceでそこまで複雑なデータは作らない方が良いと思う。
パーサー自体についての詳細なドキュメントは、 githubのmicrosoft/perfviewのドキュメントにある
解析処理の注意点
コールバック内の例外処理
解析コールバックの中で例外をキャッチし損ねると、以後のイベントが流れなくなり、更に、StopProcessingも受け付けなくなった。
なので、現時点ではコールバックの中ではtry-catchで囲った方が良い。
一つもイベントが来ない条件で指定したらフリーズする
少なくともMicrosoft.Diagnostics.Tracing.TraceEvent-2.0.61時点では、一つも該当するイベントが無いようなEventPipeProviderを設定すると、EventPipeEventSourceのコンストラクタで処理が止まってしまうという不具合があるらしい。
詳しい内容は下記issue
終わりに
前回の記事の派生で、EventPipeについて調べてみたけど、取っ掛かりは思ったより簡単だなという感想だった。
ここから、自分なりに使いやすいモニタリングツールを作ってみるのもいいかもしれない。
また、EventPipe自体はローカル限定なので、ここから外部ネットワークに流す口を作るというのも面白い発想かもしれない。
今回紹介したのはイベントの取得のみだが、その他にもメモリダンプを取ることもできる等、トラブルシューティングに役立つ機能があるので、うまく活用していきたい。
参考資料
- dotnet/diagnostics: EventPipe等の記述など
-
microsoft/perfview: 解析ツールのperfviewや、そのライブラリの
Microsoft.Diagnostics.Tracing.TraceEvent
のソース等- microsoft/perfview/documentation/TraceEvent: もう少し詳細なドキュメント
- Microsoft.Diagnostics.NETCore.Clientに関するドキュメント