MetricsEventSourceを利用する

# はじめに

[System.Diagnostics.Metricsの記事](https://qiita.com/skitoy4321/items/ae506268fb18fc2ac54d)で軽く触れた、MetricsEventSourceなるものについて、こちらで解説を入れる。
System.Diagnostics.Metricsについての基本的なことについては上記記事を参照のこと。

# MetricsEventSourceとは

簡単に言うと、 **Metricsで各種Instrumentが出している値を、EventSourceのイベントとして出力しているクラス** である。
[EventSourceについてはdocs.microsoft.comの記事](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.tracing.eventsource?view=net-5.0)を参照。

普通InstrumentはMetricsListenerで拾うのが筋だが、外部ツール等で情報を一時的に収集したいときなどに使う。
[dotnet-trace](https://docs.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace)でEventSourceのイベントをEventPipe経由で収集できるので、MetricsEventSourceを指定して収集するというわけである。

# 使い方

## サンプルプログラム

今回は例として以下のようなプログラムを走らせるとする。
このプログラムでは、

* Meterの名前"appmeter"を作成
* `Counter<int>`のカウンター"counter1"を作成
* "counter1"の値2で、tag1=1とtag1=2となるイベントを交互に500msecの感覚で発生させる

ということを行っている。

```csharp
using System.Diagnostics.Metrics;

{
    Console.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id);
    using var meter = new Meter("appmeter");
    var counter = meter.CreateCounter<int>("counter1", "unit", "desc");
    async Task CounterTask(CancellationToken ct)
    {
        int count = 2;
        int tagval = 1;
        while(!ct.IsCancellationRequested)
        {
            await Task.Delay(500).ConfigureAwait(false);
            if(counter.Enabled)
            {
                counter.Add(count,
                    new KeyValuePair<string, object?>("tag1", tagval)
                    );
            }
            tagval = tagval ^ 3;
        }
    }
    using var cts = new CancellationTokenSource();
    await Task.WhenAll(CounterTask(cts.Token), Task.Run(() =>
    {
        // エンターキーを押したら終了
        Console.ReadLine();
        cts.Cancel();
    }));
}
```

## 収集の開始

収集の開始は`dotnet-trace collect`を使用する。ここで指定するプロバイダは`System.Diagnostics.Metrics`で、簡単な例として以下のように指定する。

```
dotnet-trace collect -p [対象プロセスのPID] --providers "System.Diagnostics.Metrics:::Metrics=appmeter"
```

このコマンドの`--providers`の引数でMetricsEventSourceの監視対象や監視間隔を指定できる。この`--providers`の引数の構造だが、
`[EventSource名]:[監視フラグ(デフォルト0xffffffff=全て]:[レベル(デフォルト5=Verbose)]:[EventSource固有の引数(';'区切りのA=B形式)]`のようになっている。
つまり、上記コマンドの意味は、

* `System.Diagnostics.Metrics`という名前のEventSource名の
* イベントの監視フラグを全て立て
* 監視レベルを5にして
* `System.Diagnostics.Metrics`への引数として`Metrics=appmeter`というキーと値を渡す

という意味になる。
System.Diagnostics.Metricsへ渡せるパラメーターとして、以下のようなものがある

* SessionId
    * トレースセッションを判別するためのID
    * 一つのトレース開始から終了まで保持され、各MetricsEventSourceで発生するイベントに値が付与される
    * デフォルトはランダムなGUID
* Metrics
    * 何のInstrumentを監視するか指定する
    * 形式は`[Meter名]`あるいは`[Meter名]\[カウンタ名]`の二つがある
    * 複数監視する場合は";"で区切る
        * dotnet-traceの引数リストは`;`で分ける仕様なので、値部分を二重引用符(`"`)で囲むこと
* RefreshInterval
    * カウンタの値をとる間隔(sec)
    * doubleなので、0.5秒のような指定も可
    * MetricsEventSourceは記録された値を保持する処理をするものがあるので、メモリ制約にも気を付けること
* MaxTimeSeries
    * 一つのセッションで記録されるInstrumentの数
    * 早い者勝ち
    * デフォルトは1000
    * 超えた場合、
    * ここで保持のためのメモリを消費するのはMeterを保持している側なので注意
* MaxHistograms
    * 一つのセッションで記録されるHistograms/ObservableGaugeの数
    * デフォルトは20
    * 超えた場合、HistogramLimitReachedイベントが起こり、それ以降Histogram系メトリックが作成されても新規追加されなくなる

## イベントキーワード

EventSourceには、キーワードというフラグのようなものがあり、これを指定することによってカテゴリごとに収集するかしないかを指定できる。実際はenumなので、指定するときは数値型に直して指定することになる。

MetricsEventSourceで指定できるキーワードは以下

* Messages(0x1) : 主にデバッグ用途で発生されるイベントのキーワード
* TimeSeriesValues(0x2) : 実際の値が収集されたイベントのキーワード
* InstrumentPublishing(0x4) : Instrumentが作成されたときに発生するイベントのキーワード

## 発生しうるイベント

### `Message(1)`

Keyword=Messages

主にデバッグ用途で使われるその他のメッセージ。
パラメーターは`string message`のみ

### `CollectionStart(2)`

Keyword=TimeSeriesValue

RefreshIntervalで設定された秒数ごとに発生する、メトリックの収集開始イベント
渡されてくるパラメーターは、

* `string sessionId` : セッションID(イベントパラメーターSessionIdの値)
* `DateTime intervalStartTime` : 収集開始時刻
* `DateTime intervalEndTime` : 次の収集開始時刻

の三つ

### `CollectionStop(3)`

Keyword=TimeSeriesValue

メトリック収集が終わったら発生するイベント。渡される引数はCollectionStartと同一

### `CounterRateValuePublished(4)`

Keyword=TimeSeriesValue

`Counter<T>` と、 `ObservableCounter<T>` の値を収集したときに発生するイベント。
パラメーターは以下

* `string sessionId` : セッションID
* `string meterName` : `Meter` 作成時に指定した名前
* `string meterVersion` : `Meter` 作成時に指定した名前
* `string instrumentName` : Instrumentを作成時に指定した名前
* `string unit` : Instrument作成時に指定した単位
* `string tags` : 収集時に追加されたタグの、 `,` 区切りのキーと値のペア(なければ空文字列)
* `string rate` : 増分(少数と整数両方の場合が存在)

rateには `Counter<T>` は、前回収集された時点からAddされた値の総計値が渡される。
`ObservableCounter<T>` は、 `[その時収集した値] - [前回収集した値]` の値が渡される。

[実際の出力(CSV)](https://gist.githubusercontent.com/itn3000/eadf67d5e1eed0d36890014c1645f5d7/raw/f8a5688a5876b6cd022d796fe60a5963570a4c78/zz-result-counter.csv)

### `GaugeValuePublished(5)`

Keyword=TimeSeriesValue

`ObservableGauge<T>` の値を収集したときに発生するイベント。
パラメーターは以下

* `string sessionId` : セッションID
* `string meterName` : `Meter` 作成時に指定した名前
* `string meterVersion` : `Meter` 作成時に指定した名前
* `string instrumentName` : Instrumentを作成時に指定した名前
* `string unit` : Instrument作成時に指定した単位
* `string tags` : 収集時に追加されたタグの、 `,` 区切りのキーと値のペア(なければ空文字列)
* `string lastValue` : その時収集された値

[実際の出力(CSV)](https://gist.githubusercontent.com/itn3000/eadf67d5e1eed0d36890014c1645f5d7/raw/f8a5688a5876b6cd022d796fe60a5963570a4c78/zz-result-gauge.csv)

### `HistogramValuePublished(6)`

Keyword=TimeSeriesValue

`Histogram<T>` の値を収集したときに発生するイベント
パラメーターは以下

* `string sessionId` : セッションID
* `string meterName` : `Meter` 作成時に指定した名前
* `string meterVersion` : `Meter` 作成時に指定した名前
* `string instrumentName` : Instrumentを作成時に指定した名前
* `string unit` : Instrument作成時に指定した単位
* `string tags` : 収集時に追加されたタグの、 `,` 区切りのキーと値のペア(なければ空文字列)
* `string quantiles` : 前回収集された後に発行された`Record(value)`の値の50%ile、95%ile、99%ileの値を、以下の形式でフォーマットしたもの(高い値の方が1に近くなる)
    * `0.5=[50%ileの値];0.95=[95%ileの値];0.99=[99%ileの値]`

[実際の出力(CSV)](https://gist.githubusercontent.com/itn3000/eadf67d5e1eed0d36890014c1645f5d7/raw/f8a5688a5876b6cd022d796fe60a5963570a4c78/zz-result-histogram.csv)

### `BeginInstrumentReporting(7)`/`EndInstrumentReporting(8)`

Keyword=TimeSeriesValue

Instrumentの監視を開始/終了したときに発生する。
パラメーターは以下

* `string sessionId` : セッションID
* `string meterName` : `Meter` 作成時に指定した名前
* `string meterVersion` : `Meter` 作成時に指定した名前
* `string instrumentName` : Instrumentを作成時に指定した名前
* `string instrumentType` : Instrumentの型
* `string unit` : Instrument作成時に指定した単位
* `string description` : Instrument作成時に指定した概要

### `Error(9)`

Keyword=TimeSeriesValues | Messages | InstrumentPublishing

なにがしかの例外が、Instrument監視中に内部で発生したときに発生するイベント
パラメーターは以下

* `string sessionId` : セッションID
* `string errorMessage` : エラーメッセージ

### `InitialInstrumentEnumerationComplete(10)`

Keyword=TimeSeriesValues | InstrumentPublishing

セッションの初期化が終わって、監視を開始する時に発生するイベント
パラメーターは以下

* `string sessionId` : セッションID

### `InstrumentPublished(11)`

Keyword=InstrumentPublishing

Instrumentが作成されて、セッションに捕捉されたときに発生するイベント。
セッションの開始以前に作られたInstrumentは初期化後にこのイベントが発生し、監視対象に入れられる

### `TimeSeriesLimitReached(12)`

Keyword=TimeSeriesValues

監視されるInstrumentの数がMaxTimeSeriesに到達したときに発生するイベント。
パラメーターは `string sessionId` のみ。

これ以後新規に発生するメトリックに関しては、イベントパラメーター`Metrics`に合致しても無視される。

**なお、タグの値が別でも別物として扱われることに注意。**
(tag1=`a`とtag1=`b`のメトリックがあると、それぞれTimeSeriesとしては別物として扱われる)

### `HistogramLimitReached(13)`

Keyword=TimeSeriesValues

監視される`Histogram<T>`の数が上限に達したときに発生するイベント。
パラメーターは`string sessionId`のみ。

これ以後新規に発生する`Histogram<T>`のメトリックに関しては無視される。

[Histogramのカウントに現在バグがあるので、`MaxHistograms`の値は必ず超えないようにすること。](https://github.com/dotnet/runtime/issues/60752)
なお、修正PRはマージされているので遅くともnet7.0にはこの問題は解消する。

### `ObservableInstrumentCallbackError(14)`

Keyword=TimeSeriesValues

`ObservableInstrument<T>` のコールバックの中で例外が発生したときに呼ばれる。
パラメーターは以下。

* `string sessionId` : セッションID
* `string errorMessage` : エラーメッセージ

### `MultipleSessionsNotSupportedError(15)`

Keyword=TimeSeriesValues | Messages | InstrumentPublishing

MetricsEventSourceは [現在二つ以上のセッションを同時展開することはできない仕様となっている。](https://github.com/itn3000/runtime/blob/22f06831ae6e74ae2943dd07ebc062d75e9933d6/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MetricsEventSource.cs#L229-L235)
このイベントは、セッションを追加しようとしたときに発生するエラーイベントとなる。
引数は`string sessionId`のみ

## 注意点

執筆時点の最新版リリースでは以下の問題がある

* ~~[dotnet-traceがEventSourceに関するパラメーターをうまくパースできない](https://github.com/dotnet/diagnostics/issues/2691)~~
    * 二つ以上のEventSourceパラメーターがあると、最後のもの以外正常に解釈できない
    * v6.0.257301で解消
* [MaxHistogramsを超えた状態でさらにHistogramsのRecordを追加しようとすると、MaxCountersの制限に引っかかる場合がある](https://github.com/dotnet/runtime/issues/60752)
    * MaxHistogramsを超えない分には問題ないが、超えてから更に超えた分の`Histograms<T>.Record`が発生すると、なぜかMaxCountersの制限に引っかかって、それ以後Counterが作成されても監視対象にならなくなる
    * 遅くともdotnet7では解消される予定

# 終わりに

Metricsの監視をアプリに組み込む場合は普通にMetricsListenerで処理すればいい話なので、MetricsEventSourceを使う機会は少ないかもしれない。
しかし、こういうものが存在するということがわかっていれば、例えば開発中や、あるいは緊急対応の時などにアドホックに値を監視することによって、トラブルシューティングの役に立つかもしれない。

注意点を最後に挙げたが、これは修正され次第また記事を更新することにする。

## 参考リンク

* [実験ソース](https://gist.github.com/itn3000/eadf67d5e1eed0d36890014c1645f5d7)
* [6.0時点のソース](https://github.com/dotnet/runtime/tree/v6.0.0/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics)