W3C Trace Contextについて

本記事は[OpenTelemetry Advent Calendar 2023](https://qiita.com/advent-calendar/2023/otel)の16日目の記事になります。
昨日は@munisystemさんのOpen Agent Management Protocol (OpAMP) についてでした。

本記事では[W3C Trace Context](https://www.w3.org/TR/trace-context/)の内容を要約しています。
また、本記事を読む前にOpenTelemetry Advent Calendar 2023 3日目の@sumirenさんによる[OpenTelemetry 分散トレーシングのシステムアーキテクチャ](https://zenn.dev/sumiren/articles/7a935f14ed7265)を読んでいただくと理解が深まると思います。

# OpenTelemetryとの関係性

OpenTelemetryの[Context Propagation](https://opentelemetry.io/docs/concepts/signals/traces/#context-propagation)は複数のフォーマットをサポートしていますが、デフォルトで使用されるフォーマットがW3C Trace Contextになります。つまり標準ではW3C Trace Contextの形式でHTTPヘッダーを挿入し、トレース情報を伝播していきます。

# W3C Trace Contextとは

[W3C Trace Context](https://www.w3.org/TR/trace-context/)は、分散システムにおけるトレース情報の伝播を標準化するための仕様です。HTTPヘッダーと値の形式を定義し、それによって分散トレーシングを可能にします。具体的には、各サービス間でコンテキスト情報を送信し、変更する方法を標準化しています。その名の通り、World Wide Web Consortium (W3C)によって標準されています。

このコンテキスト情報は、分散システム内の個々のリクエストを一意に識別し、プロバイダ固有のコンテキスト情報を追加し、伝播する手段を定義します。これにより、異なるトレーシングベンダーによって収集されたトレースを相互に関連付けることが可能になります。

## トレースコンテキストのHTTPヘッダーフォーマット

分散トレースにおけるコンテキストとして、`traceparent`と`tracestate`の2つを定義しています。

**traceparent**は、そのトレースにおけるリクエストの位置を移植可能な固定長フォーマットで記述します。その設計は高速な解析に重点を置いています。すべてのトレースツールは、 tracestate のベンダ固有の情報のみに依存する場合でも、 traceparent を適切に設定しなければなりません。
**tracestate**は、名前と値のペアのセットで表されるベンダー固有のデータで traceparent を拡張します。tracestate に情報を格納することはオプションです。

最低限、`traceparent`と`tracestate`ヘッダーを伝搬し、トレースが壊れないことを保証しなければなりません。この動作はトレースの転送とも呼ばれます。
さらに、トレースは`traceparent`ヘッダーと`tracestate`ヘッダーの関連部分を修正することで、トレースに参加することもできます。これはトレースへの参加とも呼ばれます。
トレースツールは、監視しているコンポーネントへの個々のリクエストごとに、この動作を変更することができます。

### ヘッダー間の関係性

`traceparent`ヘッダーは、トレースシステムの受信リクエストを、すべてのベンダが理解できる共通のフォーマットで表します。以下は `traceparent`ヘッダーの例です。
```
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
```
`tracestate`ヘッダーは、ベンダ固有の形式である可能性のある親を含みます：
```
tracestate: congo=t61rcWkgMzE
```

例えば、あるシステムのクライアントとサーバーが異なるトレーシング・ベンダーを使用しているとします： CongoとRojoです。Congo システムでトレースされたクライアントは、送信HTTP リクエストに以下のヘッダーを追加します。
```
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: congo=t61rcWkgMzE
```
Rojoトレースシステムでトレースされた受信サーバーは、受信したtracestateを引き継ぎ、左側に新しいエントリを追加します。
```
traceparent: 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01
tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
```
Rojoのシステムは`tracestate`のエントリに`traceparent`の値を再利用しています。これは、一般的なトレースシステムであることを意味します（独自の情報は渡されません）。そうでない場合、`tracestate`のエントリは不透明で、ベンダ固有である可能性があります。
次の受信サーバーがCongoを使用する場合、Rojoからの`tracestate`を引き継ぎ、前のエントリの左に親の新しいエントリを追加します。
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01
tracestate: congo=ucfJifl5GOE,rojo=00f067aa0ba902b7
注: `ucfJifl5GOE` は、Base64 エンコードされた親 ID `b9c7c989f97918e1` です。

Congoがその`traceparent`エントリーを書いたとき、それがエンコードされていないことに注意する必要があります。しかし`tracestate`の値はエンコードされており、`traceparent`とは異なります。これは問題ありません。

最後に、`tracestate`はRojoのエントリを、右にプッシュされている以外は、そのまま保持していることがわかります。一番左の位置は、次のサーバーにどのトレースシステムが`traceparent`に対応するかを知らせます。この場合、Congoが`traceparent`を書いたので、その`tracestate`のエントリは一番左になるはずです。

## Traceparent ヘッダー

`traceparent` HTTPヘッダーフィールドはトレースシステムで受信リクエストを識別し、以下の4つのフィールドから構成されています。

* version
* trace-id
* parent-id
* trace-flags

## Tracestateヘッダー

`tracestate` HTTP ヘッダーの主な目的は、異なる分散トレースシステム間で追加のベンダ固有のトレース識別情報を提供することであり、`traceparent`フィールドのコンパニオンヘッダーです。また、複数の分散トレースグラフにおけるリクエストの位置についての情報も伝えます。

## トレースコンテキストの処理モデル

この処理モデルは、トレースコンテキストヘッダーを修正して転送するベンダーの動作を記述します。このモデルがどのように動作するかは、`traceparent`ヘッダーを受信するかどうかに依存します。

### トレースペアレントの受信なし

traceparentヘッダーを受け取らない場合：

1. ベンダは受信リクエストに`traceparent`ヘッダーと`tracestate`ヘッダーがないかチェックします。
1. `traceparent`ヘッダーが受け取られないので、ベンダーは現在のリクエストを表す新しい`trace-id`と`parent-id`を作成します。
1. `traceparent`ヘッダーを伴わずに`tracestate`ヘッダーを受け取る場合、それは無効なので破棄されなければならない。
1. ベンダーは新しい`tracestate`ヘッダーを生成し、新しいkey/valueペアを追加すべきである。
1. ベンダーは発信リクエストに`traceparent`ヘッダーと`tracestate`ヘッダーを設定します。

### トレースペアレントの受信

`traceparent`ヘッダーを受信した場合：

1. `traceparent`ヘッダーを受け取った場合: ベンダーは受信リクエストに`traceparent`ヘッダーと`tracestate`ヘッダーがあるかどうかチェックします。
1. `traceparent`ヘッダーが存在するので、ベンダーは`traceparent`ヘッダーの バージョンの解析を試みます。
    1. バージョンが解析できない場合、ベンダは新しい`traceparent`ヘッダーを作成し、`tracestate`を削除します。
    1. バージョン番号がトレーサがサポートするバージョンより高い場合、ベンダーはこの仕様で定義されたフォーマット (`00`) を使用して`trace-id`と`parent-id`を解析します。ベンダーは、この仕様のこのバージョンでサポートされている`trace-flags`値のみを解析し、その他の値はすべて無視します。解析に失敗した場合、ベンダーは新しい`traceparent`ヘッダーを作成し、`tracestate`を削除します。ベンダーは発信リクエストに対して、すべての解析されていない / 未知の`trace-flags`を0に設定します。
    1. ベンダーがバージョン番号をサポートしている場合、`trace-id`と`parent-id`を検証します。`trace-id`、`parent-id`、`trace-flags`のいずれかが無効な場合、ベンダーは新しい`traceparent`ヘッダーを作成し、`tracestate`を削除します。
1. ベンダーは`tracestate`ヘッダーを検証してもよい。`tracestate`ヘッダーを解析できない場合、ベンダーはヘッダー全体を破棄してもよい。無効な`tracestate`エントリも破棄してもよい。
1. 各送信リクエストに対して、ベンダーは以下の手順を実行します：
    1. ベンダーは`traceparent`ヘッダーを修正しなければならない：
        * `parent-id`の**更新**： `parent-id`プロパティの値を、現在の操作のIDを表す値に設定しなければならない。
        * `sampled`の**更新**： `sampled`の値は、呼び出し元の記録動作を反映します。`trace-flags`の`sampled`フラグの値は、トレースデータが記録される可能性が高い場合は`1`に、そうでない場合は`0`に設定してもよい。このフラグを設定することは、トレースが記録されることを保証するものではありませんが、エンドツーエンドで記録されるトレースの可能性を高めます。
    1. ベンダーは tracestate ヘッダーを修正してもよい：
        * **キーバリューの更新**： キーの値を更新する: 任意のキーの値を更新することができます。変更されたキーはリストの先頭(左)に移動しなければならない。
        * **新しいキーとバリューのペアの追加**： 新しいキーと値のペアは、リストの先頭(左)に追加されなければなりません。
        * **キーと値のペアの削除**： どのkey/valueペアも削除してもよい。ベンダーは、自分で生成していないキーを削除すべきではない。キーと値のペアを削除すると、他のシステムとの相関が壊れてしまうかもしれません。
    1. ベンダーは発信リクエストに`traceparent`ヘッダーと`tracestate`ヘッダーを設定します。

# まとめ

さらに細かい詳細はぜひ[W3C Trace Context](https://www.w3.org/TR/trace-context/)をご確認ください。また、W3C Editor's Draftとして[Trace Context Level 3](https://w3c.github.io/trace-context/)が議論されています。気になる方はこちらもチェックしてみてください。