🔭

OpenTelemetry でトレースを収集・可視化する(Jaeger, AWS X-Ray)

2022/10/02に公開

はじめに

OpenTelemetry を最近勉強しているので実際に動くものを作ってみました。Golang+Echo で DynamoDB とやりとりする簡単なアプリケーションを作って AWS App Runner にデプロイし、トレースデータを AWS X-Ray に送ってトレースを可視化できるようにしました。最初はコードを更新するたびに App Runner にデプロイしていましたがそれだと時間がかかるので、Docker Compose を使ってローカルで OpenTelemetry Collector や Jaeger を動かし手元でも開発しやすくしました。リポジトリはこちら

OpenTelemetry とは

公式の画像がわかりやすいです。

例えばトレースの可視化用ツール A、メトリクスの可視化用ツール B、クラウドプロバイダのロギングツール C というふうに複数のツールを使っている場合を考えてみます。それぞれのツール用のエージェントをサイドカーで動かし、アプリケーションにはツール用の SDK を組み込んでそれぞれのツール向けにデータを送信するというパターンが多いでしょう。これだけ複数のツールを使うのは極端な場合だけかもしれませんが、それでも複数のツール用にサイドカーのエージェントを管理するのは煩雑ですし、アプリケーションにツール専用の SDK を組み込むと他のツールへの乗り換えが難しくなります。またツールごとに API や機能に差異があるとさらに乗り換えは難しくなるでしょう。

そこで OpenTelemetryです。公式ドキュメントでは以下のように説明されています。

OpenTelemetry, also known as OTel for short, is a vendor-neutral open-source Observability framework for instrumenting, generating, collecting, and exporting telemetry data such as traces, metrics, logs. As an industry-standard it is natively supported by a number of vendors.

OpenTelemetry はベンダー中立の Observability フレームワークでトレース・メトリクス・ログといったテレメトリデータを取り扱うことができます。詳しい歴史はドキュメントのここら辺にまとまっていて、今は CNCF の incubating プロジェクトでありベンダー中立の SDK や API を策定しています。後述する Collector をサイドカーなどの形式で動かしておいて、アプリケーション側からは OpenTelemetry の SDK を使って Collector にテレメトリデータを送信すれば、あとは Collector の設定に従ってバックエンドのツールにテレメトリデータを送信して可視化などを行えます。ツールごとにエージェントや SDK を組み込むのではなく、バックエンドのツールごとに設定を変えた上で Collector を共通で動かして OpenTelemetry という共通の SDK を使ってテレメトリデータを送信することができます。

Collector

こちらも公式の画像がわかりやすいです。

Collector は Receiver・Processor・Exporter の3つから構成されていて、それぞれの設定を yaml ファイルで定義します。それぞれのコンポーネントはドキュメントのこのページで詳しく解説されています。例えば OTLP プロトコルでテレメトリを受信して、Batch Processor でまとめて処理して、バックエンドの AWS X-Ray に送信するような設定は以下のようになります。

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:

exporters:
  awsxray:
    region: "us-east-1"

service:
  pipelines:
    traces:
      receivers:
        - otlp
      exporters:
        - awsxray

AWS Distro for OpenTelemetry (ADOT)

AWS Distro for OpenTelemetry (ADOT) は AWS がサポートしている OpenTelemetry のディストリビューションです。AWS 環境で利用しやすいようにしたディストリビューションではあるものの、基本的に AWS はアップストリームへ先にコントリビュートしていくことを FAQ で明記しています。

AWS contributes to OpenTelemetry in an upstream-first model: our enhancements to the project are first contributed to the upstream project, then we build the AWS Distro for OpenTelemetry from upstream OpenTelemetry components.

ローカルでの開発

毎回クラウドにデプロイしないと動作確認できないのでは不便なので開発するときはローカルで Collector を動かしておきましょう。以前書いたように VSCode Remote Containers を使って開発していきます。

Docker Compose を使う(ADOT)

こちらに ADOT Collector を Docker Compose で使う方法が書かれています。

version: "3.1"

services:
  otel-collector:
    image: public.ecr.aws/aws-observability/aws-otel-collector:latest
    command: [ "--config=/etc/otel-agent-config.yaml" ]
    volumes:
      - ./otel-config.yaml:/etc/otel-agent-config.yaml
    environment:
      - AWS_REGION=us-east-1
      - AWS_ACCESS_KEY_ID=DUMMYIDEXAMPLE
      - AWS_SECRET_ACCESS_KEY=DUMMYEXAMPLEKEY
    ports:
      - 4317:4317

Collector コンテナの起動時に config ファイルを指定しています。開発時にも X-Ray にトレースを送りたい場合は AWS のクレデンシャル情報を環境変数にセットする必要がありますが、必要でなければダミーのものでも構いません(ダミーを使う場合当然ですがトレースを X-Ray に送れないよというログが表示されます)。

Docker Compose を使う(Jaeger)

Jaeger にトレースを送る場合は All in one コンテナイメージを使うと便利です。これは Jaeger のエージェントだけでなくウェブ UI も同梱された全部入りコンテナで、このコンテナに対してトレースを送り UI でトレースを確認することができます。

注意点として All in one イメージで OpenTelemetry Collector を有効化するにはここにあるように環境変数 COLLECTOR_OTLP_ENABLEDtrue に設定する必要があります。

Receiver のエンドポイント

