Go OpenTelemetry分散トレーシング:ゼロからフルオブザーバビリティまでの6つのキーステップ

DevOps

マイクロサービスコールチェーンがブラックボックスになった

ユーザーが「注文が遅い」と報告し、ログを開くと、十数のサービスに散らばったタイムスタンプが見える——注文サービス3ms、在庫サービス2ms、決済サービス……タイムアウト?それとも呼び出されていない?リクエストがどのサービスを経由し、どこで詰まったのか全く分からない。分散トレーシングがこの問題の銀の弾丸であり、OpenTelemetry(OTel)は事実上の標準となっています。

本記事はゼロから出発し、OTel SDK初期化→Trace/Span作成→コンテキスト伝播→自動計装→Jaeger/Tempo統合→メトリクス相関の6つのキーステップを完了し、マイクロサービスコールチェーンをブラックボックスから透明なパイプラインに変えます。


OpenTelemetryコア概念

概念 説明
Trace 完全なリクエストのトレースチェーン、複数のSpanで構成
Span 単一操作ユニット、名前・所要時間・ステータス・属性を含む
Context トレースコンテキスト、TraceID/SpanIDを含み、プロセス間で伝播
Propagator コンテキスト伝播器、HTTP/gRPCヘッダーにContextを注入・抽出
TracerProvider Tracerファクトリ、Tracerインスタンスの作成と管理
SpanProcessor Spanプロセッサ、Spanのバッチ処理・フィルタリング・エクスポート
Exporter エクスポーター、SpanデータをJaeger/Tempo/OTLPバックエンドに送信
Resource リソース記述子、テレメトリデータを生成するサービスを識別

トレースデータフロー

リクエストフロー:
1. エントリサービスがリクエストを受信、Root Spanを作成
2. 下流サービス呼び出し時、PropagatorがContextをHTTP/gRPCヘッダーに注入
3. 下流サービスがヘッダーからContextを抽出、Child Spanを作成
4. Span完了後、SpanProcessorがバッチ処理
5. ExporterがSpanデータをJaeger/Tempoに送信
6. UIで完全なコールチェーングラフを表示

問題分析:分散トレーシング導入の5つの課題

  1. SDK初期化の複雑さ:TracerProvider、SpanProcessor、Exporter、Resourceの設定順序と依存関係が混同しやすい
  2. コンテキスト伝播の漏れ:クロスサービス呼び出しでContextの伝播を忘れ、チェーンが切断
  3. Span粒度の制御不能:粗すぎるとボトルネックが見えず、細すぎると大量データでバックエンドが圧迫
  4. 自動計装と手動計装の衝突:HTTP/gRPC自動計装とビジネス手動Spanが重複やネストエラー
  5. メトリクスとトレースの分断:MetricsとTracesが独立して動作し、メトリクスから特定のTraceを特定できない

ステップバイステップ:完全OTel導入

Step 1:TracerProvider初期化

package telemetry

import (
	"context"
	"fmt"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

type Telemetry struct {
	provider *sdktrace.TracerProvider
}

func InitTelemetry(ctx context.Context, serviceName, serviceVersion, otlpEndpoint string) (*Telemetry, error) {
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint(otlpEndpoint),
		otlptracegrpc.WithInsecure(),
	)
	if err != nil {
		return nil, fmt.Errorf("create OTLP exporter: %w", err)
	}

	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String(serviceName),
			semconv.ServiceVersionKey.String(serviceVersion),
		),
	)
	if err != nil {
		return nil, fmt.Errorf("create resource: %w", err)
	}

	bsp := sdktrace.NewBatchSpanProcessor(exporter,
		sdktrace.WithBatchTimeout(5*time.Second),
		sdktrace.WithMaxExportBatchSize(512),
		sdktrace.WithMaxQueueSize(2048),
	)

	provider := sdktrace.NewTracerProvider(
		sdktrace.WithResource(res),
		sdktrace.WithSpanProcessor(bsp),
		sdktrace.WithSampler(sdktrace.ParentBased(
			sdktrace.TraceIDRatioBased(0.5),
		)),
	)

	otel.SetTracerProvider(provider)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))

	return &Telemetry{provider: provider}, nil
}

func (t *Telemetry) Shutdown(ctx context.Context) error {
	return t.provider.Shutdown(ctx)
}

Step 2:TraceとSpanの作成

package service

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
)

var tracer = otel.Tracer("order-service")

