🧐

OpenTelemetry でトレースを可視化できない時のトラブルシュート

2024/03/21に公開

はじめに

今までいくつか OpenTelemetry の記事を書いてきました(Go で書いたサンプルOpenTelemetry Go SDK そのものを調べた記事AWS Lambda からトレースを送る記事など)。公開しているコードは完成形のもので最初から動くコードを書けたわけではありません。今回 opentelemetry-rust を使ってトレースを可視化しようとすると案の定詰まったので、今回はトレースを可視化できない時どのような原因が考えられるかをいくつかあげていこうと思います。

アプリケーションから sidecar の OpenTelemetry Collector 経由でトレースを送る

インフラの設定に原因がある場合

Collector にトレースをバックエンドに送信する権限がない

例えば AWS X-Ray にトレースを送信する場合は OpenTelemetry Collector に X-Ray への書き込み権限が必要です。Amazon ECS on AWS Fargate でアプリケーションを動かし OpenTelemetry Collector をサイドカーで動かす場合は ECS タスクロールに X-Ray への書き込み権限が、AWS Lambda の function を計装し Collector を Lambda layer や Lambda extension に登録している場合は Lambda execution role に X-Ray への書き込み権限が必要になります。これらの権限がない場合はトレースを X-Ray に送ることができないのでコンソールにトレースは表示されません。

Collector の設定が間違っている

xray exporter の場合は公式ドキュメントにサンプルがあります。ここの設定が間違っている(例えば X-Ray のリージョンを打ち間違えるとか、receiver のエンドポイントがアプリケーションが想定しているものと違うとか)とトレースは送信できません。

New Relic の場合は実際に動かして確認したわけではないですがこれを見る限りだと exporter のエンドポイントと API KEY の設定を間違えているとトレースを送ることができなそうです。

アプリの設定に問題がある場合

インフラの設定に問題がある場合で思いつくのはこれくらいですが正直数はあまり多くない気がします。今まで上げたものが原因だとある意味幸せだと思います(アプリの計装はちゃんとできていたということなので)。

厄介なのはここからでアプリの計装を間違っているとトレースは表示されません。自分がよくハマるのもアプリの計装を間違っている時です。トレースが表示されない場合は二つに分けることができて、そもそもトレースが一つも表示されない場合とトレースの親子関係が想定した通りに表示されない場合が考えられます。この場合のトラブルシュート方法を考えてみました。

色々な場所で TraceID を print debug する

アプリの計装を間違えていてトレースが思った通りに表示されない場合はどこかで TraceID が 0 になっているか途中で TraceID が別の値になっている可能性が高いです(逆にそうでないのにトレースが表示されないならインフラの設定に立ち返ることも検討するといいでしょう)。たとえば Propagator から TraceID をうまく取り出せなかったとか、Span の生成時にうまく Context を渡せなかったなどが考えられる理由です。

TraceID をどうやって取得するかについては OpenTelemetry の仕様に書いているように Span Context から取り出せばいいでしょう。例えば Go の場合は Span.SpanContext() で Span Context を持ってこれるので、これの TraceID() を呼べばいいです。まとめると以下のようになります。

package main

import (
    "context"
    "go.opentelemetry.io/otel"
)

func showTraceID(ctx context.Context) {
    // tracer provider の設定は割愛
    tracer := otel.Tracer("backend-tracer")
    // ctx は context.Context
    ctx, span := tracer.Start(ctx, "some operation")
    traceID = span.SpanContext().TraceID().String()
    fmt.Println("Trace ID is ",traceID)
}

Span の操作や ctx の更新前後で Trace ID を表示します。そして TraceID が 0 になっている箇所があればその周辺で Context が初期化されている可能性が高いです。

debug exporter を使う

debug exporter は現時点では development 状態ですが文字通りデバッグに有用です。コンソールに出力されるので TraceID や ParentID が正しくセットされているか確認することができます。

OpenTelemetry Collector のエンドポイントが正しくセットされているか確認する

例えば以下のように Docker Compose で ADOT Collector を動かしている場合を考えます。このときアプリケーションコンテナ app からトレースを ADOT Collector に送る場合のエンドポイントは http://otel-collector:4317 になります( Compose でのネットワーキングドキュメントより)。一方でこのアプリケーションコンテナを例えばクラウドにデプロイするとエンドポイントは変わります( SDK にとって gRPC のエンドポイントのデフォルトは 0.0.0.0:4317 です)。ローカルで Docker Compose で開発するつもりでエンドポイントがアプリのコードにハードコードされていないか確認するといいと思います。

version: "3.1"

services:
    app:
    # アプリケーションコンテナ詳細は割愛
        depends_on:
        - "otel-collector"
    otel-collector:
        image: public.ecr.aws/aws-observability/aws-otel-collector:v0.34.0
        command: [ "--config=/etc/otel-agent-config.yaml" ]
        volumes:
            - type: bind
              source: ./adot-config.yaml
              target: /etc/otel-agent-config.yaml
        ports:
            - 4317:4317

gRPC で Collector に接続するときのオプションを見る限りだとエンドポイントに疎通が取れなくてもアプリが落ちることはなさそうです。

Propagator が設定されているか確認する

