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つの課題
- SDK初期化の複雑さ:TracerProvider、SpanProcessor、Exporter、Resourceの設定順序と依存関係が混同しやすい
- コンテキスト伝播の漏れ:クロスサービス呼び出しでContextの伝播を忘れ、チェーンが切断
- Span粒度の制御不能:粗すぎるとボトルネックが見えず、細すぎると大量データでバックエンドが圧迫
- 自動計装と手動計装の衝突:HTTP/gRPC自動計装とビジネス手動Spanが重複やネストエラー
- メトリクスとトレースの分断: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とサンプリング戦略を適切に設定し、後のチェーン切断やデータフラッドを防ぐことです。
オンラインツール推奨
- JSONフォーマッター:/ja/json/format
- Base64エンコード/デコード:/ja/encode/base64
- Hash計算:/ja/encode/hash
- JWTデコード:/ja/encode/jwt-decode
ブラウザローカルツールを無料で試す →
#Go#OpenTelemetry#分布式追踪#可观测性#链路追踪#2026#DevOps