func ProcessOrder(ctx context.Context, orderID string) error {
	ctx, span := tracer.Start(ctx, "ProcessOrder",
		trace.WithAttributes(
			attribute.String("order.id", orderID),
		),
		trace.WithSpanKind(trace.SpanKindInternal),
	)
	defer span.End()

	if err := validateOrder(ctx, orderID); err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())
		return err
	}

	if err := reserveInventory(ctx, orderID); err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())
		return err
	}

	span.SetStatus(codes.Ok, "")
	return nil
}

func validateOrder(ctx context.Context, orderID string) error {
	ctx, span := tracer.Start(ctx, "validateOrder",
		trace.WithAttributes(attribute.String("order.id", orderID)),
	)
	defer span.End()

	if orderID == "" {
		err := fmt.Errorf("order ID is empty")
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())
		return err
	}

	span.AddEvent("validation_passed", trace.WithAttributes(
		attribute.String("order.id", orderID),
	))
	return nil
}

func reserveInventory(ctx context.Context, orderID string) error {
	ctx, span := tracer.Start(ctx, "reserveInventory")
	defer span.End()

	span.SetAttributes(attribute.String("order.id", orderID))
	return nil
}

Step 3:HTTPコンテキスト伝播

package middleware

import (
	"net/http"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
)

func HTTPMiddleware(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))

		tracer := otel.Tracer("http-server")
		spanName := r.Method + " " + r.URL.Path
		ctx, span := tracer.Start(ctx, spanName,
			trace.WithSpanKind(trace.SpanKindServer),
			trace.WithAttributes(
				attribute.String("http.method", r.Method),
				attribute.String("http.url", r.URL.String()),
				attribute.String("http.host", r.Host),
			),
		)
		defer span.End()

		rw := &responseWriter{ResponseWriter: w, statusCode: 200}
		next.ServeHTTP(rw, r.WithContext(ctx))

		span.SetAttributes(
			attribute.Int("http.status_code", rw.statusCode),
		)
	})
}

type responseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

Step 4:アウトバウンドHTTPコンテキスト伝播

package client

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
)

type InstrumentedClient struct {
	client *http.Client
}

func NewInstrumentedClient() *InstrumentedClient {
	return &InstrumentedClient{
		client: &http.Client{Timeout: 30 * time.Second},
	}
}

func (c *InstrumentedClient) Do(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) {
	tracer := otel.Tracer("http-client")
	ctx, span := tracer.Start(ctx, method+" "+url,
		trace.WithSpanKind(trace.SpanKindClient),
		trace.WithAttributes(
			attribute.String("http.method", method),
			attribute.String("http.url", url),
		),
	)
	defer span.End()

	req, err := http.NewRequestWithContext(ctx, method, url, body)
	if err != nil {
		span.RecordError(err)
		return nil, fmt.Errorf("create request: %w", err)
	}

	propagator := otel.GetTextMapPropagator()
	propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))

	resp, err := c.client.Do(req)
	if err != nil {
		span.RecordError(err)
		return nil, fmt.Errorf("execute request: %w", err)
	}

	span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode))
	return resp, nil
}

Step 5:gRPC自動計装

package main

import (
	"context"

	"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func NewGRPCClient(ctx context.Context, target string) (*grpc.ClientConn, error) {
	conn, err := grpc.DialContext(ctx, target,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
	)
	if err != nil {
		return nil, err
	}
	return conn, nil
}

func NewGRPCServer() *grpc.Server {
	server := grpc.NewServer(
		grpc.StatsHandler(otelgrpc.NewServerHandler()),
	)
	return server
}

Step 6:メトリクスとトレースの相関

package telemetry

import (
	"context"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
	"go.opentelemetry.io/otel/sdk/resource"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

type MetricsProvider struct {
	provider *sdkmetric.MeterProvider
}

func InitMetrics(ctx context.Context, serviceName, otlpEndpoint string) (*MetricsProvider, error) {
	exporter, err := otlpmetricgrpc.New(ctx,
		otlpmetricgrpc.WithEndpoint(otlpEndpoint),
		otlpmetricgrpc.WithInsecure(),
	)
	if err != nil {
		return nil, err
	}

	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String(serviceName),
		),
	)
	if err != nil {
		return nil, err
	}

	provider := sdkmetric.NewMeterProvider(
		sdkmetric.WithResource(res),
		sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter)),
	)

	otel.SetMeterProvider(provider)
	return &MetricsProvider{provider: provider}, nil
}

func (m *MetricsProvider) Shutdown(ctx context.Context) error {
	return m.provider.Shutdown(ctx)
}

