OTEP-4931: コンテキストスコープの属性

原著者: Christian Neumüller

動機

このOTEPは、過去に提起されてきたさまざまな関連要求に対応することを目的としています。 そこでは、リソース属性のスコープ(および有効期間)が広すぎる一方で、スパン属性のスコープは狭すぎるという問題があります。 たとえば、これはOpenTelemetry SDK(したがってTracerProvider、MeterProvider、LoggerProvider)のプロセス全体にわたる初期化と、(サブ)サービスの意味的なスコープとの間に不一致がある場合に発生します。

典型的なユースケースはマルチテナンシーのサポートで、テナント情報がリクエスト自体(たとえば、ヘッダーのキー、アクセストークン、リクエストパラメーター)に存在する場合です。 このテナント情報は、コンテキストスコープの属性を介してプロセス内で伝播できます。 リクエストの処理中に生成されるあらゆるテレメトリーが、対応するリクエスト・テナントに自動的に関連付けられるようになります。 他の代替策には、それぞれ問題があります。

  • テナントごとにSDKインスタンスを持つこと(テナント固有の情報を報告するため)は、数百から数千の異なるテナントが存在する可能性があるため、法外なコストになり得ます。 また、処理・エクスポートパイプラインがN倍に重複してしまいます。
  • テナント固有の情報を付加するカスタムプロセッサーは、スパンとログに対しては機能します。 しかし、このOTEPの執筆時点では、メトリクス向けのプロセッサー機能は仕様化されていません。
  • 一般的に、OpenTelemetryは、ユーザーに繰り返しカスタムコンポーネントを書かせるのではなく、広く使われる共通機能を標準で提供すべきです。

関連するユースケースは、Issue open-telemetry/opentelemetry-specification#335「HTTPアプリケーションを区別するリソース(セマンティック規約)の追加を検討する(http.app属性)」で提起されています。 opentelemetry-javaエージェントが、tomcatのような従来型のJava Application Serverを実行しているJVMに注入されている場合、そのJVM全体に対して単一のリソースが存在することになります。 Application Serverは、複数の独立したアプリケーションをホストすることができます。 現状では、http.routeurl.templateのような汎用のHTTP属性以外に、これらを区別する方法はありません。 論理的には、以下の図に示すように、app属性はApp Serverを通過する際、そのトレース内のすべてのスパンに適用されるべきです。

2つのトレースが単一のサービスを通過する様子を示す図。各トレースがそのサービスを通過する範囲内のすべてのスパンにhttp.app属性が適用されている。

この例では、frontendサービスから発生する1つと、user-verification-batchjobから発生するもう1つの、2つの「HTTP GET」ルートスパンを持つ2つのトレースが示されています。 これらのHTTP GETスパンはそれぞれ、3つ目のサービスであるmy-teams-tomcatを呼び出します。 そのサービスは2つの独立したHTTPサブサービスをホストしており、各トレースがそこを通過する際、すべてのスパンにはそれぞれ対応するhttp.appが関連付けられています。 my-teams-tomcatサービスが別のサービスauthserviceを呼び出す際、その属性はリモートの子スパンには適用されません

Azure Functionsにおけるfaas.namecloud.resource_idFunction as a Serviceのリソースセマンティック規約)についても、同様の問題が発生します。 どちらもリソース属性として定義されていますが、Azureのfunction appは、異なる名前とIDを持つ複数の共同デプロイされた関数をホストします。 PR open-telemetry/opentelemetry-specification#2502では、これらのリソース属性をFaaSのルートスパンに設定できるようにすることでこの問題を解決しました。 これには、関数(アプリ)内で発生する可能性のある子スパンにおいて、どの関数の中でスパンが実行されたのかというコンテキストが失われるという欠点があります。

説明

コンテキストスコープの属性を使うと、コンテキスト内で発行されるすべてのテレメトリーシグナルに属性をアタッチできます。 コンテキストスコープの属性は標準的な属性であり、つまり他のテレメトリー項目と同じように、文字列、整数、浮動小数点数、真偽値、配列、または複合型を使用できるということです。

コンテキストスコープの属性は、その機能が有効化されているシグナルのテレメトリー項目に対して、コンテキストスコープの属性を含むコンテキストがアクティブな状態でその項目が発行される場合、または、そのコンテキストがそれらの親として明示的に設定されている場合には、MUST で追加されなければなりません。

コンテキストスコープの属性は、それが適用される各テレメトリー項目に対して属性を直接追加するのと等価だと考えるべきです。 これらの属性は、パイプラインの下流にあるあらゆるコンポーネント、たとえばサンプラーやプロセッサーからも自動的に利用可能になります。

コンテキストスコープの属性は、サービスをまたいで伝播されては MUST NOT なりません。 つまり、これらのためにコンテキストプロパゲーターを実装してはなりません。 これは、これらの属性が単一のサービスに関連するテレメトリー項目の(サブセットに)注釈を付けることを意図しているためです(次のセクションも参照してください)。

SDKは、コンテキストスコープの属性をシグナルごとのオプトイン機能として提供 MUST しなければなりません。 これは、予期しない副作用や、追加のメモリ確保によるパフォーマンスへの影響の可能性を減らすためです。 ユーザーは、この機能を明示的に有効にすること MUST が求められます。

tracer_provider:
  context_scoped_attributes: true
metrics_provider:
  context_scoped_attributes: true
logger_provider:
  context_scoped_attributes: true

コンテキストスコープの属性を設定したい計装ライブラリは、これをオプトインの動作として提供すべき(SHOULD)です(つまり、ライブラリはデフォルトではコンテキストスコープの属性を設定しません)。 使用される属性についての包括的なドキュメントが、エンドユーザーに提供されるべき(SHOULD)です。 これには、これらの属性が高カーディナリティの値を持つか低カーディナリティの値を持つかについての詳細が含まれるべき(SHOULD)です。

実装に関するさらなる詳細については、未解決の問題を参照してください。

コンテキストスコープの属性とBaggageの比較

コンテキストスコープの属性とBaggageには、2つの大きな共通点があります。

  • どちらも「属性の袋」であること
  • どちらもプロセス内では同じように伝播すること(Baggageもコンテキスト上に存在します)。

しかし、ユースケースは大きく異なります。 Baggageは実行パスに沿ってアプリレベルのデータを運ぶことを意図している一方、コンテキストスコープの属性は、同じ実行パス内のテレメトリーに対する注釈です。

これは、機能面とAPI面の違いも説明します。

  • もっとも重要な点として、Baggageはサービスをまたいで伝播することを意図しています。 実際、これがBaggageの主要なユースケースです。 コンテキストスコープの属性用のプロパゲーターを実装すること自体は可能ですが、それは属性のスコープを呼び出し先のサービスにまで拡張することで意図した意味を壊してしまい、多くのセキュリティ上の懸念も引き起こすでしょう。
  • Baggageは文字列の値のみをサポートします。

コンテキストスコープの属性とInstrumentation Scope属性の比較

コンテキストスコープの属性とInstrumentation Scope属性には、次の共通点があります。

  • どちらも名前に「スコープ」が含まれること。
  • どちらも一連のテレメトリー属性を、一連のテレメトリー項目(それぞれの「スコープ」に含まれるもの)に適用すること。

Instrumentation Scope属性が同じTracer、Meter、LogEmitter(つまり、典型的には同じテレメトリー実装単位のコード)によって発行されるすべての項目に適用されるのに対し、コンテキストスコープの属性のスコープは、使用されているTracer、Meter、LogEmitterとは独立に、実行時のコンテキストによって決まります。

さらに、Tracer、Meter、Loggerのインスタンスは通常、アプリケーション自体と同じ寿命を持ちますが、コンテキストスコープの属性の寿命はコンテキスト自体に紐づいています。 たとえば、単一の受信リクエストのために作成されたコンテキストは、そのリクエストが完了すると存在しなくなります。

実際には、Instrumentation ScopeとコンテキストスコープはしばしばまったくOrthogonal(直交)な関係になります。 コンテキストが単一のサービス内をプロセス内で流れる場合、多くの場合、Tracerごとにちょうど1つのスパンが存在することになります(たとえば、リクエストが到着した際のHTTPサーバー計装によるスパンが1つ、そして下流のサービスが呼び出される際のHTTPクライアント計装によるスパンがもう1つ)。

内部の詳細

このセクションでは、これをサポートするために必要となるAPIおよびSDKレベルの変更について説明します。

API変更

APIは、コンテキストスコープの属性を取得・設定するための2つの操作で拡張されます。

  • コンテキスト属性の追加:たとえば、Context AddContextScopedAttributes(Context context, Attributes attributes)Context.addAttributes()
  • コンテキスト属性の取得:たとえば、Attributes GetContextScopedAttributes(Context context)Context.getAttributes()

Add Context Attributesは、指定されたコンテキストに属性の集合を追加します。 これらの属性は、以前に設定されたコンテキストスコープの属性に追加され、同名のエントリは上書きされます。 更新後の値の集合を含む新しいContextオブジェクトが返されます。

Get Context Attributesは、コンテキストスコープの属性を返します。存在しない場合は空の集合を返します。

SDK変更

SpanおよびLogRecordインスタンスの作成時、SDKは論理的に関連付けられたコンテキストのコンテキストスコープの属性を取得し MUST しなければならず、それらを新しく作成されたテレメトリー項目に追加します。 その際、テレメトリー項目に既に存在するキーを持つ属性はスキップします(コンテキストスコープの属性の優先度はより低いためです)。 これは、SamplerやProcessorなど、いかなる拡張ポイントが呼び出される前にも行われなければ MUST なりません。 Spanインスタンスについては、これらの属性は後からユーザーによって手動で上書きされ得ます。

同期的に報告されるメトリクスについては、SDKは論理的に関連付けられたコンテキストのコンテキストスコープの属性を取得し MUST しなければならず、実際に使用されている属性セットにアクセスできるようにするために、(Instrument作成時ではなく)計測値が記録される際にそれらをアタッチします。 これは、views、advisories、カーディナリティ制限など、下流のいかなる処理が行われる前にも実施されなければ MUST なりません。 Metrics SDKの計測値処理のセクションは、このステップの詳細を明確に反映するように更新される予定です。

非同期に報告されるメトリクスについては、コンテキストスコープの属性のサポートは当初は定義されません。 今後の反復でこれのサポートが追加される可能性はありますが、これは本OTEPのスコープ外です。

これは、SDKのAPI表面に変更を加えることのない実装レベルの変更となります(つまり、コンテキストスコープの属性を「直接の」テレメトリー属性と区別可能にする必要はありません)。

以下のコードスニペットは、サンプリング層との相互作用を含め、スパンについて期待される動作がどのようなものかを示しています。

public Span startSpan(...) {
  // Implicit (currently active) or explicit parent.
  Context context = getParentContext();

  // Attributes specified by the user upon creation time.
  Attributes initialAttributes = getInitialAttributes();

  // Merge the Context attributes, which have lesser priority
  // than the attributes explicitly specified at telemetry item
  // creation.
  getContextScopedAttributes(context).forEach((key, value) -> {
    if (!initialAttributes.containsKey(key)) {
      initialAttributes.put(key, value);
    }
  });

  // Pass the updated attributes to the sampling layer.
  SamplingDecision samplingDecision = sampler.ShouldSample(
      context,
      initialAttributes, ...);

  return new Span(initialAttributes, ...);
}

トレードオフと緩和策

  • コンテキストスコープの属性の使用は、追加のメモリ確保やメトリクスのカーディナリティの爆発といった、予期しない副作用をもたらす可能性があります。 このため、この機能はシグナルごとにユーザーが明示的にオプトインしなければなりません。
  • 計装ライブラリがコンテキストスコープの属性を設定しないことを推奨する予定ですが、この機能を活用することで恩恵を受けられる非常に特定のライブラリも存在するかもしれません。 本OTEPが仕様に組み込まれる際にそれについて議論する予定です。 未解決の問題を参照してください。

先行技術と代替技術

AddBeforeEndコールバックとAddFieldToTrace

(わずかに関連する)Issue open-telemetry/opentelemetry-specification#1089「スパンがまだ書き込み可能な状態のコールバックを持つためのBeforeEndの追加」があります。 これは本質的に同じ問題に動機づけられているようですが、その動機はより技術的なレベルで述べられています。 「私が実装しようとしていたのは、Honeycombのtrace fieldと呼ばれる機能です(参照: AddFieldToTrace)」。 前述の機能のドキュメントには、「AddFieldToTraceは、現在アクティブなスパンと、このプロセス内で発生するこのトレースに関与する他のすべてのスパンの両方にフィールドを追加します」と記載されています。

代替案:バックエンド上でのトレース向け実装

トレースデータ(そしてトレースデータのみです。他のシグナルはありません)の受信側は、スパンの親子関係を通じて実行フローに関する情報を持っています。 それを使って、受信後に親スパンから子スパンへ属性を伝播・コピーすることができます。 しかし、これで何が可能かについて、いくつかの問題・違いがあります。

  • バックエンドは、どの属性が特定のスパンにのみ適用され、どの属性がサブトレース全体に適用されるのかを判断する必要があります。 計装ライブラリがこの情報を追加できる専用のAPIが存在しない場合、そのために事前に決められたルール(たとえば固定の属性キーのリスト)しか使えません。
  • スパンは(通常のSpanProcessor+Exporterパイプラインによって)終了時にのみ送信されるため、通常は子スパンより親スパンの方が後に到着することになります。 これは処理を複雑にし、追加のバッファリングや再処理(たとえば読み取り時の反復処理)が必要になる可能性があります。
  • サービス内でのみ属性を伝播するには、open-telemetry/oteps#182(親スパンのコンテキストのIsRemoteプロパティをOTLPプロトコルで送信すること)が必要になります。 これには、OTLP、あるいはこの情報を伝送する他のプロトコル(存在するでしょうか)を使用する必要もあります。 また、関係するサービスで使われているOTLPエクスポーターと言語SDKが、実際にこの機能を実装している必要もあります。 それがない場合、スパンの種類(client/producerの子であるserver/consumer)だけが、これを回避するための唯一のヒューリスティックになります。

さらに、コンテキストスコープの属性には、メトリクスとログにも使用できるという利点があります。 バックエンドは、すべてのシグナルに対して単一のバックエンドである場合、マッチしたスパンから属性を再統合しようと代替的に試みることもできますが、それはおそらく実装がさらに高コストになります。

代替案:Baggageの上にコンテキストスコープの属性を実装する

あわせて参照:

専用のコンテキストキーとAPIを使う代わりに、コンテキストスコープの属性が解決しようとしているユースケースをサポートするために、Baggage APIと実装を拡張することもできます。 これには以下が必要になります。

  • Baggageのエントリが「本物のBaggage」であるべきか、テレメトリー属性であるべきかを決定するためのメタデータをBaggageのエントリに持たせること。 これにより、Baggageのエントリがテレメトリー項目に追加されるかどうか、また伝播され(ない)かどうかが決まります。 これらは別々のフラグにすることができ、より多くのユースケースを実現できる可能性があります。
  • Baggageのエントリを、値として文字列ではなくAttributeを保持するように変更すること。
  • Baggageとテレメトリー項目の間の関連付けを定義・実装すること(この点は、提案されたOTEPで必要とされることとほぼ同じになります。たとえば、Baggageを列挙し属性をフィルタリングするSpanProcessorを追加することができます)。

コンテキストスコープの属性を実装する手段としてBaggageを使うことは妥当なアプローチのように思われ、その判断は各言語に委ねることができます。 これが賢明かどうかは、たとえば、Baggageが将来的に得るかもしれない機能(たとえば、OpenCensusとの相互運用のために伝播を停止するフラグが有用かもしれません)や、新しいAPIを実装するのと既存のAPIを拡張するのとで、その言語でどれだけの定型的なコードが必要になるかに左右されるかもしれません。

代替案:組み込みのProcessorを使ってコンテキストスコープの属性を実装する

コンテキストスコープの属性でテレメトリー項目にスタンプを押すために、SDK組み込みのProcessorを追加することもできます。 これは、Contextからコンテキストスコープの属性を取得するための内部キーにアクセスするために、特に組み込みである必要があります。 このアプローチにはいくつかの問題があります。

  • SamplerのShouldSample操作は、ProcessorがOnStartを呼び出されるに呼び出されます。 そのため、ProcessorインターフェースにBeforeShouldSampleBeforeStartのような操作を追加する必要がありますが、これは冗長に思われます。
  • Processorはシグナルごとに設定される必要があり、これは冗長である一方で、より高い粒度を提供できる可能性もあります。

未解決の問題

  • Baggageはこの新しいプロセスの一部に含められるべきでしょうか。 個々のテレメトリー項目にBaggageの値をアタッチする必要性は、過去にユーザーから要望として出されてきました。 これは、検討された代替案の1つの一部としても考慮されています。
  • 設定は、少なくとも初期段階では、SDK全体がdisabled設定オプションによって有効・無効化される方法と同様に、シグナルごとに単一の真偽値設定として定義することができます。 たとえば、トレースとログではこれを有効にするがメトリクスでは無効にする、といったようにです。 ただし、本OTEPでは、さらなるオプション(たとえば、シグナルごとに特定の属性のallow listやdeny list)が必要になった場合に備えて、このトピックを未解決のままにしています。
  • プロファイリングのサポートは(最終的には)追加されるべきですが、これはprofiling SIGと議論・確認する必要があります。 必要であれば、このサポートはデフォルトで無効にすることもできます。
  • 計装ライブラリは一般にこの機能に依存すべきではありませんが、もし依存する場合には、それをオプトインとして公開すべきです。 ただし、これをデフォルトで有効にすることが非常に大きな価値を生む場合もあります。 そのような計装がこの機能を使用するためにどのような条件を満たすべきかについて、議論が必要です。

将来の可能性

本OTEPで実装される変更により、いくつかの長年未解決だった仕様のIssueが前進することを期待しています。 動機を参照してください。

  • 非同期に報告されるメトリクスは、この機能をサポートしません。 サポートは必要に応じて後から追加できます。