📊

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を導入することで:

  1. TraceIDでリクエスト全体を追跡できる
  2. SpanIDで個別の処理を特定できる
  3. ログ、メトリクス、トレースを統合的に管理できる
  4. マイクロサービス間の連携が可視化される

参考

ispec inc.

Discussion