Docker Compose だとサービス名を使ってそれぞれのコンテナが通信できます(ドキュメントはこちら)。OTLP Receiverの README に書いているように OTLP Receiver のデフォルトエンドポイントは 0.0.0.0:4317 です。そこで Docker Compose でローカル開発しているときは環境変数 OTEL_EXPORTER_OTLP_ENDPOINT を設定してローカルで動かしている OpenTelemetry Collector のコンテナ( otel-collector:4317 )をエンドポイントに指定し、環境変数が設定されていなければデフォルトのエンドポイント( 0.0.0.0:4317 )を指定するようにしています。こうしておけばアプリケーションのコードを変更せずにローカル開発と本番へのデプロイを両立できます。さらに後述するように X-Ray と Jaeger で IDGenerator が異なるのでここでバックエンドサービスによって設定を変えるようにしています。

endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
if endpoint == "" {
  endpoint = "0.0.0.0:4317" // setting default endpoint for exporter
}
// Create and start new OTLP trace exporter
traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithDialOption(grpc.WithBlock()))

ADOT for Golang

ドキュメントからリンクされているサンプルプロジェクトはこちらです(Gorilla、というよりそのルーターコンポーネントの gorilla/mux を使っています)。ADOT Go SDK を使って X-Ray にトレースを送る方法はこのドキュメントに書かれているので詳しくみていきます。

コードを見てみる

トレースを手動で実装する場合はこちらのドキュメントに一連の流れがまとまっています。ここに書いていることに加えて気になるところをいくつかピックアップしてみていきます。

OpenTelemetry と X-Ray のトレース仕様

X-Ray のトレース ID はこちらのドキュメントに書かれているように 1-58406520-a006649127e371903a2de979 のような形式です。

  • 最初の 1: バージョン番号
  • ハイフンで区切った次の 8 桁: 元のリクエストの時刻。Unix エポック時間で、8 桁の 16 進数。
  • ハイフンで区切った次の 24 桁: グローバルに一意なトレースの 96 ビット識別子

一方で OpenTelemetry のトレース ID はこちらに書かれているように 58406520a006649127e371903a2de979 のような 32 文字のランダムな 16 進数文字列形式です。

また X-Ray のトレースをヘッダーで伝播させていく仕様はドキュメントのこのページにまとまっています。 X-Amzn-Trace-Id というヘッダーにトレース ID とサンプリングされているかを含めています。

一方で OpenTelemetry の場合は W3C の標準仕様である Trace Context に従っています。

このように X-Ray と OpenTelemetry で形式が違う部分がありますが、ADOT ではそれぞれ X-Ray ID Generator (このブログで解説されています)と AWS X-Ray Propagator を開発してコントリビュートしています。GitHub の OpenTelemetry-Go Contrib リポジトリの中に AWS がコントリビュートしている部分があります。

AWS SDK を計装する

ドキュメントはこちらです。注意点として 2022/09 時点だと AWS SDK v2 にしか対応していません。

Note: We currently don't provide support for instrumenting AWS SDK v1 calls.

AWS SDK for Go だと API コールを行うメソッドの第一引数は context.Context です。 Tracer.Start で新しく作成した span を含む Context を生成し、生成した Context をメソッドの第一引数に渡せば良いです。

AWS の API コールを計装する部分はドキュメントのここに書いています。

// これはドキュメントに載っている例です
tracer := otel.Tracer("demo")
ctx, span := tracer.Start(context.Background(), "AWS SDK instrumentation")
defer span.End()

// init aws config
cfg, err := awsConfig.LoadDefaultConfig(ctx)
if err != nil {
    panic("configuration error, " + err.Error())
}

// instrument all aws clients
otelaws.AppendMiddlewares(&cfg.APIOptions)

// Call to S3
s3Client := s3.NewFromConfig(cfg)
input := &s3.ListBucketsInput{}
result, err := s3Client.ListBuckets(ctx, input)
if err != nil {
    fmt.Printf("Got an error retrieving buckets, %v", err)
    return
}

バックエンドを Jaeger と X-Ray で切り替える

OpenTelemetry とはのところで書いたように、OpenTelemetry を使うことでアプリのコードはほとんど変えずに異なるバックエンドに向けてテレメトリデータを送信することができます。X-Ray の場合トレースの仕様が OpenTelemetry と異なるので ID Generator を X-Ray 用のものを使う必要があり若干のコードの修正は必要です。とはいえ AWS SDK を計装する部分は Jaeger と X-Ray で同じということはコードを読めばなんとなくわかるんじゃないかと思います(ここでバックエンドごとに処理を切り替える以外は Jaeger と X-Ray でアプリケーションのコードは同じです)。

AWS にデプロイ

インフラのコードは CDK で書いているので以下のコマンドで一発デプロイできます。使用するリソースによって料金が発生するので最後にリソースを消すのを忘れずに。

$ npm install
# us-east-1 で bootstrap していなければしておく
# $ cdk bootstrap
$ cdk deploy --require-approval never

ローカルで Docker Compose を使ってアプリケーションコンテナと Jaeger コンテナを立ち上げると手元でトレースを可視化できます。

docker-compose up
curl http://localhost:8080/
curl http://localhost:8080/traceid

us-east-1 の X-Ray のページにいくとサービスマップが表示されます。クライアント -> App Runner サービス -> DynamoDB へのリクエストの流れが可視化されます。

おわりに

OpenTelemetry を使えば今回のように Jaeger と X-Ray の切り替えが少しの設定変更で実現できて便利です。

Discussion