落とし穴ガイド

落とし穴1:グローバルPropagatorの設定忘れ

// ❌ 誤り:Propagator未設定、Contextがプロセス間で伝播できない
provider := sdktrace.NewTracerProvider(...)
otel.SetTracerProvider(provider)
// otel.SetTextMapPropagator(...) が欠落

// ✅ 正しい:Composite Propagatorを設定
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
    propagation.TraceContext{},
    propagation.Baggage{},
))

落とし穴2:Span.End()の呼び出し忘れ

// ❌ 誤り:Spanが終了せず、エクスポートされない
ctx, span := tracer.Start(ctx, "operation")
doWork(ctx)
// span.End() 忘れ

// ✅ 正しい:deferでSpan終了を保証
ctx, span := tracer.Start(ctx, "operation")
defer span.End()
doWork(ctx)

落とし穴3:不適切なサンプリングレート

// ❌ 誤り:本番でAlwaysSampleは大量データを生成
sdktrace.WithSampler(sdktrace.AlwaysSample())

// ✅ 正しい:ParentBased + TraceIDRatioBasedを使用
sdktrace.WithSampler(sdktrace.ParentBased(
    sdktrace.TraceIDRatioBased(0.1), // 10%サンプリング
))

落とし穴4:ゴルーチンでContextが失われる

// ❌ 誤り:ゴルーチンにctxが渡されていない
go func() {
    ctx, span := tracer.Start(context.Background(), "async_work")
    defer span.End()
}()

// ✅ 正しい:親Contextをゴルーチンに渡す
go func(ctx context.Context) {
    ctx, span := tracer.Start(ctx, "async_work")
    defer span.End()
}(ctx)

落とし穴5:Shutdownタイムアウトでデータ損失

// ❌ 誤り:Shutdownに十分な時間がない
func main() {
    tel, _ := telemetry.InitTelemetry(ctx, "svc", "1.0", "localhost:4317")
    defer tel.Shutdown(context.Background()) // タイムアウトの可能性
}

// ✅ 正しい:Shutdownに十分なタイムアウトを設定
func main() {
    tel, _ := telemetry.InitTelemetry(ctx, "svc", "1.0", "localhost:4317")
    defer func() {
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        tel.Shutdown(shutdownCtx)
    }()
}

エラートラブルシューティング

# エラーメッセージ 原因 解決方法
1 connection refused: localhost:4317 OTLP Collector未起動 otel-collectorコンテナを起動、ポートマッピング確認
2 traces not showing in Jaeger Exporter設定エラーまたはサンプリング率0 Exporterターゲット確認、サンプリング率>0を確認
3 context deadline exceeded Collector応答遅延またはネットワーク不通 タイムアウト増加、ネットワーク接続確認
4 span missing parent コンテキスト伝播失敗 Propagator設定確認、HTTPヘッダー注入確認
5 resource attributes missing Resource未設定 resource.WithAttributes(semconv.ServiceNameKey.String(...))を追加
6 too many open files Spanキューバックログ、Exporteブロック MaxQueueSize削減、BatchTimeout増加
7 trace_id not found in baggage BaggageとTraceContextの混同 TraceContextはTraceIDを伝播、Baggageはビジネスデータを伝播
8 grpc: no transport security gRPCでWithInsecure使用 開発環境は許容、本番ではTLS設定
9 duplicate span name 同名Spanが複数あり混同 識別属性を追加または動的名を使用
10 metric reader timeout Metricエクスポートタイムアウト PeriodicReaderでIntervalとTimeoutを増加

高度な最適化

1. カスタムSpanProcessorによる機密データフィルタリング

package telemetry

