ActivitySourceについての概要


はじめに

以前DiagnosticSourceについての解説記事を書いた。
この記事の中で、Activityに軽く触れているが、今回net5.0よりActivitySourceが登場し、
OpenTelemetryのC#実装もこちらを積極的に使うようにしたらしいので、使い方の記事を書く。

なお、DiagnosticSourceとは兄弟のようなものだが、基本的に継承、所有関係等は無く、互いに独立したクラスであることに注意してほしい。

Activityについて

Activityについては、アクティビティユーザーガイドという公式文書はあるが、日本語が無いのと、DiagnosticSourceでの使い方しか書いてないので、ここでも解説する。
また、多くの概念をW3C Trace Contextから参照しているので、コメントを見てもよくわからない部分があったらそちらを参照するのがいい。

何のためのものか

HTTPリクエストの処理等、ある区間の処理に要した時間を計測するために使う。
デバッグログ等にstopwatch等で計測した時間を記述するというのも不可能ではないが、
平均、分散等の統計的な値を見たい場合には、それに特化した形でデータを蓄積していく方が利便性はある。

Activity.Current

Activityが開始されると、Activity.Currentに、開始されたActivityが設定される。
ただし、これは AsyncLocalなものとなる ので、全く異なるasyncコンテクストには影響しない。

Activityが持つ要素

詳しくは Activityユーザーガイドにも記述はあるが、主に使用するものについて書く。

ID

個別のActivityを区別するためのもの。 後述するリンクや親子関係の参照にも使うため、必ずユニークなIDにする。通常はActivitySource.StartActivityした時点で自動的にセットされるので意識はしなくてもいい。
フォーマットについて、net5.0からはデフォルトでW3C Trace ContextのTrace ID、それより前のtfmでは、Hierarchical IDが使われる。Flat Request-Idというのもあったが、既に非推奨となっている。

また、SpanIdTraceIdなるものもあるが、これはW3C Trace IDフォーマットでいうと、SpanId=parent-id、TraceId=trace-idとなる。

Recorded

Activityがサンプリングされた結果、このActivityが選別されたことを示す。
例えば、Recordedフラグが立っている場合だけ特別な情報を追加するという処理が考えられる。
なお、このフラグは W3C Trace IDのsampled flag に対応する。

IsAllDataRequested

Activityに関する追加データも要求されていることを示す。

TraceStateString

W3Cのtracestateより来ているフィールドで、任意の値を入れておける(大体はx=yのキーバリュー形式)。 子に伝搬するので注意

名前(OperationName)

Activityに付けられる名前。主に表示やグループ分け用に使用する。基本的に制約はないが、頻繁に使用されることが予想されるため、短く簡潔な方が望ましい。

親Activity(Parent)

Activityは親子関係を持たせることが可能で、これにより、どこの処理に時間がかかっているかを正確に把握ができる。

開始時刻(StartTime)

開始時間。明示的に指定することも可能だが、大抵は生成時の時刻

経過時間(Duration)

開始からどの程度時間が経過したか。

Baggage

Activityに対して設定できるメタ情報。
親で設定したBaggageは 子にも受け継がれるため、なるべく小さくしておくのが吉

Tags

Activityに対して設定できるメタ情報その2。
Baggageと異なるのは、 子には伝搬しない という点。

リンク(Links)

親子関係とは別に、関連付けたい外部Activityがあれば設定する。

アクティビティ種別(Kind)

アクティビティのおおまかな分類を設定する。リスナー側に対する目印のようなもので、それ自体に何か特別な効果は無い

プロジェクトへの導入

以下の方法で導入すれば、System.Diagnostics.ActivitySourceや各種関連クラスを使用できる。

net5.0の場合

何もしなくても使える。

netstandard2.1またはnetcoreapp3.1以前の場合

nugetで、5.0.x以降のパッケージを導入すれば使える
4.xにはActivitySourceは存在しないので注意。

ActivitySourceの発行元の作成

まず、イベントを発生するActivitySourceの使い方を書く。

手順としては大雑把に言うと、

  1. ActivitySourceの作成
  2. ActivitySourceからActivityを開始
  3. 属性、イベントを追加
  4. ActivityのDisposeで購読側にActivityの終了を通知

となる。

ActivitySourceの作成

System.Diagnostics.ActivitySourcenew ActivitySource(name, version)で作成する。
この時の注意点としては、

  • nameversionは、後述するイベント受信側で処理を選別するために使用される
  • nameの命名規則はDiagnosticSourceのものに準じるのが吉。
    • 厳密に従わなければならないということはないが、とにかく名前がユニークであることは必須
  • インスタンスを大量作成する用途は想定されていないため、大抵の場合、static readonlyでシングルトンとして作成する
    • newした時点でグローバルなリストに追加されるため

Activityの開始