以前自分が書いた記事で Context Propagation は説明しています。分散トレーシングでは Trace ID などの情報(Span Context)を複数システムに伝播させる必要があり、この context の伝播つまり propagation が正しく行われている必要があります。例えば client から server に HTTP リクエストを送って一連の処理をトレースしたい場合、context propagation が行われないと client 側と server 側で別の Trace ID が生成されて横串でトレースを可視化できないといったことが起こりえます。

Propagator は Go の場合だとこちらのリポジトリにいくつか定義されています(Jaeger や AWS X-Ray など)。HTTP でやり取りする際は Propagator ごとにどのヘッダー名から Trace ID を取り出すかが異なります(X-Ray なら X-Amzn-Trace-Id ヘッダーから取り出し、Trace Context なら traceparent ヘッダーから取り出す)。そのためシステム全体でどの Propagator を使うか決めて上で以下のように各コンポーネントで Propagator を設定するコードを書き忘れていないか確認すると良いでしょう。

package main
import (
    "go.opentelemetry.io/contrib/propagators/aws/xray"
    "go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/propagation"
)

func setOtelPropagator() {
    // AWS X-Ray 用の Propagator を設定する場合
    otel.SetTextMapPropagator(xray.Propagator{})
    // W3C Trace Context encoding を設定する場合
    otel.SetTextMapPropagator(propagation.TraceContext{})
}

(AWS Lambda の場合)環境変数からトレース ID を取得する

AWS Lambda の場合トレース ID は環境変数にセットされています。Go のランタイムを使っている場合はここでトレース ID を context に登録しているので handler 関数は ctx 経由でトレース ID にアクセスできます。一方で例えば Rust を使っている場合は `span_context_from_str で SpanContext を生成してトレース ID を取得する必要があります。

(X-Ray を使う場合) ID Generator が設定されているか確認する(ADOT v0.34 より前の場合)

AWS X-Ray でトレースを可視化する場合で AWS Distro for OpenTelemetry collector v0.34 より前のバージョンを使っている場合の話です。それ以降のバージョンだとこちらのアップデートにより AWS X-Ray が W3C フォーマットのトレースを受け付けるようになったため以下に書いている Id Generator の指定は不要です。

これは AWS X-Ray で詰まることが多いケースです。AWS X-Ray の Trace ID フォーマットはドキュメントにあるように1-58406520-a006649127e371903a2de979 のような形式である必要があります(1、16 進数 8 桁、16 進数 24 桁)。OpenTelemetry の Trace ID はここに書かれているように 16 進数 32 桁で X-Ray のフォーマットと異なります。例えば Go の場合は TracerProvider を作成するときに Id Generator を指定します。

(Rust の場合) tracing クレートか opentelemetry クレートどちらを使っているか整理する

Rust の場合 tokio が開発している tracing と OpenTelemetry の二種類のエコシステムがあります(issue もあります)。OpenTelemetry SDK で Span を生成し exporter 経由でバックエンドに送ることは当然できますし、tracing を使って Span を生成し tracing-opentelemetry を使って OpenTelemetry 互換のバックエンドに送ることもできます。サンプルをコピペして動かしていると設定を間違えていてトレースを可視化できないことがあります。

例えばこちらの Rust Lambda Runtime でトレースを生成する例を見ると以下のように tracing_subscriber が設定されています。

let tracer_provider = trace::TracerProvider::builder()
    .with_batch_exporter(exporter, runtime::Tokio)
    .build();

// Set up link between OpenTelemetry and tracing crate
tracing_subscriber::registry()
    .with(tracing_opentelemetry::OpenTelemetryLayer::new(
        tracer_provider.tracer("my-app"),
    ))
.init();

計装したい関数で Span を生成する場合に例えば以下のようにするとうまくバックエンドで可視化できません。

let tracer = global::tracer("lambda-tracer");
let mut span = tracer.start("detect-label");

なぜかというと OpenTelemetry の世界での Tracer Provider が登録されていないからです。以下のように Tracer Provider を登録することでそれ以外の場所で global::tracer() を呼ぶと Tracer Provider から Tracer を取得するようになります。

let _ = opentelemetry::global::set_tracer_provider(tracer_provider);

fake collector を使ってテストする

トラブルシュートする場合にローカルのコードを変更する -> コンテナイメージをビルドする -> クラウドにデプロイする -> 動作確認する、ということを繰り返していると時間がかかってしまいます。試行錯誤を加速するためにローカル上で OpenTelemetry Collector と例えば Jaeger を動かす(以前この記事で紹介しました)アプローチがあります。ただ本番環境で X-Ray にトレースを送るつもりだと本番では使わない Jaeger 用の設定がアプリコードに入ってきてしまいます。そこで今回は別のアプローチとして fake collector を開発時に導入する方法を紹介します。

Go の場合は公式のリポジトリに tracetest パッケージがありますし、Rust の場合は公式ではないですが fake-opentelemetry-collector クレートがあります。これらを使って単体テストのコードを書いておくといちいちクラウドにデプロイして動作確認せずに済みます。

おわりに

OpenTelemetry で計装するときに詰まった場合はこの記事で紹介した方法を試してみてください。

Discussion