import (
	"context"
	"strings"

	"go.opentelemetry.io/otel/attribute"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

type sanitizingProcessor struct {
	next          sdktrace.SpanProcessor
	sensitiveKeys []string
}

func NewSanitizingProcessor(next sdktrace.SpanProcessor, sensitiveKeys []string) sdktrace.SpanProcessor {
	return &sanitizingProcessor{next: next, sensitiveKeys: sensitiveKeys}
}

func (p *sanitizingProcessor) OnStart(ctx context.Context, s sdktrace.ReadWriteSpan) {
	p.next.OnStart(ctx, s)
}

func (p *sanitizingProcessor) OnEnd(s sdktrace.ReadOnlySpan) {
	attrs := s.Attributes()
	filtered := make([]attribute.KeyValue, 0, len(attrs))
	for _, attr := range attrs {
		if p.isSensitive(string(attr.Key)) {
			filtered = append(filtered, attribute.String(string(attr.Key), "[REDACTED]"))
		} else {
			filtered = append(filtered, attr)
		}
	}
	p.next.OnEnd(s)
}

func (p *sanitizingProcessor) isSensitive(key string) bool {
	for _, sk := range p.sensitiveKeys {
		if strings.Contains(strings.ToLower(key), strings.ToLower(sk)) {
			return true
		}
	}
	return false
}

func (p *sanitizingProcessor) ForceFlush(ctx context.Context) error {
	return p.next.ForceFlush(ctx)
}

func (p *sanitizingProcessor) Shutdown(ctx context.Context) error {
	return p.next.Shutdown(ctx)
}

2. エラーレートベースの動的サンプリング

package telemetry

import (
	"sync/atomic"

	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

type errorAwareSampler struct {
	errorCount atomic.Int64
	totalCount atomic.Int64
	baseRatio  float64
	errorRatio float64
}

func NewErrorAwareSampler(baseRatio, errorRatio float64) sdktrace.Sampler {
	return &errorAwareSampler{baseRatio: baseRatio, errorRatio: errorRatio}
}

func (s *errorAwareSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult {
	s.totalCount.Add(1)

	for _, attr := range p.Attributes {
		if attr.Key == "error" {
			s.errorCount.Add(1)
		}
	}

	ratio := s.baseRatio
	if s.errorCount.Load() > 0 {
		errorRate := float64(s.errorCount.Load()) / float64(s.totalCount.Load())
		if errorRate > 0.01 {
			ratio = s.errorRatio
		}
	}

	return sdktrace.TraceIDRatioBased(ratio).ShouldSample(p)
}

func (s *errorAwareSampler) Description() string {
	return "ErrorAwareSampler"
}

3. トレースとログの相関

package telemetry

import (
	"context"
	"log/slog"

	"go.opentelemetry.io/otel/trace"
)

type traceHandler struct {
	next slog.Handler
}

func NewTraceHandler(next slog.Handler) slog.Handler {
	return &traceHandler{next: next}
}

func (h *traceHandler) Handle(ctx context.Context, r slog.Record) error {
	spanCtx := trace.SpanContextFromContext(ctx)
	if spanCtx.IsValid() {
		r.AddAttrs(
			slog.String("trace_id", spanCtx.TraceID().String()),
			slog.String("span_id", spanCtx.SpanID().String()),
		)
	}
	return h.next.Handle(ctx, r)
}

func (h *traceHandler) Enabled(ctx context.Context, level slog.Level) bool {
	return h.next.Enabled(ctx, level)
}

func (h *traceHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	return &traceHandler{next: h.next.WithAttrs(attrs)}
}

func (h *traceHandler) WithGroup(name string) slog.Handler {
	return &traceHandler{next: h.next.WithGroup(name)}
}

比較分析

次元 OpenTelemetry Jaeger Client Zipkin Brave SkyWalking Datadog APM
ベンダー中立 ✅CNCF標準 ⚠️Jaegerのみ ⚠️Zipkinのみ ❌Apacheだがエコシステム閉鎖 ❌商用
多言語対応 ✅11+言語 ⚠️6言語 ⚠️Java中心 ⚠️8言語 ✅10+
メトリクス統合 ✅ネイティブ ❌Prometheus必要
自動計装 ✅HTTP/gRPC ⚠️限定的
サンプリング戦略 ✅柔軟 ⚠️シンプル ⚠️シンプル ❌固定
コミュニティ活発度 ⭐非常に高い ⭐高い ⭐中程度 ⭐高い ⭐商用
コスト 無料 無料 無料 無料 $31/月〜

まとめ:OpenTelemetryはもう一つのAPMツールではなく、オブザーバビリティのインフラストラクチャ層です。その核心的価値は、一度の計装でマルチバックエンドエクスポート、Trace/Metrics/Logsの三位一体。2026年のベストプラクティス:OTel SDKで統一計装→OTLPプロトコルでCollectorに送信→CollectorがJaeger(Trace)+Prometheus(Metrics)+Loki(Log)にルーティング。鍵はSDK初期化時にPropagatorとサンプリング戦略を適切に設定し、後のチェーン切断やデータフラッドを防ぐことです。


オンラインツール推奨

ブラウザローカルツールを無料で試す →

#Go#OpenTelemetry#分布式追踪#可观测性#链路追踪#2026#DevOps