> Source: https://www.ymotongpoo.com/works/oteps/trace/otep-0250/


# OTEP-0250: コンポジットサンプラーの提案

本提案は、[OpenTelemetry SDK](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling)で説明されているヘッドベースサンプリングを扱います。
本提案では、追加の _コンポジットサンプラー_ を導入します。
コンポジットサンプラーは、サンプリングの決定を行うために他のサンプラー（_デリゲート_ または _子_）を使用します。
コンポジットサンプラーはデリゲートサンプラーを呼び出しますが、最終的な決定は自身で行います。

ここで提案する新しいサンプラーは、Consistent Probability Samplers（一貫性のある確率サンプラー）と連携するように設計されています。
この概念の詳細な説明については、[probability sampling（OTEP 235）](https://github.com/open-telemetry/oteps/blob/main/text/trace/0235-sampling-threshold-in-trace-state.md)を参照してください。
また、ドラフトPR 3910の[W3C Trace Context Level 2に基づく確率サンプラー](https://github.com/open-telemetry/opentelemetry-specification/pull/3910)も参照してください。

文書の履歴:

- **改訂** [仕様のリリース1.48.0は、本OTEPへの軽微な更新が行われる前に公開されました。この更新では、サンプリングの決定を行う際に「信頼できない」サンプリング閾値が使用された場合、独立性を保つために新しい独立したランダム性の値を生成し、使用したうえで破棄すべきであると規定しています。](https://github.com/open-telemetry/opentelemetry-specification/pull/4569)

**目次:**

- [動機](#motivation)
- [ゴール](#the-goal)
  - [例](#example)
- [提案するサンプラー](#proposed-samplers)
  - [新しいAPI](#new-api)
    - [GetSamplingIntent](#getsamplingintent)
    - [GetSamplingIntentの必須引数](#required-arguments-for-getsamplingintent)
    - [戻り値](#return-value)
    - [基本サンプラーの要件](#requirements-for-the-basic-samplers)
    - [SamplingResultの構築](#constructing-samplingresult)
  - [ConsistentRuleBased](#consistentrulebased)
    - [Predicate](#predicate)
      - [SpanMatches](#spanmatches)
      - [Predicateの必須引数](#required-arguments-for-predicates)
    - [ConsistentRuleBasedの必須引数](#required-arguments-for-consistentrulebased)
  - [ConsistentParentBased](#consistentparentbased)
  - [ConsistentAnyOf](#consistentanyof)
  - [ConsistentRateLimiting](#consistentratelimiting)
    - [ConsistentRateLimitingの必須引数](#required-arguments-for-consistentratelimiting)
- [まとめ](#summary)
  - [例 - サンプリング設定](#example---sampling-configuration)
  - [制限事項](#limitations)
  - [プロトタイピング](#prototyping)
- [先行技術](#prior-art)

## Motivation {#motivation}

ヘッドサンプリングの設定に対するニーズは、[Sampling SIG](https://docs.google.com/document/d/1gASMhmxNt9qCa8czEMheGlUW2xpORiYoD7dBD7aNtbQ/edit)内でも、より広いコミュニティにおいても、明示的または暗黙的にたびたび示されてきました。
議論の一部は何年も前にさかのぼります。
以下に例を示します。

- issue [173](https://github.com/open-telemetry/opentelemetry-specification/issues/173): すべての言語で自動計装のTracerを使う際にヘルスチェックのトレースを無視する方法はあるか？
- issue [1060](https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/1060): URLをトレーシングから除外する
- issue [1844](https://github.com/open-telemetry/opentelemetry-specification/issues/1844): コンポジットサンプラー

残念ながら、Sampling SIGのミーティングで交わされた価値ある意見の中には、発案された当時に記録されなかったものもありますが、いくつかの例については[Sampling SIG Research Notes](https://github.com/open-telemetry/oteps/pull/213)や[OTEP 240: A Sampling Configuration proposal](https://github.com/open-telemetry/oteps/pull/240)配下のコメントを参照してください。

## The Goal {#the-goal}

本提案の目標は、事前定義済みのビルディングブロックを使って高度なサンプリング設定を構築できるようにすることです。
次のようなサンプリング要件の例を考えてみましょう。
多くのユーザーが同様のパターンに従う要件を持っていると考えられます。
ここで特に注目すべき要素は、ターゲットURLに基づくトレースの分類、特別な扱いを必要とする一部のスパン、そしてエクスポートされるスパンの総量に対する妥当な上限の設定です。

これはサンプラー仕様の新しい部分であるため、OpenTelemetry SDKにとってはオプションのコンポーネントとなることが想定されています。
ただし、SDKが _opts-in_（オプトイン）する場合、本書で説明するすべてのサンプラーを実装すべきです（SHOULD）。

### Example {#example}

ヘッドベースサンプリングの要件:

- ルートスパンの場合:
  - `/healthcheck` へのすべてのリクエストをドロップする
  - `/checkout` へのすべてのリクエストをキャプチャする
  - それ以外のすべてのリクエストの25%をキャプチャする
- ルート以外のスパンの場合
  - 親のサンプリング決定に従う
- ただし、サービス `/foo` へのすべての呼び出しをキャプチャする（トレースが不完全になる場合でも）
- いずれの場合でも、1000スパン/分を超えない

_注_: 提案するサンプラーのいくつかは、Attribute（属性）セットの _和集合 (union)_ を計算することを求めます。
このような和集合が構築される際、属性キーが競合する場合には、そのキーを使用する最後のセットにおける属性定義が有効になります。
同様に、`Tracestate` の変更が連続して行われる場合、キーが競合する際には最後の変更が以前の値を上書きします。

## Proposed Samplers {#proposed-samplers}

新しいサンプラーすべてに共通する動作原理は、`ShouldSample` がコンポジットサンプラーによって形成される木構造のルートに対してのみ、一度だけ呼び出されるという点です。
サンプラーの合成によって提供されるすべてのロジックは、`GetSamplingIntent` を通じて閾値を計算することによって処理され、必要に応じてその計算は下流に委譲されます。

### New API {#new-api}

このアプローチを可能にするために、サンプラーの合成に参加するすべてのConsistent Probability Sampler（一貫性のある確率サンプラー）は、標準のSampler APIに加えて、以下のAPIを実装する必要があります。
本書では、この新しいAPIを提供し、ここで説明する規則に準拠する一貫性のある確率サンプラーを指して、_Composable（合成可能）_ サンプラーという用語を使用します。
以下で説明するすべてのサンプラーは _Composable_ サンプラーです。

#### GetSamplingIntent {#getsamplingintent}

これは、すべての `Composable` サンプラーに共通する操作です。
その目的は、あるスパンに対してサンプリングの決定を行うよう求められた場合にサンプラーが実行するであろう動作について、実際のサンプリングDecision（決定）を構築することなく問い合わせることです。

#### Required Arguments for GetSamplingIntent {#required-arguments-for-getsamplingintent}

引数は、`TraceId` を除いて[`ShouldSample`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#shouldsample)と同じです。

- 親 `Span` を含む `Context`
- 作成される `Span` の名前
- 作成される `Span` の `SpanKind`
- 作成される `Span` の初期の `Attributes` の集合
- 作成される `Span` に関連付けられるリンクのコレクション

#### Return value {#return-value}

戻り値は、以下の要素を持つ構造体（`SamplingIntent`）です。

- 14文字の16進数文字列として表現されるTHRESHOLD値。値が `null` の場合は非確率的な `DROP` の決定を表す（実装は、より高性能または便利であると思われる場合、異なる表現方法を使用してもよい（MAY））
- 調整カウント（サンプリング確率の逆数として計算される）がスパンのメトリクスを推定するために忠実に使用できることを示す `boolean` 値を提供する関数（`IsAdjustedCountReliable`）
- 最終的なサンプリングの決定が肯定的である場合に `Span` に追加される `Attributes` の集合を提供する関数（`GetAttributes`）
- 入力の `Tracestate` とサンプリングのDecisionが与えられたときに、`Span` に関連付けられる `Tracestate` を提供する関数（`UpdateTraceState`）。これらの関数の内部で、サンプラーは `ot` キーに対する `th` 値を追加または変更すべきではありません（SHOULD NOT）。コンポジットサンプラーの木構造のルートノードのみが、この値を設定またはクリアする責任を負います（下記の `SamplingResult` の構築を参照）

`IsAdjustedCountReliable` が `false` を返すことは、THRESHOLD値をサンプリングの決定に使用できる場合であっても、その決定が[TraceState: Probability Sampling](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/tracestate-probability-sampling.md#tracestate-probability-sampling)で概説されているConsistent Probability Samplingの原則に適合しない可能性があり、また関数名が示すとおり、収集されたスパンに基づいて（呼び出し回数などの）メトリクスを推定することができないことを示します。
たとえば、THRESHOLD値を導出する過程で使用されるサンプリング確率は、トレースのランダム性の値、親の `sampled` フラグ、またはその他の非確率的な要因に直接的または間接的に依存している場合があります。
その例については[ConsistentParentBased](#consistentparentbased)を参照してください。

#### Requirements for the basic samplers {#requirements-for-the-basic-samplers}

`ConsistentAlwaysOff` サンプラーは、以下を満たす `SamplingIntent` を提供しなければなりません（MUST）。

- THRESHOLD値が `null`（またはそれに相当する値）である
- `IsAdjustedCountReliable` が `false` を返す
- `GetAttributes` が空集合を返す
- `UpdateTraceState` が、その引数を変更せずにそのまま返す

`ConsistentAlwaysOn` サンプラーは、以下を満たす `SamplingIntent` を提供しなければなりません（MUST）。

- THRESHOLD値が `00000000000000`（またはそれに相当する値）である
- `IsAdjustedCountReliable` が `true` を返す
- `GetAttributes` が空集合を返す
- `UpdateTraceState` が、その引数を変更せずにそのまま返す

`ConsistentFixedThreshold` サンプラーは、本質的には `TraceIdRatioBased` サンプラーですが `Composable` インターフェースを実装しており、以下を満たす `SamplingIntent` を提供しなければなりません（MUST）。

- 提案されている[Rejection Threshold](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/tracestate-probability-sampling.md#rejection-threshold-t)に従って計算された閾値を表すTHRESHOLD値である
- `IsAdjustedCountReliable` が `true` を返す
- `GetAttributes` が空集合を返す
- `UpdateTraceState` が、その引数を変更せずにそのまま返す

#### Constructing `SamplingResult` {#constructing-samplingresult}

コンポジットサンプラーの木構造のルートサンプラーに対する `ShouldSample` の呼び出しに応じて、最終的な `SamplingResult` を構築するプロセスは、以下のステップで構成されます。

- サンプラーは自身の `SamplingIntent` を取得する。これは（サンプラーが末端でない限り）以下で説明する再帰的なプロセスである
- 受け取ったTHRESHOLD値が `null` の場合、サンプリングの決定は `DROP` となる。そうでない場合は
- サンプラーは受け取った `IsAdjustedCountReliable` 関数を呼び出し、`true` の場合は[Randomness Value (R)](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/tracestate-probability-sampling.md#randomness-value-r)で説明されているとおりTraceStateまたはTraceIdからランダム性の値Rを導出し、`false` の場合は新しいランダムな56ビットの数値を16進数エンコードして新しいランダム性の値Rを生成する
- サンプラーは、[Decision algorithm](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/tracestate-probability-sampling.md#decision-algorithm)で説明されているとおり、受け取ったTHRESHOLD値とランダム性の値Rを（実装に応じて辞書式順序またはその他の方法で）比較し、最終的なサンプリング `Decision` を導出する
- サンプラーは、受け取った `UpdateTraceState` 関数に親の `Tracestate` と最終的なサンプリング `Decision` を渡して呼び出し、`Span` に関連付けられる新しい `Tracestate` を取得する。これもまた、多くの場合は再帰的なステップである
- サンプリングの決定が肯定的な場合:
  - サンプラーは、受け取った `GetAttributes` 関数を呼び出して `Span` に追加する `Attributes` の集合を決定する。これも多くの場合は再帰的なステップである
  - `IsAdjustedCountReliable` 関数が `true` を返した場合、受け取ったTHRESHOLDに従って `Tracestate` 内の `ot` キーに対する `th` 値を変更する。返された値が `false` であった場合は、`Tracestate` から `ot` キーに対する `th` 値を削除する
- サンプリングの決定が否定的な場合、`Tracestate` から `ot` キーに対する `th` 値を削除する

### ConsistentRuleBased {#consistentrulebased}

`ConsistentRuleBased` は、（サンプリングの決定が `Span` の属性に依存する場合などに）`Span` の分類とサンプリングを行うコンポジットサンプラーです。
Spanは個別のカテゴリーにグループ化でき、各カテゴリーは異なるSamplerを使用できます。
Spanの分類は `Predicates` によって支援されます。

#### Predicate {#predicate}

Predicateは、`Span` の `Attributes`（またはサンプリングの決定を行う際に利用可能なその他の情報）にアクセスできる論理式を表し、アクセス可能な値に対してテストを実行します。
たとえば、SERVERスパンのターゲットURLが特定のパターンに一致するかどうかをテストできます。
`Predicate` インターフェースにより、ユーザーはサンプリングの決定を行う時点で利用可能な情報に基づいて、カスタムのカテゴリーを作成できます。
一貫性のある確率サンプリングの整合性を保つため、Predicateは親の `sampled` フラグにも、`TraceId` の下位56ビット（_ランダム性_ の値を表しうる部分）にも依存してはなりません（MUST NOT）。

##### SpanMatches {#spanmatches}

これは `Predicate` に対する操作であり、与えられた `Span` が一致する場合、つまりPredicateによって記述されたカテゴリーに属する場合に `true` を返します。

##### Required Arguments for Predicates {#required-arguments-for-predicates}

引数は、`ShouldSample` に対して利用可能となる値を表します。

- 親 `Span` を含む `Context`
- 作成される `Span` の `TraceId`
- 作成される `Span` の名前
- 作成される `Span` の初期の `Attributes` の集合
- 作成される `Span` に関連付けられるリンクのコレクション

#### Required Arguments for ConsistentRuleBased {#required-arguments-for-consistentrulebased}

- オプションの `SpanKind`
- （`Predicate`、`Composable`）のペアのリスト

`SamplingIntent` を計算するために、`Span` の種類が指定された種類に一致する場合、または種類が指定されていない場合、サンプラーは与えられた順序でリストを走査し、受け取ったのと同じ引数を渡して各 `Predicate` の `SpanMatches` を呼び出します。
ある呼び出しが `true` を返した場合、結果は対応する `Composable` に対して呼び出された `GetSamplingIntent` が返す値となり、他の `Predicate` は評価されません。
`SpanKind` が一致しない場合、または `SpanMatches` の呼び出しがいずれも `true` を返さなかった場合、結果は `ConsistentAlwaysOffSampler` に対して `GetSamplingIntent` を呼び出すことで得られます。

### ConsistentParentBased {#consistentparentbased}

`ConsistentParentBased` サンプラーの機能は、標準の `ParentBased` サンプラーに対応します。
引数として1つの `Composable` サンプラーのデリゲートを取ります。
このデリゲートは、ROOTスパンに対するサンプリングの決定を行うために使用されます。

`ConsistentParentBased` の振る舞いは、親スパンのサンプリングに非確率的なサンプラーが使用されていた場合に対応します。

`GetSamplingIntent` 操作が呼び出されると、サンプラーは有効な親スパンのコンテキストが存在するかどうかを確認します。
存在しない場合、サンプラーはデリゲートに対して `GetSamplingIntent` を呼び出した結果を返さなければなりません（MUST）。

そうでない場合、サンプラーは親のトレース状態から閾値を抽出しようとします。
サンプラーは、以下のとおり `SamplingIntent` を返さなければなりません（MUST）。

親のトレース状態が有効な閾値Tを持つ場合:

- 結果のTHRESHOLD値はTです
- `IsAdjustedCountReliable` は `true` を返します

親のトレース状態に有効な閾値がない場合、サンプラーはtraceparentの `sampled` フラグを調べます。

フラグが設定されている場合:

- 結果のTHRESHOLD値は `00000000000000`（またはそれに相当する値）です
- `IsAdjustedCountReliable` は `false` を返します

フラグが設定されていない場合:

- 結果のTHRESHOLD値は `null`（またはそれに相当する値）です
- `IsAdjustedCountReliable` は `false` を返します

デフォルトでは、有効な親コンテキストを持つすべてのケースにおいて:

- `GetAttributes` 関数は空集合を返します
- `UpdateTraceState` 関数は、その引数を変更せずにそのまま返します

ただし、`ConsistentParentBased` の実装は、親Spanがローカルかリモートかに応じて、追加されるAttributesをカスタマイズしたり、トレース状態を変更したりできるようにすべきです（SHOULD）。

### ConsistentAnyOf {#consistentanyof}

`ConsistentAnyOf` は、`Composable` サンプラー（デリゲート）の空でないリストを引数に取るコンポジットサンプラーです。
その意図は、デリゲートの _いずれか_ が肯定的な決定を行う場合に、肯定的なサンプリングの決定を行うことです。

`GetSamplingIntent` 操作が呼び出されると、リスト全体を走査し、受け取ったのと同じ引数を渡して各デリゲートサンプラーの `GetSamplingIntent` 操作を呼び出さなければなりません（MUST）。

`ConsistentAnyOf` サンプラーは、以下のとおり構築される `SamplingIntent` を返さなければなりません（MUST）。

- いずれかのデリゲートが `null` でない閾値を返した場合、結果の閾値は、それらの `null` でない値の集合のうち辞書式順序で最小の値Tとなります。そうでない場合は `null` です
- `IsAdjustedCountReliable` は、閾値Tと等しい値を返したデリゲートのいずれかが、自身の `IsAdjustedCountReliable` 関数の呼び出しで `true` を返す場合に `true` を返します。そうでない場合は `false` を返します
- `GetAttributes` 関数は、宣言された順序で、各デリゲートに対する `GetAttributes` 関数の呼び出しが返す `Attribute` セットの和集合を計算します
- `UpdateTraceState` 関数は、デリゲートが返す `UpdateTraceState` 関数を連鎖的に呼び出します。受け取った `Tracestate` を後続の呼び出しの引数として渡し、最後に受け取った値を返します

各デリゲートサンプラーは、上記のとおり `SamplingIntent` の計算に参加する機会を必ず与えられなければならず（MUST）、また同じ引数の値を見なければなりません（MUST）。
デリゲートサンプラーの順序は、最終的なサンプリング `Decision` には影響しません。

### ConsistentRateLimiting {#consistentratelimiting}

`ConsistentRateLimiting` は、別のサンプラー（デリゲート）にサンプリングのヒントを提供させつつ、サンプリングされるスパンの平均レートを制御するのに役立つコンポジットサンプラーです。

#### Required Arguments for ConsistentRateLimiting {#required-arguments-for-consistentratelimiting}

- Composable（デリゲート）
- サンプリングの最大目標レート（スループット）

サンプラーは、受信するスパンの平均レートを計測して保持し、それによって受信スパンレートと目標スパンレートの間の望ましい比率も保持すべきです（SHOULD）。
`GetSamplingIntent` 操作が呼び出されると、コンポジットサンプラーは、受け取ったのと同じ引数を渡してデリゲートサンプラーから `SamplingIntent` を取得しなければなりません（MUST）。

返される `SamplingIntent` は、以下のとおり構築されます。

- 取得した閾値を最終的な閾値としてそのまま使用すると、宣言された目標レートを超えてスパンをサンプリングすることになる場合、サンプラーは閾値を目標レートを満たす値に設定すべきです（SHOULD）。閾値の調整にはいくつかのアルゴリズムを使用できますが、仕様として特定の振る舞いは規定されていません
- `IsAdjustedCountReliable` は、デリゲートが提供する `SamplingIntent` に対してこの関数を呼び出した結果を返します
- `GetAttributes` 関数は、デリゲートが提供する `SamplingIntent` に対してこの関数を呼び出した結果を返します
- `UpdateTraceState` 関数は、デリゲートの `SamplingIntent` から `UpdateTraceState` を呼び出して返される `Tracestate` を返します

## Summary {#summary}

### Example - sampling configuration {#example---sampling-configuration}

サンプリング要件の[例](#example)に戻ると、サンプラーとその引数に関する非公式な記法を使って、この特定のケースをサポートするようにヘッドサンプラーを設定できます。
まず、ROOTスパンに対する要件を次のように表現してみましょう。

```
S1 = ConsistentRuleBased(ROOT, {
 (http.target == /healthcheck) => ConsistentAlwaysOff,
 (http.target == /checkout) => ConsistentAlwaysOn,
 true => ConsistentFixedThreshold(0.25)
 })
```

注: 厳密には、`ROOT` は `SpanKind` ではなく、無効な親コンテキストを持つすべてのSpan（つまり種類を問わないROOTスパン）に一致する特別なトークンです。

次のステップでは、ルート以外のスパンも扱えるようにサンプラーを構築します。

```
S2 = ConsistentParentBased(S1)
```

サービス `/foo` を呼び出す特殊なケースは、これで次のようにサポートできます。

```
S3 = ConsistentAnyOf(S2, ConsistentRuleBased(CLIENT, {
           (http.url == /foo) => ConsistentAlwaysOn
     }))
```

最後のステップとして、エクスポートされるスパンのストリームに上限を設けます。

```
S4 = ConsistentRateLimiting(S3, 1000)
```

これが完全な例です。

```
S4 = 
ConsistentRateLimiting(
    ConsistentAnyOf(
        ConsistentParentBased(
            ConsistentRuleBased(ROOT, {
                (http.target == /healthcheck) => ConsistentAlwaysOff,
                (http.target == /checkout) => ConsistentAlwaysOn,
                true => ConsistentFixedThreshold(0.25),
            })
        ),
        ConsistentRuleBased(CLIENT, {
            (http.url == /foo) = > ConsistentAlwaysOn,
        }),
    ),
    1000,
)
```

### Limitations {#limitations}

`Composable` サンプラーの開発者は、自身が意図として宣言するサンプリングDecisionが、最終的なサンプリングDecisionとは異なる場合があることを考慮すべきです。

### Prototyping {#prototyping}

JavaのためのComposableサンプラーのプロトタイプ実装が利用可能です。
[ConsistentSampler](https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/consistent-sampling/src/main/java/io/opentelemetry/contrib/sampler/consistent56/ConsistentSampler.java)とそのサブクラスを参照してください。

## Prior art {#prior-art}

すでに、独立したコントリビューションとして数多くのコンポジットサンプラーが利用可能です（[RuleBasedRoutingSampler](https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/samplers/src/main/java/io/opentelemetry/contrib/sampler/RuleBasedRoutingSampler.java)、[Stratified Sampling](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/stratified-sampling-example)、LinksBasedSampler [Java版](https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/samplers/src/main/java/io/opentelemetry/contrib/sampler/LinksBasedSampler.java)および[.NET版](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/links-based-sampler)）。
また、歴史的には、[JaegerRemoteSampler](https://www.jaegertracing.io/docs/2.14/architecture/sampling/#remote-sampling)によって一部のSpanの分類が導入されていました。

本提案は、これらのアイデアを一般化し、コンポジットサンプラーの振る舞いについてもう少し形式的な仕様を提供することを目指しています。

