こんにちは!株式会社カナリーで新規プロダクト開発をしている tom-uchida です。
弊社ではモニタリングツールとして Datadog を使用しているのですが、現在開発中の新規プロダクトにおいて、OpenTelemetry x Datadog で分散トレーシングを実現することで、エラー発生時のデバッグ体験を向上させることができたので、本記事にてご紹介できればと思います。
はじめに
本記事において分散トレーシングの対象となるプロダクトのアーキテクチャについて簡単にご紹介しておきます。
図1のような分散アーキテクチャとなっており、Frontend、API Gateway、Microservice がそれぞれ GKE 上で動作しています:
図1. 本記事において分散トレーシングの対象となる分散アーキテクチャ
API Gateway では grpc-gateway を使用しており、Frontend ⇔ API Gateway 間の通信は REST、API Gateway ⇔ Microservice 間の通信は gRPC となっているため、抽象化すると図2のような構成となっています:
図2. 本記事において分散トレーシングの対象となる分散アーキテクチャ(抽象化版)
分散トレーシングの実現方法
Context Propagation(コンテキスト伝搬)
分散トレーシングを実現するうえでポイントとなるのが「Context Propagation(コンテキスト伝搬)」と呼ばれる概念です:
Context Propagation とは、シグナルをサービス間の通信を通じて引き継ぐ仕組みのことです。これにより、シグナルを異なるサービス間で共有することが可能となるため、ログとトレースを紐付けることができるようになり、分散トレーシングを実現できるようになります。
たとえば、本記事で扱う分散アーキテクチャ(図2)の場合、図3のように Context Propagation させることができれば、分散トレーシングを実現できるということになります:
図3. 本記事で実現したい Context Propagation
それでは、実現したいゴールが明確になったところで、OpenTelemetry x Datadog における Context Propagation の具体的な実装方法について解説していきたいと思います。
Frontend(HTTP Client)
リクエストの起点となる Frontend では、以下の Datadog 形式の HTTP ヘッダーを設定して API Gateway にリクエストするように設定します:
-
x-datadog-trace-id
(トレースID) -
x-datadog-parent-id
(親スパンID) -
x-datadog-sampling-priority
(サンプリング優先度)
API Gateway(HTTP Server A + gRPC Server A)
API Gateway では、Frontend から送信される Datadog 形式の HTTP ヘッダーの値を HTTP Server A で受信して、適切に gRPC Server A へと Context Propagation させる必要があります。
これを実現するために設定するべき項目が大きく5つあるため、順番に見ていきましょう。
1. OpenTelemetry の Tracer Provider の設定
Tracer Provider とは、OpenTelemetry でトレースを計装するうえでの最初のステップです。
Tracer Provider はサーバー起動時に一度だけ初期化され、そのライフサイクルはアプリケーションのライフサイクルと一致します。そのため、サーバー単位で起動時に1回だけ設定すればよいものになります。
たとえば、本記事で扱う分散アーキテクチャ(図1)の場合、API Gateway と Microservice の2つのサービスが設定対象となります。具体的には、以下のような関数をサーバー起動時に呼び出します:
import (
"context"
"os"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
)
func InitTracerWithDatadog(ctx context.Context, serviceName string) (func() error, error) {
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
DatadogPropagator{},
),
)
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String(serviceName)
),
)
if err != nil {
return nil, err
}
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpointURL(
os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"),
),
)
if err != nil {
return nil, err
}
tp := trace.NewTracerProvider(
trace.WithResource(res),
trace.WithBatcher(exporter),
)
otel.SetTracerProvider(tp)
cleanup := func() error {
return tp.Shutdown(ctx)
}
return cleanup, nil
}
この関数では、OpenTelemetry の Tracer Provider を初期化することに加えて、Datadog と連携させるための設定が2つ含まれています。
1つ目は以下の実装箇所です。こちらの詳細については次項で説明します:
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
DatadogPropagator{},
),
)
2つ目は以下の実装箇所です。ここで、OTEL_EXPORTER_OTLP_ENDPOINT
という環境変数を使用しています。これは、OpenTelemetry がトレース情報を送信する先のエンドポイントを指定する環境変数になります:
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpointURL(
os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"),
),
)
今回は Datadog と連携させたいため、OTEL_EXPORTER_OTLP_ENDPOINT
には Datadog Agent のエンドポイントを設定しています:
Datadog Agent の設定方法については本記事では割愛しますが、過去に弊社テックブログで公開された以下の記事などを参考にしてください:
2. Datadog 形式に対応した TextMap Propagator の設定
Datadog 形式の HTTP ヘッダー(x-datadog-trace-id
など)は Datadog 独自のトレースコンテキスト形式であり、OpenTelemetry のデフォルトの TextMap Propagator(W3C TraceContext)とは互換性がありません。
そのため、OpenTelemetry SDK に Datadog 独自のトレースコンテキスト形式を理解させるためには、Datadog 形式に対応した TextMap Popagator を実装して、otel.SetTextMapPropagator
関数で登録させる必要があります。
では、Datadog 形式に対応した TextMap Propagator はどのように実装すればよいのでしょうか?
OpenTelemetry の otel.SetTextMapPropagator
関数は、仮引数として propagation.TextMapPropagator
インターフェイスを受け取るようになっているので、このインターフェイスの実装を見てみましょう:
すると、メソッドが3つ定義されていることがわかります。各メソッドの概要は以下の通りです:
メソッド | 内容 |
---|---|
Inject |
・送信時に使われる(クライアント側で使用される) ・Go の context に入っているトレース情報を取り出して、HTTP ヘッダーや gRPC メタデータに注入(Inject)する
|
Extract |
・受信時に使われる(サーバー側で使用される) ・HTTP ヘッダーや gRPC メタデータからトレース情報を抽出(Extract)して、Go の context に埋め込む
|
Fields |
Inject メソッドによって設定されたフィールド一覧を返す |
したがって、Datadog 形式に対応した TextMap Propagator を実装するためには、これら3つのメソッドを定義することで propagation.TextMapPropagator
インターフェイスを満たしてあげれば良いということになります。
たとえば、propagation.TextMapPropagator
インターフェイスを満たす構造体の名前を DatadogPropagator
とした場合、以下のような実装となります:
import (
"context"
"go.opentelemetry.io/otel/propagation"
)
type DatadogPropagator struct{}
var _ propagation.TextMapPropagator = &DatadogPropagator{}
func (p DatadogPropagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
...
}
func (p DatadogPropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
...
}
func (p DatadogPropagator) Fields() []string {
...
}
各メソッドの具体的な実装方法については、OpenTelemetry の Registry にサンプル実装のリポジトリが登録されているため、こちらのリポジトリを参照していただければと思います:
ここまでで、API Gateway に対して「Datadog 形式でトレース情報をやりとりする」というルールの登録が完了したことになります。
3. HTTP Middleware の設定
ここでは、前項で設定しておいた Datadog 形式に対応した TextMap Propagator を使用して、HTTP リクエストのヘッダーからトレース情報を抽出(Extract)して、Go の context
に埋め込むための HTTP Middleware を実装します。
具体的には、以下の実装を HTTP Middleware として追加することで実現できます:
import (
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func OtelMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
propagator := otel.GetTextMapPropagator()
ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
これにより、図3における HTTP Client → HTTP Server A の Context Propagation を実現します。
4. HTTP ヘッダー → gRPC メタデータへの伝搬設定
grpc-gateway の場合、Datadog 形式の HTTP ヘッダーを gRPC メタデータに伝搬させるため、以下を設定する必要があります:
import (
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
)
mux := runtime.NewServeMux(
...
runtime.WithIncomingHeaderMatcher(func(key string) (string, bool) {
switch strings.ToLower(key) {
case "x-datadog-trace-id", "x-datadog-parent-id", "x-datadog-sampling-priority":
return key, true
default:
return runtime.DefaultHeaderMatcher(key)
}
})
)
これにより、図3における HTTP Server A → gRPC Server A の Context Propagation を実現します。
5. gRPC Server/Client の設定
gRPC のサーバー側とクライアント側でそれぞれ以下を設定することで、リクエストの受信時と送信時に、TextMap Propagator の Extract
メソッドと Inject
メソッドがそれぞれ実行されるようにします:
gRPC Server 側の設定
import (
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
grpcServer := grpc.NewServer(
...
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
gRPC Client 側の設定
import (
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
grpcConn, err := grpc.DialContext(
...
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
)
これにより、図3における gRPC Server A → gRPC Server B への Context Propagation を実現します。
Microservice(gRPC Server B)
図1において、Microservice は受信のみのため、前節の gRPC Server 側の設定のみをします。
アプリケーションで設定すべき項目は以上となります!
以上の設定がすべて完了すれば、Frontend、API Gateway、Microservice という異なるサービス間で適切に Context Propagation されるようになっており、図4のように分散トレーシングを実現できるようになっています:
図4. 今回実現した分散トレーシングの全体像
Datadog でログとトレースを確認する
ここまで、分散トレーシングを実現するためにアプリケーションでさまざまな下準備をしてきましたが、いよいよ Datadog でログとトレースがどのように表示されるかを確認していきましょう。
Datadog のトレースビューを開くと、図5のように表示されるようになっています!🎉:
図5. Datadog のトレースビュー
Frontend、API Gateway、Microservice という異なるサービスで出力されたエラーログが、Context Propagation されたトレースと適切に紐付いていることで、Datadog で一覧表示されるようになっています。
これによってリクエスト単位でエラーログを確認することが容易になり、デバッグ体験を向上させることができました!
おわりに
新規プロダクトの分散アーキテクチャにおいて、OpenTelemetry x Datadog で分散トレーシングを実現する方法についてご紹介しました。
我々のチームでは、分散トレーシングを実現するまでは、図7のように各サービスごとに独立してスタックトレースを出力している状態に留まっていました。そのため、各サービスごとにエラー単位で因果関係はわかっても、それが他のサービスのどのエラーと因果関係があるかまでを特定することは困難でした:
図7. 分散トレーシング実現前の状態(各サービスのログが独立している)
しかし、今回の分散トレーシングの実現によりログとトレースが紐付くようになった結果、図8のようにサービス横断でリクエスト単位のデバッグが可能となりました:
図8. 分散トレーシング実現後の状態(各サービスのログが紐付いている)
これにより、サービス全体としてリクエスト単位で何が起きたのかの経緯を追跡することが容易になり、オブザーバビリティを向上させることができました。
最後になりますが、本記事が OpenTelemetry x Datadog で分散トレーシングを実現する際に少しでもお役に立てば幸いです。
Views: 0