はじめに
dotnetには、アプリケーションの統計情報収集フレームワークの一つとして、EventCounterというものが存在している。
この記事では、このEventCounterについて解説する。
なお、EventCounterはEventSourceの上に成り立っているので、EventSourceについての理解は必須だが、長くなるのでこの記事では詳しく解説しない。
そちらの理解がまだならば、 公式ドキュメント 等を参照のこと。
また、主な実装はsrc/libraries/System.Private.CoreLib/System/Diagnostics/Tracingにあるので、実装を見るときはそちらで。
インターフェイスを見たい場合はsrc/libraries/System.Diagnostics.Tracingを参照。
詳細は後述するが、インターフェイスはMetrics API に似ている(EventCounterが先発)。
なので、Metrics APIも併せて知っておくと、より理解が深まるかもしれない。
EventCounterは何のために使うか
dotnetの場合は、GCの回数や所要時間等、数値でモニタリングしたい指標というものがある。
EventCounterというのはそのような情報を外部に対して発信したいときに使うものとなる。
別記事で紹介している Metrics API はあくまでインプロセスで処理を行う必要があり、外部から観測するには何らかの処理追加が必要になるが、EventCounterはEventSourceの上に乗っているものなので、外部からアドホックに取得可能というのが利点としてある。
また、通常のEventSourceに無い特徴として、値の集計等を自動でしてくれるという利点があり、さらに、発信側から一方的に情報を送るのではなく、要求に応じて値を発信するというプル型のアプローチも可能なので、EventSourceで何らかの数値的な指標がある場合は検討してみてもいいと思う。
ただし、あくまでEventSourceの上に乗るものなので、そちら由来の制約もあることに注意すること。
EventCounterの基本的な実装
情報を発信するため、EventCounterを使用する。
EventCounterを使うには、まず紐づけるためのEventSourceを作る必要がある。
EventSourceは必ずしもイベントを作る必要はなく、以下のように空でも構わない。
ただ、各インスタンスの生存期間等を考えると、実用上はEventCounterはEventSourceに内包させ、
何かのイベントに付随させて値を記録することが多いので、ここでも後でそのようにする。
また、クラスに付与するEventSourceの名前は、EventCounterの特定に必要なので、しっかり考える必要がある。
using System.Diagnostics.Tracing;
// 別名を付けたい場合はName属性を設定する。明示的につけなければ、クラス名が名前として使われる。
[EventSource(Name = "My-Event-Source")]
class MyEventSource : EventSource
{
// シングルトンにしたいため、勝手にインスタンス化されないようにする
private MyEventSource() {}
public static readonly MyEventSource Log = new MyEventSource();
}
そして、作ったEventSourceにEventCounterを作って紐づける。
注意点として、
- 生成時にグローバルで管理しているリストの中に登録するため、重複してインスタンスを作らないようにする
- 個々のカウンターは名前で識別するため、同じEventSource内で名前が被らないようにする
以上より、多くの場合はEventSourceの初期化時に一緒にインスタンスを作成することが多い。
追加の属性としてstring DisplayUnits
とstring DisplayName
が設定できる。
また、その他のメタ属性を設定したい場合は、EventCounter.AddMetaData(string, string)
を使うことができる。
これらは後述するイベントを取得する側に渡されるので、イベント取得時の振り分けや表示等に役立てることができる。
using System.Diagnostics.Tracing;
// 別名を付けたい場合はName属性を設定する。明示的につけなければ、クラス名が名前として使われる。
[EventSource(Name = "My-Event-Source")]
class MyEventSource : EventSource
{
private readonly EventCounter _Counter;
// シングルトンにするため、勝手にインスタンス化されないようにする
private MyEventSource()
{
// EventCounterインスタンス生成時にEventSource紐づける必要がある
_Counter = new EventCounter("CounterName", this);
}
public static readonly MyEventSource Log = new MyEventSource();
// イベント発生と同時に値を記録するパターン
// WriteEventとWriteMetricの順番はどちらが先でも構わない
[Event(1)]
public void Event1()
{
WriteEvent(1);
if(IsEnabled())
{
_Counter?.WriteMetric(1);
}
}
// イベントの記録無しで値だけ記録するパターン
// EventSource内でpublicなメソッドを追加する場合は、
// NonEventAttributeかEventAttributeを明示的に追加しないと、既存のイベントとIDが競合する場合があり、
// 正常にイベントの監視ができなくなる
[NonEvent]
public void CountOnly()
{
if(IsEnabled())
{
_Counter?.WriteMetric(1);
}
}
// コンストラクタ以外で作るパターン
// OnEventCommandとは、EventListenerや外部からEventSourceの監視開始、終了等が通知された時に発生するイベント
protected override void OnEventCommand(EventCommandEventArgs args)
{
if (args.Command == EventCommand.Enable)
{
_Counter ??= new EventCounter("CounterName", this);
{
DisplayName = "counter description",
DisplayUnits = "metrics_unit"
};
}
}
protected override void Dispose(bool disposing)
{
// EventSourceの生存期間>カウンターの生存期間にすること
_Counter?.Dispose();
_Counter = null;
base.Dispose();
}
}
後は MyEventSource.Log
インスタンスを通して操作するだけである。
EventCounterの種類
EventCounter
が代表的存在ではあるが、他にも以下のようなカウンターが存在する。
- IncrementingEventCounter
- PollingCounter(netcoreapp3.1以降)
- IncrementingPollingCounter(netcoreapp3.1以降)
また、EventCounterを含めてこれらの型のベースとなるDiagnosticCounter
が存在する
共通項目(DiagnosticCounter)
全てのカウンターはDiagnosticCounterというクラスから派生している。
各カウンタ共通で設定できる項目は以下の通り
- DisplayName: 表示名
- DisplayUnits: 表示単位
- あくまで表示用なので、制約はない
- Name: プログラム上で扱う名前
- 識別用に使うので、一意かつプログラム的に処理しやすい名前である方が良い
また、その他独自のメタデータをAddMetaData(string, string)
として設定できる。
これらの項目はスレッドセーフではないので、生成時にまとめてやっておくのが吉
EventCounter
代表的なカウンター。
EventCounter.WriteMetric(double)
で値を記録する。
カウンターで現れる値は、監視周期の間に発行された値の平均値と標準偏差となる。
収集イベント発生時に渡される値の型はEventPayload
だが、入っている値は後述するCounterPayload
。
IncrementingCounter
IncrementingCounter.Increment(double)
で、値の平均ではなく増分値を記録する。
カウンターで現れる値は、監視周期の間に発行された値のトータルの値となる。
収集イベント発生時に渡される値の型はEventPayload
だが、入っている値は後述するIncrementingCounterPayload
PollingCounter
netcoreapp3.1以降で使用可能
EventCounterとは異なり、下記のように、収集時に値を取得するためのコールバック(Func<double>
)を設定する。
PollingCounter PC1 = new PollingCounter("[名前]", eventSource, () => [返す値]);
注意が必要なのが、このコールバックはスレッドセーフとは限らないため、
内部で値を操作する場合は、Interlocked等の排他処理を忘れないようにすること。
収集イベント発生時に渡される値の型はEventPayload
だが、入っている値は後述するCounterPayload
IncrementingPollingCounter
netcoreapp3.1以降で使用可能
PollingCounterと同じくコールバック方式で、イベントリスナーの方には
今回の値から前回の値を減算した値が渡されるため、その時の総計値を返すようにする。
IncrementingPollingCounter IPC1 = new IncrementingPollingCounter("[名前]", eventSource, () => [値の総計値]);
こちらも同じく呼び出しはスレッドセーフではないため、中で値を操作する場合は排他処理を忘れないようにしよう。
TimeSpan DisplayRateTimeScale
なるプロパティが存在するが、これは監視側がどの程度の間隔で監視したら良いかという目安を示すもので、
監視側は必ずしもこれに従う必要はない。
収集イベント発生時に渡される値の型はEventPayload
だが、入っている値は後述するIncrementingCounterPayload
EventPayload
定義は src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/TraceLogging/EventPayload.cs にある。
内部的に宣言されている型なので、直接アクセスすることはできないが、IDictionary<string, object>
を実装しているので、キャストして使うことができる。
カウンターの各クラスは、イベント発行時にそれぞれの型からこのEventPayloadに値を詰め込み、最終的にEventSourceのPayloadに入れている。
CounterPayload
EventCounter及びPollingCounterを使った時にEventPayloadに詰め込まれる値の型。
取り出せる値は以下。
キー | 型 | 内容 |
---|---|---|
Name | string | Counterの名前 |
DisplayName | string | DisplayNameの値そのまま |
Mean | double | 平均値 |
StandardDeviation | double | 標準偏差 |
Count | int | 前回の収集から発行された回数 |
Min | double | 最低値 |
Max | double | 最大値 |
IntervalSec | float | 前回収集からの間隔(秒) |
Series | string | 現在は固定でIntervalSec=[監視間隔(ミリ秒)]
|
CounterType | string | 固定で"Mean" |
Metadata | string |
AddMetaData で追加した値が[キー1]:[値1],[キー2]:[値2] のように入る |
DisplayUnits | string | CounterのDisplayUnitsそのまま |
定義は src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterPayload.cs にある。
Series
フィールドのみ存在意義が不明だが、コメントを見る限り、将来的にマルチセッションの時にフォーマットが変わるかもしれない。
IncrementingCounterPayload
IncrementingCounterとIncrementingPollingCounterを使ったときに、EventPayloadに詰め込まれる値の型
取り出せる値は以下
キー | 値 | 内容 |
---|---|---|
Name | string | Counterの名前 |
DisplayName | string | DisplayNameの値そのまま |
Increment | double | 前回収集から記録された値の合計値 |
IntervalSec | double | 前回収集から経過した時間(sec) |
Metadata | string |
AddMetaData で追加した値が[キー1]:[値1],[キー2]:[値2] のように入る |
Series | string | 現在は固定でIntervalSec=[監視間隔(ミリ秒)]
|
CounterType | string | 固定で"Sum" |
DisplayUnits | string | CounterのDisplayUnitsそのまま |
定義はCounterPayloadと同じ src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/CounterPayload.cs にある。
観測方法
EventListener
監視開始
インプロセスでイベントの値を取得するにはEventSourceと同様に System.Diagnostics.Tracing.EventListener
を使用する。
ただし、EventSourceの監視開始時(EventListener.EnableEvents()
)に、オプションとしてEventCounterIntervalSec=[監視周期(秒)]
を追加する必要がある。
EventCounterのイベントはLogAlwaysレベルで発生するので、特にEventLevelに何を指定するかは問わない。
また、EventKeywordも特に設定はされていない。
オプションを指定した状態でEventListenerでの監視を開始すると、EventCounterIntervalSecで設定した秒数ごとに、監視イベントが発生する。
サンプルコードは以下。
using System.Diagnostics.Tracing;
// このクラスをnewすれば監視開始できる(Disposeで監視停止)
class MyEventListener: EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
if(eventSource.Name == "[EventSourceの名前]")
{
this.EnableEvents(eventSource, EventLevel.Critical, EventKeywords.All,
new Dictionary<string, string>() { ["EventCounterIntervalSec" = "1.0" });
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
// イベント発生時の処理
if(eventData.EventSource.Name == "[EventSourceの名前]" && eventData.EventName == "EventCounters")
{
// カウンターで発行された値の取り出し
if(eventData.Payload.Count == 1 && eventData.Payload[0] is IDictionary<string, object> payload)
{
foreach(var (key, value) in payload)
{
// 表示等
}
}
}
}
}
イベント発生時に来るデータ
上記サンプルコードのOnEventWritten
内で、来たデータの処理を行うわけだが、
この時に来るeventDataの中身を記述する。
名前 | 中身 |
---|---|
EventName | 固定で"EventCounters" |
Keywords | EventKeywords.None |
Level | EventLevel.LogAlways |
OpCode | EventOpCode.Info |
Tags | EventTags.None |
PayloadNames | "Payload"のみ |
Payload |
IDictionary<string, object> となるインスタンスが一つ |
Payloadに入っているデータは、カウンターの型に応じて前述のCounterPayloadか、IncrementingCounterPayloadの値が詰め込まれたEventPayloadが入る。
dotnet-countersによる計測
dotnet-countersというツールがある。
これは、dotnetのプロセスにEventPipeでアタッチして、指定したカウンターの値を定期的に取得するプログラムとなる。
公式サイトから単体EXEをDLすることも可能だが、dotnetグローバルツールでもあるので、dotnet tool install -g dotnet-counters
でもインストールは可能。
サブコマンドで主に使うのは"ps","monitor","collect"となる。
それぞれかいつまんで言うと、
- "ps": モニタリングできるプロセスのPIDの一覧を出力する
- "monitor": コンソール上に、指定したカウンタを指定した秒数ごとに更新して出力する
- "collect": 収集した結果をファイルに出力する
という動作をする。
詳しくは 公式ドキュメント を見てもらうとして、とりあえず使いたい場合は、
- 対象アプリを起動
-
dotnet-counters ps
で対象PIDを確認 -
dotnet collect(またはmonitor) -p [プロセスID] --counters [カウンターに紐づけたEventSourceの名前]
- カウンタを指定しないと"System.Runtime"が採用される
とすれば、収集を開始してくれる。
注意点
詳細は不明だが、EventListenerでインプロセス監視をしている所に、後でdotnet-countersで監視をして、dotnet-countersを停止したところ、EventListenerの方の監視が止まってしまうという現象が発生した。
機序が特定できてないので詳細は書かないが、複数の監視ルートがある時は注意が必要かもしれない。
終わりに
EventCounterというのは多くの場合はEventSourceの通常のイベントで事足りる場合が多いが、自動的に値の集計や平均を出してくれるのはありがたい場面もあるだろう。
また、Metrics APIの登場により今後相対的に影が薄くなりそうだが、外部からの情報取得がツールでサポートされているというのは利点として大いにあるので、今後もなくなることはないと思われる。
まだ調べきれてない所がちらほらあるので、何かあれば随時書き足していきたい。