GoマイクロサービスでOpenTelemetryを始める - ログ、メトリクス、トレースの基本と実践
はじめに
マイクロサービスを運用していて、こんな経験はありませんか?
- 「どのサービスでエラーが起きているかわからない」
- 「処理が遅いけど、どこがボトルネックかわからない」
- 「ログは出力しているけど、サービス間の連携が追えない」
これらの問題を解決するのが オブザーバビリティ(Observability) です。
先日、社内のマイクロサービスにOpenTelemetryを導入した際に学んだ、基本概念から実装まで共有します。
オブザーバビリティとは
オブザーバビリティとは、システムの内部状態を外部から観測する能力のことです。
「アプリケーションの中で何が起きているかを、外から見てわかるようにすること」と考えてください。
分散システムでは、以下の3つの柱で構成されます:
3つの柱
1. ログ
何が起きたかの記録です。
2025/09/19 21:21:23 INFO ユーザーログイン成功 user_id=123
2025/09/19 21:21:24 ERROR データベース接続失敗 error="connection timeout"
特徴:
- 時系列で記録される
- 構造化されたデータ(キー・バリュー形式)
- 詳細なコンテキスト情報を含む
用途: エラーの原因調査、動作確認
2. メトリクス
数値で測れるデータです。
CPU使用率: 75%
メモリ使用量: 2.1GB
リクエスト数: 1,234 req/sec
応答時間: 150ms
特徴:
- 定期的に収集される数値データ
- 時系列で蓄積される
- 集約・計算が可能
用途: パフォーマンス監視、アラート設定
3. トレース
1つのリクエストが複数のサービスをまたいで処理される全体の流れです。
例:ECサイトでの商品購入
TraceID: abc123
├── Span1: フロントエンド (10ms)
│ └── Span2: API Gateway (5ms)
│ └── Span3: 商品サービス (50ms)
│ └── Span4: データベース (30ms)
│ └── Span5: 在庫サービス (40ms)
│ └── Span6: データベース (25ms)
│ └── Span7: 決済サービス (200ms)
│ └── Span8: 外部API (180ms)
特徴:
- 1つのTraceIDで全体を追跡
- 各処理(Span)の開始・終了時間を記録
- サービス間の依存関係を可視化
用途:
- どこが遅いか(ボトルネック)の特定
- どこでエラーが起きたかの特定
- サービス間の呼び出し関係の把握
TraceIDとSpanID
TraceID
1つのリクエスト全体につける番号です。
例:ユーザーが「商品を購入」ボタンを押したとき
TraceID: abc123
このabc123
が、以下すべての処理で使われます:
商品情報取得 → 在庫確認 → 決済処理 → 在庫更新 → メール送信
abc123 abc123 abc123 abc123 abc123
SpanID
個別の処理につける番号です。
同じTraceIDの中で、処理ごとに違うSpanIDがつきます:
TraceID: abc123
├── SpanID: span1 (商品情報取得)
├── SpanID: span2 (在庫確認)
├── SpanID: span3 (決済処理)
└── SpanID: span4 (メール送信)
実際の実装
1. OpenTelemetry SDKのセットアップ
func main() {
ctx := context.Background()
// OpenTelemetry SDKの初期化
otelShutdown, err := setupOTelSDK(ctx, "health-reporter")
if err != nil {
slog.Error("Failed to set up OTel SDK", "error", err)
os.Exit(1)
}
defer otelShutdown(context.Background())
// アプリケーションのメイン処理
startApplication(ctx)
}
2. トレーサーとスパンの作成
func startApplication(ctx context.Context) {
// トレーサーを取得
tracer := otel.Tracer("health-reporter")
// スパンを開始
ctx, span := tracer.Start(ctx, "health-reporter-worker")
defer span.End()
// TraceIDをログに出力
traceID := span.SpanContext().TraceID().String()
slog.Info("Starting health reporter...", "trace_id", traceID)
// 処理を実行
if err := processHealthCheck(ctx); err != nil {
slog.Error("Worker failed", "error", err, "trace_id", traceID)
return
}
slog.Info("Health reporter completed", "trace_id", traceID)
}
3. 実際のログ出力
上記のコードを実行すると、以下のようなログが出力されます:
2025/09/19 21:21:23 INFO Starting health reporter... trace_id=267e3466a645f6129abffdbf4c43add2 # ← TraceID開始
2025/09/19 21:21:23 INFO Connected to service addr=service:9000
2025/09/19 21:21:23 INFO Health report sent successfully device_id=01234567-89ab-cdef-0123-456789abcdef ip=192.168.0.9
2025/09/19 21:21:23 INFO Health reporter completed trace_id=267e3466a645f6129abffdbf4c43add2 # ← 同じTraceID
OpenTelemetryコレクターでの確認
OpenTelemetryコレクターのログを見ると、テレメトリーデータが正しく送信されていることがわかります:
# ログデータ
LogRecord #1
Body: Str(Starting health reporter...)
Attributes:
-> service: Str(health-reporter)
-> environment: Str(development)
-> trace_id: Str(267e3466a645f6129abffdbf4c43add2)
Trace ID: 267e3466a645f6129abffdbf4c43add2
Span ID: 7f5aa898380ff08c
実装時に遭遇した問題
slogとOpenTelemetryの統合
実装中に気づいたのが、手動でtrace_id
をログ属性に追加しても、OpenTelemetryのTrace ID
フィールドには反映されないことでした。
// これだと属性としてのみ記録される
slog.Info("message", "trace_id", traceID)
// ↓ OpenTelemetryコレクターのログ
// Attributes:
// -> trace_id: Str(267e3466a645f6129abffdbf4c43add2)
// Trace ID: (空) ← ここが空になる
これは、slogがOpenTelemetryのコンテキストを自動伝播しないためです。完全な統合には、otelslogブリッジなどの使用が推奨されます。
マイクロサービスでの設計
複数のサービスで構成されるシステムでは、OpenTelemetryコレクターを分離することも重要です。
なぜコレクターを分離するのか
1. セキュリティの境界
- エッジデバイス:外部ネットワークからの制限、軽量な処理が必要
- 管理システム:内部ネットワークで高度な分析処理を実行
2. 処理要件の違い
- エッジ側:必要最小限のメトリクス収集、ローカル前処理
- 管理側:複雑な集約処理、長期保存、アラート設定
3. ネットワーク分離
- それぞれの環境に適したコレクター設定
- データの流れを明確に分離
実際の設定例
# Docker Compose例
services:
# エッジデバイスサイドのコレクター
edge-device-otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-collector/edge-device-config.yaml:/etc/config.yaml
networks:
- edge-device-network
# 管理サイドのコレクター
management-otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-collector/management-config.yaml:/etc/config.yaml
networks:
- management-network
まとめ
OpenTelemetryを導入することで:
- TraceIDでリクエスト全体を追跡できる
- SpanIDで個別の処理を特定できる
- ログ、メトリクス、トレースを統合的に管理できる
- マイクロサービス間の連携が可視化される
Discussion