トレースコンテキスト仕様とW3C Trace Context ​

マイクロサービスアーキテクチャでは、複数のサービスをまたがるリクエストを追跡することが重要です。しかし、サービス間の通信時にトレース情報が失われてしまっては、その価値が半減してしまいます。この課題を解決するのが「トレースコンテキスト仕様」です。本記事では、業界標準として採用されているW3C Trace Contextの仕組みと、その実装方法について解説します。

なぜコンテキスト仕様が必要か ​

マイクロサービス環境では、1つのユーザーリクエストが複数のサービスを経由します。例えば、ユーザーが商品を注文する際、フロントエンド → オーダーサービス → 在庫サービス → 決済サービス というように複数のサービスが連携します。

サービス間通信での課題 ​

各サービスはローカルでトレースデータを記録していますが、これらが関連しているという情報がなければ、後から「このリクエストが全体でどのように処理されたか」を追跡できません。

従来は各企業やプロジェクトが独自の方法でこの問題を解決していました。あるサービスはカスタムHTTPヘッダを使い、別のサービスはメタデータを利用するといった具合です。このバラバラな状態では、複数のベンダーのツールを組み合わせた時に互換性がなくなってしまいます。

業界標準の必要性 ​

トレース情報を統一されたフォーマットで伝播させることで、次のメリットが得られます:

  • 相互運用性: 異なるベンダーのトレーシングツール同士が協働できる
  • 一貫性: サービス全体で統一されたトレース形式を使用できる
  • シンプルな実装: 標準に従うだけで複雑な統合作業が不要

この課題に対する解決策が、W3Cによって策定された「Trace Context」仕様です。

W3C Trace Context標準 ​

W3C Trace Contextは、分散トレーシングの標準規格として、2020年に正式勧告(Recommendation)となりました。HTTPリクエストのヘッダを通じて、トレース情報を伝播させるメカニズムを定義しています。

traceparentヘッダ:基本的なトレース情報 ​

W3C Trace Contextの中核をなすのが「traceparent」ヘッダです。このヘッダにはトレーサーが必要とする最小限の情報が含まれます。

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

このフォーマットは以下の構成です:

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

各要素の詳細:

  • Version (2桁): トレースコンテキストのバージョン。現在は「00」(Version 1)
  • Trace ID (32桁の16進数): リクエスト全体を識別するID。ルートサービスで生成されます
  • Parent ID (16桁の16進数): 親スパンを識別するID。リクエストを送信したサービスのスパンIDがこれにあたります
  • Trace Flags (2桁の16進数): トレーシングの動作を制御するフラグ。「01」はトレース対象(sampled)を意味します

具体例を通じた理解 ​

リクエストの流れを追ってみましょう。ユーザーがWebアプリケーションにアクセスすると:

  1. フロントエンドサービス: 新しいTrace IDを生成し、自身をParent IDとしてtraceparentを作成
  2. APIゲートウェイへのリクエスト: traceparentヘッダを含めて送信
  3. APIゲートウェイ: 受け取ったtraceparentを確認し、自身のスパンIDを新しいParent IDとして更新
  4. バックエンドサービス: 同じTrace IDを保持したまま、Parent IDだけが更新されたtraceparentを受け取る

このプロセスにより、全体のリクエストチェーン内で同じTrace IDが保たれ、サービス間のいかなる組み合わせでも追跡が可能になります。

tracestate ヘッダ:ベンダー固有の拡張 ​

W3C Trace Contextの設計哲学は「最小限の標準」です。traceparentだけでは、ベンダー固有のトレーシングシステムが必要とする追加情報を伝播できません。そこで登場するのが「tracestate」ヘッダです。

tracestate: vendorname1=value1,vendorname2=value2

tracestate ヘッダは複数のベンダーが独自のキーと値を追加できる拡張メカニズムを提供します。例えば:

tracestate: jaeger=value1,newrelic=value2,custom_tenant_id=tenant123

各ベンダーやアプリケーションは独自の情報を「,」で区切って追加できます。重要なのは、あるベンダーがtracestate内の他ベンダーのデータを理解する必要がないということです。自分に関連する値だけを抽出して処理すればよいのです。

パース(解析)のルール ​

W3C Trace Contextの安定性を保つため、解析ルールは明確に定められています:

  1. traceparent失敗時: traceparentをパースできなかった場合、tracestate も解析してはいけません。traceparentが不正なら、全体の信頼性が失われるためです
  2. tracestate失敗時: tracestate のパースに失敗しても、traceparent の処理は継続します。tracestate は補助的な情報だからです

このルール設計により、traceparent が正しければ、最低限のトレーシング機能は確保されます。

従来の仕様との比較と互換性 ​

W3C Trace Contextが標準化される前から、複数のトレーシングシステムが独自のコンテキスト伝播方式を開発していました。OpenTelemetryは、これら複数の形式をサポートしています。

Jaeger形式(Uber-Trace-ID) ​

JaegerはUberが開発した分散トレーシングシステムで、以下のヘッダフォーマットを使用します:

uber-trace-id: [trace_id]:[span_id]:[parent_span_id]:[sampled]

例:

uber-trace-id: abc123def456:789:0:1

特徴:

  • シンプルで読みやすい形式
  • 3つのコロンで区切られた値
  • sampled フラグで追跡対象を指定

B3 Propagation(Zipkin形式) ​

Zipkinが採用するB3フォーマットは、複数のヘッダに情報を分散させます:

X-B3-TraceId: abc123def456
X-B3-SpanId: 789abc
X-B3-ParentSpanId: 456def
X-B3-Sampled: 1

特徴:

  • HTTPヘッダを複数使用
  • 各要素が個別のヘッダに分離
  • より柔軟な拡張性

仕様比較表 ​

項目W3C Trace ContextJaeger(Uber-Trace-ID)B3 Propagation
標準化状態W3C正式勧告Uber独自仕様Zipkin仕様
ヘッダ形式単一ヘッダ単一ヘッダ複数ヘッダ
バージョン対応明示的に対応バージョン機能なしバージョン機能なし
ベンダー拡張tracestate で対応サポートなしカスタムヘッダで対応
導入容易性標準化により簡単既存システム多数既存システム多数
相互運用性ベンダー中立的Jaegerシステム向けZipkinシステム向け

OpenTelemetryでの複数形式サポート ​

OpenTelemetryは「プロパゲーター」というコンポーネントを通じて、複数のトレースコンテキスト形式をサポートしています。環境変数 OTEL_PROPAGATORS で使用する形式を指定できます:

bash
OTEL_PROPAGATORS="tracecontext,baggage"          # W3C標準
OTEL_PROPAGATORS="jaeger"                        # Jaeger形式
OTEL_PROPAGATORS="b3multi"                       # B3マルチヘッダ

この柔軟性により、既存のJaegerやZipkinシステムとの共存を保ちながら、新規プロジェクトではW3C標準への移行が可能になります。

コンテキスト伝播の実装パターン ​

トレースコンテキストが定義されたら、実際にそれをサービス間で伝播させる必要があります。通信方式や言語によって、実装パターンは異なります。

HTTPリクエスト/レスポンス ​

同期的なREST API通信では、リクエストのHTTPヘッダにtraceparentを含めます:

GET /api/users/123 HTTP/1.1
Host: api.example.com
traceparent: 00-abc123-def456-01
tracestate: custom_key=value

レスポンスのヘッダに含める場合もあります。これにより、双方向のトレーシングが可能になります。

gRPCでのメタデータ伝播 ​

gRPCではHTTPヘッダの代わりに「メタデータ」を使用します。仕組みは同じですが、形式がネイティブに異なります:

proto
metadata['traceparent'] = '00-abc123-def456-01'
metadata['tracestate'] = 'custom_key=value'

メッセージキュー(Kafka等)での伝播 ​

非同期通信の場合、メッセージのヘッダやペイロードにトレースコンテキストを埋め込みます:

json
{
  "headers": {
    "traceparent": "00-abc123-def456-01",
    "tracestate": "custom_key=value"
  },
  "body": { "data": "..." }
}

言語別の伝播メカニズム ​

W3C Trace Contextは標準ですが、プログラミング言語によって、コンテキストを保持・伝播させるメカニズムは異なります。

Python:Context.attach/detach ​

Pythonのopentelemetry-apiでは、Context オブジェクトを通じてトレースコンテキストを管理します:

python
from opentelemetry import trace, context
from opentelemetry.sdk.trace.propagation import TraceContextPropagator

# traceparentヘッダからコンテキストを抽出
propagator = TraceContextPropagator()
ctx = propagator.extract(carrier=headers)

# コンテキストをアタッチ(有効化)
token = context.attach(ctx)

# スパン作成時に自動的に正しいコンテキストが適用される
# ...

# 終了時にデタッチ
context.detach(token)

Java:TraceContextのスコープ管理 ​

JavaのOpenTelemetryでは、スパンのスコープを自動管理します:

java
SpanContext spanContext = Span.current().getSpanContext();
String traceId = spanContext.getTraceIdAsHexString();

try (Scope scope = tracer.spanBuilder("operation").startSpan().makeCurrent()) {
    // スコープ内のすべての処理が同じコンテキストで実行される
}

Node.js:AsyncLocalStorageでの保持 ​

Node.jsのOpenTelemetryでは、AsyncLocalStorage を使用して非同期処理のコンテキストを保持します:

javascript
const { trace, context } = require('@opentelemetry/api');

context.with(extractedContext, () => {
  // このスコープ内での非同期操作は
  // 同じトレースコンテキストを保持する
  const span = tracer.startSpan('operation');
});

Go:context.Contextパッケージの伝播 ​

Goはコンテキストが言語に組み込まれており、トレースコンテキストはこれを活用します:

go
// tracer は事前に取得済み、parentCtx は親コンテキストとします
ctx, span := tracer.Start(parentCtx, "operation")
defer span.End()
// ctxを後続の関数に渡すことで、コンテキストが伝播される

このように、各言語の特性に合わせて、効率的にトレースコンテキストが管理されています。

まとめ ​

W3C Trace Contextは、マイクロサービスアーキテクチャにおいて、複数サービス間でトレース情報を統一的に伝播させるための業界標準です。traceparentとtracestate という2つのHTTPヘッダを使用した シンプルで拡張性の高い設計により、ベンダー中立的な相互運用性を実現しています。

OpenTelemetryがこの標準をデフォルトで採用することで、トレースコンテキスト伝播の実装は大幅に簡素化されました。複数のレガシーフォーマット(JaegerやB3)もサポートされているため、既存システムとの親和性も保たれています。

次の記事では、こうしたコンテキスト仕様を実際にアプリケーションでどのように活用し、エンドツーエンドのトレーシングを実装するかについて、さらに詳しく解説していきます。