0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

EventPipe(EventSource)から流れてくるデータを処理する

Last updated at Posted at 2020-10-13

始めに

前回の記事で、DiagnosticSourceからEventSourceへ出力する記事 を書いたが、今回は、そのEventSourceのデータを処理するためのライブラリの使い方を書く。

これによって、コマンドラインで来たデータを加工したり、あるいはWPFで流す等の処理も書けるようになる。

今回調査に使ったソースをgistに貼っておく。

前提条件

  • 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.ClientMicrosoft.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で生のバイトデータが取得できる
    • 使用後は、必ず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.PayloadNamesTraceEvent.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>には二つ要素が格納されており、それぞれのキーはKeyValueで、値の方にそれぞれプロパティ名と、実際の値が格納されている
具体的には以下のように値を取り出す。

// 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自体はローカル限定なので、ここから外部ネットワークに流す口を作るというのも面白い発想かもしれない。

今回紹介したのはイベントの取得のみだが、その他にもメモリダンプを取ることもできる等、トラブルシューティングに役立つ機能があるので、うまく活用していきたい。

参考資料

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?