OpenTelemetry を使用した Go Web アプリケーションにおけるデータベースおよび HTTP クライアント呼び出しのトレース
James Reed
Infrastructure Engineer · Leapcell

Go Web アプリケーションのデータベースおよび HTTP クライアント呼び出しのトレース
今日の複雑な分散システムでは、リクエストの流れを理解し、パフォーマンスのボトルネックを特定することが不可欠です。データベースや外部 HTTP サービスとやり取りすることが多い Go Web アプリケーションも例外ではありません。適切なオブザーバビリティなしでは、問題のデバッグは時間のかかる、フラストレーションのたまる作業となり、平均解決時間 (MTTR) の増加や顧客満足度の低下につながる可能性があります。ここで分散トレーシングが登場します。アプリケーションを計装することで、リクエストがさまざまなコンポーネントを通過する際のライフタイムに関する貴重な洞察を得ることができます。この記事では、Go Web アプリケーションに OpenTelemetry を手動で統合して、データベースクエリと HTTP クライアントの呼び出しをトレースし、アプリケーションの動作を詳細に表示する実践的な側面について説明します。
分散トレーシングの柱を理解する
コーディングに入る前に、トレーシングの取り組みの基礎となる OpenTelemetry のコアコンセプトについて共通の理解を深めましょう。
- トレース (Trace): トレースは、分散システム内の単一のエンドツーエンドのトランザクションまたはリクエストを表します。これは、全体的な操作を collectively に記述する、因果関係のあるスパン (span) のコレクションです。
- スパン (Span): スパンは、トレース内の単一の原子操作です。これは、着信 HTTP リクエスト、データベースクエリ、または発信 API 呼び出しなどの作業単位を表します。各スパンには、名前、開始時刻、終了時刻、およびコンテキスト情報を提供する属性があります。スパンはネストして、親子関係を形成できます。
- トレーサー (Tracer): トレーサーは、スパンを作成するために使用されるインターフェースです。通常、
TracerProvider
から取得されます。 - トレーサープロバイダー (TracerProvider):
TracerProvider
はTracer
インスタンスを管理および構成します。スパンプロセッサやエクスポート処理など、トレーシングパイプラインの設定を担当します。 - スパンプロセッサ (Span Processor):
SpanProcessor
は、スパンがエクスポートされる前に処理できるようにするインターフェースです。サンプリング、追加の属性でのスパンのエンリッチ、またはスパンのバッファリングなどのタスクを実行できます。 - エクスポート (Exporter):
Exporter
は、収集されたテレメトリデータ(スパン、メトリクス、ログ)を Jaeger、Zipkin、または OpenTelemetry Collector などのバックエンドシステムに送信します。 - コンテキスト伝搬 (Context Propagation): トレースコンテキスト(トレース ID とスパン ID を含む)がサービス境界を越えて渡されるメカニズムです。これは、スパンをリンクして完全なトレースを形成するために重要です。HTTP では、これは通常、リクエストヘッダーを介して行われます。
トレーシングの原則は、各重要な操作に対して新しいスパンを作成し、コンテキスト伝搬を使用して親スパンにリンクすることです。これにより、リクエストフローの詳細なタイムラインを提供するスパンの有向非巡回グラフ (DAG) が形成されます。
データベースおよび HTTP クライアントのトレースのための OpenTelemetry の手動統合
私たちの目標は、Go Web アプリケーションを手動で計装して、データベース(例:PostgreSQL)および外部 HTTP リクエストへの呼び出しをトレースすることです。基本的な OpenTelemetry パイプラインを設定して、トレースをローカルの OpenTelemetry Collector にエクスポートし、その後、Jaeger のようなトレーシングバックエンドに転送できるようにします。
デモンストレーション目的で、Docker Compose を使用して OpenTelemetry Collector と Jaeger をローカルで設定することから始めましょう。
# docker-compose.yaml version: '3.8' services: jaeger: image: jaegertracing/all-in-one:1.35 ports: - "6831:6831/udp" # Agent UDP - "16686:16686" # UI - "14268:14268" # Collector HTTP environment: COLLECTOR_ZIPKIN_HOST_PORT: 9411 COLLECTOR_OTLP_ENABLED: true otel-collector: image: otel/opentelemetry-collector:0.86.0 command: ["--config=/etc/otel-collector-config.yaml"] volumes: - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml ports: - "4317:4317" # OTLP gRPC receiver - "4318:4318" # OTLP HTTP receiver depends_on: - jaeger
および OpenTelemetry Collector の構成:
# otel-collector-config.yaml receivers: otlp: protocols: grpc: http: exporters: jaeger: endpoint: jaeger:14250 tls: insecure: true service: pipelines: traces: receivers: [otlp] exporters: [jaeger]
これらを docker-compose up -d
で実行します。その後、Jaeger UI に http://localhost:16686
でアクセスできます。
次に、Go アプリケーションを作成しましょう。この例では、PostgreSQL データベースからデータを取得し、その後外部 HTTP 呼び出しを行うシンプルな HTTP サーバーを特徴とします。
まず、OpenTelemetry TracerProvider
を初期化し、スパンをエクスポートするように構成する必要があります。
package main import ( "context" "fmt" "log" "net/http" "time" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var tracer = otel.Tracer("my-go-web-app") // InitTracerProvider initializes the OpenTelemetry TracerProvider func InitTracerProvider() *sdktrace.TracerProvider { ctx := context.Background() // Configure the OTLP exporter to send traces to the collector conn, err := grpc.DialContext(ctx, "otel-collector:4317", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), ) if err != nil { log.Fatalf("failed to dial gRPC: %v", err) } exporter, err := otlptrace.New( ctx, otlptracegrpc.WithGRPCConn(conn), ) if err != nil { log.Fatalf("failed to create OTLP trace exporter: %v", err) } // Create a new trace processor that will export spans bsp := sdktrace.NewBatchSpanProcessor(exporter) // Define the resource that identifies this service res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceNameKey.String("go-web-app"), semconv.ServiceVersionKey.String("1.0.0"), ), ) if err != nil { log.Fatalf("failed to create resource: %v", err) } // Create a new TracerProvider tp := sdktrace.NewTracerProvider( sdktrace.WithBatchSpanProcessor(bsp), sdktrace.WithResource(res), ) // Register the global TracerProvider otel.SetTracerProvider(tp) return tp } // simulateDBQuery simulates a database query. func simulateDBQuery(ctx context.Context, userID string) (string, error) { // Create a new span for the database query ctx, span := tracer.Start(ctx, "db.get_user_data", trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes( attribute.String("db.system", "postgresql"), attribute.String("db.statement", "SELECT * FROM users WHERE id = $1"), attribute.String("db.user_id", userID), attribute.String("database.connection_string", "user=postgres password=password dbname=test host=localhost port=5432 sslmode=disable"), ), ) defer span.End() // Simulate database connection and query execution conn, err := pgx.Connect(ctx, "postgresql://postgres:password@localhost:5433/test?sslmode=disable") if err != nil { span.RecordError(err) span.SetStatus(trace.StatusError, "Failed to connect to database") return "", fmt.Errorf("failed to connect to database: %w", err) } defer conn.Close(ctx) // Simulate a query delay time.Sleep(100 * time.Millisecond) var username string err = conn.QueryRow(ctx, "SELECT username FROM users WHERE id = $1", userID).Scan(&username) if err != nil { span.RecordError(err) if err == pgx.ErrNoRows { span.SetStatus(trace.StatusError, "User not found") return "", fmt.Errorf("user %s not found", userID) } span.SetStatus(trace.StatusError, "Failed to query database") return "", fmt.Errorf("failed to query database: %w", err) } span.SetStatus(trace.StatusOK, "Successfully retrieved user data") return username, nil } // makeExternalAPIRequest simulates an HTTP client call to an external service. func makeExternalAPIRequest(ctx context.Context, endpoint string) (string, error) { // Create a new span for the HTTP client call ctx, span := tracer.Start(ctx, fmt.Sprintf("http.client.get_%s", endpoint), trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes( attribute.String("http.method", "GET"), attribute.String("http.url", endpoint), ), ) defer span.End() req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) if err != nil { span.RecordError(err) span.SetStatus(trace.StatusError, "Failed to create HTTP request") return "", fmt.Errorf("failed to create HTTP request: %w", err) } // Propagate trace context to the outgoing request otel.GetTextMapPropagator().Inject(ctx, otel.HeaderCarrier(req.Header)) client := http.DefaultClient resp, err := client.Do(req) if err != nil { span.RecordError(err) span.SetStatus(trace.StatusError, "Failed to make HTTP request") return "", fmt.Errorf("failed to make HTTP request: %w", err) } defer resp.Body.Close() span.SetAttributes( attribute.Int("http.status_code", resp.StatusCode), ) if resp.StatusCode != http.StatusOK { span.SetStatus(trace.StatusError, fmt.Sprintf("HTTP request failed with status: %d", resp.StatusCode)) return "", fmt.Errorf("external API call failed with status: %d", resp.StatusCode) } // Simulate processing the response time.Sleep(50 * time.Millisecond) span.SetStatus(trace.StatusOK, "Successfully made external API request") return "External API response for: " + endpoint, nil } func main() { // Initialize OpenTelemetry TracerProvider tp := InitTracerProvider() defer func() { if err := tp.Shutdown(context.Background()); err != nil { log.Printf("Error shutting down tracer provider: %v", err) } }() // Database setup (for demonstration) // You need to have a PostgreSQL instance running, // e.g., with a Docker container: // docker run --name some-postgres -p 5433:5432 -e POSTGRES_PASSWORD=password -d postgres // Then connect and create a 'users' table: // CREATE TABLE users (id VARCHAR(255) PRIMARY KEY, username VARCHAR(255)); // INSERT INTO users (id, username) VALUES ('123', 'john_doe'); // INSERT INTO users (id, username) VALUES ('456', 'jane_doe'); router := gin.Default() router.Use(otelgin.Middleware("my-go-web-app")) // Use Gin OpenTelemetry middleware for incoming requests router.GET("/user/:id", func(c *gin.Context) { ctx := c.Request.Context() // Get context from Gin, which already contains the trace context userID := c.Param("id") // Trace database call username, err := simulateDBQuery(ctx, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Trace an external HTTP client call externalAPIResponse, err := makeExternalAPIRequest(ctx, "http://jsonplaceholder.typicode.com/todos/1") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("User %s found: %s", userID, username), "external_api_result": externalAPIResponse, }) }) log.Fatal(router.Run(":8080")) // Listen and serve on 0.0.0.0:8080 }
この例を実行するには、以下の Go モジュールが必要です。
go get github.com/gin-gonic/gin go get go.opentelemetry.io/otel go get go.opentelemetry.io/otel/attribute go get go.opentelemetry.io/otel/exporters/otlp/otlptrace go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go get go.opentelemetry.io/otel/sdk/resource go get go.opentelemetry.io/otel/sdk/trace go get go.opentelemetry.io/otel/semconv/v1.21.0 go get google.golang.org/grpc go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin go get github.com/jackc/pgx/v5
コードの説明:
-
InitTracerProvider()
:- この関数は OpenTelemetry トレーシングパイプラインをセットアップします。
- OpenTelemetry Collector の
otel-collector:4317
に接続する gRPC クライアントを作成します。 otlptracegrpc.New()
は、gRPC を介してスパンを送信する OTLP トレースエクスポートを作成します。sdktrace.NewBatchSpanProcessor()
はスパンをバッファリングし、バッチで送信してパフォーマンスを向上させます。ServiceNameKey
とServiceVersionKey
を持つresource
が定義され、トレーシングバックエンドでアプリケーションを識別します。sdktrace.NewTracerProvider()
は、設定されたプロセッサとリソースでTracerProvider
を作成します。otel.SetTracerProvider(tp)
は、このプロバイダーをグローバルに登録し、アプリケーションの他の部分がTracer
を取得できるようにします。
-
main()
関数:- 最初に
InitTracerProvider()
を呼び出し、終了時にtp.Shutdown()
が呼び出されて、バッファリングされたスパンがすべてフラッシュされることを保証します。 - Gin 統合:
router.Use(otelgin.Middleware("my-go-web-app"))
は、OpenTelemetry Gin ミドルウェアを利用します。このミドルウェアは、各着信 HTTP リクエストのルートスパンを自動的に作成し、リクエストヘッダーからトレースコンテキストを抽出し(存在する場合)、コンテキストを Gin のRequest.Context()
に注入します。これは HTTP リクエスト間のコンテキスト伝搬に不可欠です。 tracer
変数はotel.Tracer("my-go-web-app")
を使用して取得され、グローバルに設定されたTracerProvider
を利用します。
- 最初に
-
simulateDBQuery()
関数:tracer.Start(ctx, "db.get_user_data", ...)
は、データベース操作のために新しいスパンを手動で作成します。trace.WithSpanKind(trace.SpanKindClient)
は、このスパンがクライアントサイドの呼び出し(データベースのクライアントとして機能するアプリケーション)を表していることを示します。trace.WithAttributes(...)
は、データベース呼び出しに関する豊富なコンテキスト(システム、ステートメント、ユーザー ID)を提供するセマンティック規約とカスタム属性をスパンに追加します。これは Jaeger でトレースをフィルタリングおよび分析するために不可欠です。defer span.End()
は、スパンが常に終了し、その期間を記録することを保証します。- エラー処理には、
span.RecordError(err)
およびspan.SetStatus(trace.StatusError, ...)
が含まれ、トレース内のエラーを示します。
-
makeExternalAPIRequest()
関数:- データベース関数と同様に、HTTP クライアント呼び出しのために新しいスパンが作成されます。
otel.GetTextMapPropagator().Inject(ctx, otel.HeaderCarrier(req.Header))
は、分散コンテキスト伝搬のための重要なステップです。現在のトレースコンテキストをctx
から取得し、それを発信 HTTP リクエストヘッダー(例:traceparent
)に注入します。外部サービスも OpenTelemetry で計装されている場合、このコンテキストを抽出し、トレースを継続して、単一のエンドツーエンドトレースを形成します。http.method
やhttp.url
などの属性は、HTTP リクエストの詳細を提供します。レスポンスからの HTTP ステータスコードも属性として追加されます。
トレースの観察
docker-compose up -d
を実行して Go アプリケーションを起動した後:
- アプリケーションにリクエストを送信します:
curl http://localhost:8080/user/123
。 http://localhost:16686
で Jaeger UI を開きます。- サービスドロップダウンから「go-web-app」を選択し、「Find Traces」をクリックします。
以下のようなトレースが表示されるはずです。
- GET /user/
(Gin HTTP Server Span - otelgin.Middleware
から)- db.get_user_data (手動で作成したデータベーススパン)
- http.client.get_http://jsonplaceholder.typicode.com/todos/1 (手動で作成した HTTP クライアントスパン)
各スパンには、設定した属性を含む詳細な属性があり、リクエスト中に何が起こったのかを明確に示します。各操作の期間を検査し、潜在的なボトルネックを特定できます。
結論
Go Web アプリケーションでデータベースおよび HTTP クライアントの呼び出しをトレースするために OpenTelemetry を手動で統合すると、システム動作の詳細な可視性が得られます。トレース、スパン、コンテキスト伝搬などのコアコンセプトを理解し、OpenTelemetry SDK を使用して慎重に計装することで、堅牢なオブザーバビリティフレームワークを構築できます。このアプローチにより、開発者はパフォーマンスの問題を効率的に診断し、分散サービス内の複雑な対話を理解できるようになり、最終的にはより安定したパフォーマンスの高いアプリケーションにつながります。OpenTelemetry を採用することは、包括的なシステムオブザーバビリティを実現するための重要なステップです。