生成したActivitySourceから、Activity ActivitySource.StartActivity(name, ActivityKind)を実行する。
この時の注意点としては、

  • この時点でActivityが生成される
  • 購読するリスナーがいない場合、またはActivitySource購読の所で後述するサンプリング処理で処理しないと判断した場合、返されるActivityはnullになるので注意
  • ActivityKindは、主にイベントリスナー側の情報の選別のために使用されるもので、発信側には特に影響は無い

また、StartActivityの基本は上記だが、明示的に以下の親Activityを指定するには、

  • Activity ActivitySource.StartActivity(string name, ActivityKind kind, string parentId)
  • Activity ActivitySource.StartActivity(string name, ActivityKind kind, ActivityContext ctx)

のどちらかを使用する。明示的に指定しない場合は、Activity.Currentが使われる。

追加の引数として以下のようなものを設定できる

名前 概要
tags IEnumerable<string, object> Activityに紐づけられる追加属性(後で追加可能)
links IEnumerable<ActivityLink> 関連するActivityを設定する。後で追加は不可
startTime DateTimeOffset 開始時間を設定する。通常は現在時刻

また、StartActivityと同時に、Activity.Currentが生成されたインスタンスに設定される。

Activityへの属性、イベントの追加

生成したActivityに対して、Activity.AddBaggage(string key, string value)や、Activity.AddTags(string key, string value)で追加属性を指定できる。
また、Activity.AddEvent(ActivityEvent ev)で、イベントも追加可能。
ここで出てきたActivityEventだが、以下のメンバーをnewの時に設定する

名前 概要
name string イベント名、必須ではないが、設定はした方が良い
timestamp DateTimeOffset イベント発生時刻。デフォルトは現在時刻
tags ActivityTagsCollection イベント追加属性

Activityの終了

ActivityをDisposeすることで、リスナーにActivity終了のイベントを通知する

ActivitySourceの購読

ActivitySourceのイベントを購読するのに使用するのはSystem.Diagnostics.ActivityListenerとなる。
大雑把に使い方を説明すると、

  1. インスタンスをnewする
  2. 各種デリゲートを設定する
  3. ActivitySource.AddActivityListener(ActivityListener)で購読を開始する

となる。

各種デリゲートについて

デリゲートは以下のようなものがある。

  • Action<Activity> ActivityStarted
  • Action<Activity> ActivityStopped
  • Func<ActivitySource, bool> ShouldListenTo
  • SampleActivity<ActivityContext> Sample
  • SampleActivity<string> SampleUsingParentId

SampleActivity<ActivityContext>とは、ActivitySamplingResult F(ref ActivityCreationOptions<ActivityContext> options)のこと。

以下、それぞれについて解説する

ActivityStarted

アクティビティ開始時に呼ばれる。

ActivityStopped

アクティビティ停止時(Activity.Dispose)に呼ばれる

ShouldListenTo

引数で渡ってきたActivitySourceの監視を行うかを決定する。
呼ばれるのはActivitySource.AddActivityListener直後とActivitySource追加時だけ。

Sample

ActivitySource.StartActivity(string,ActivityKind)またはActivitySource.StartActivity(string,ActivityKind,ActivityContext)を指定した時に呼ばれる。

Activity生成前に呼び出され、実際にActivityを生成するかどうかを決定する。

引数で渡ってくるActivityCreationOptions<T>はTraceIDの他、ActivitySource.StartActivityで指定した親Activityに関する情報がT ActivityCreationOptions<T>.Parentとして入っている。

戻り値としてActivitySamplingResultを返さなければならないが、各値の意味は以下のようになる。

意味 実際の影響
None Activityを開始しない StartActivityで返ってくる値がnullになる
PropagationData Activityの開始を行うが、追加データは要求しない Activityは生成され、RecordedとIsAllDataRequestedがfalseになる
AllData Activityの開始を行い、かつ追加データを要求する Activityは生成され、IsAllDataRequestedがtrueになり、Recordedがfalseになる
AllDataAndRecorded Activityの監視を行い、かつW3C Trace Contextでいう所のsamplingフラグを立てる Activityは生成され、IsAllDataRequestedとRecorded両方がtrueとなる

全てにNone以外のフラグを返しても構わないが、高負荷が見込まれる場合は、一定数Noneを返して負荷軽減を図ることができるようになっている。

ただし、頻繁に呼ばれる可能性がある箇所なので、あまり重い処理は行わないように注意すること。

SampleUsingParentId

こちらはActivitySource.StartActivity(string,ActivityKind,string)を指定した時に呼ばれる。
他はSampleと同じ。

サンプルソース

サンプルソースをgistに作成した。

終わりに

DiagnosticSourceからActivityを使うのは正直しんどい所もあったので、ActivitySourceが登場したのは良かったと思う。
ActivityListenerに関しては、実際は直接使用することなく、OpenTelemetry等のフレームワーク越しに使う事が多くなるかもしれない。

また、今回は使い方の説明だったが、そのうちActivityの操作自体にかかる処理時間についても考察したいと思う。

また、TFMの違いで生成されるIDフォーマットに違いがあるのは罠だなと思った。