Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OpenTelemetry 実践解説書 アウトライン

OpenTelemetry は、クラウドネイティブ環境やマイクロサービスにおいて、システムの動作状況を観測・分析するための標準規格およびツール群です。分散トレーシング、メトリクス収集、ログ管理といった「オブザーバビリティ(可観測性)」を一元化されたプロトコル(OTLP)で実現します。

本書では、OpenTelemetry の基礎概念から実践的なシステム構築までをわかりやすく解説します。プログラム言語には Python と C# を使用します。また、最小構成である「OpenTelemetry SDK と Jaeger v2 の直接連携」から、実運用に即した「Collector、Jaeger v2、Prometheus、Grafana を組み合わせたフルスタック構成」まで、コンテナ環境(Docker Compose)を用いた具体的な構築手順と動作コードを提供します。

章構成

第1章 OpenTelemetry の基礎概念とアーキテクチャ

本章では、OpenTelemetry が生まれた背景、オブザーバビリティ(可観測性)の考え方、3大シグナルの特徴、そして標準プロトコルである OTLP と OpenTelemetry Collector の仕組みを学びます。

オブザーバビリティ(可観測性)とは何か

システムが正しく動いているか、どこで問題が起きているかを把握する能力をオブザーバビリティと呼びます。

昔ながらのモノリシック(単一構成)なシステムでは、1台のサーバーが出力するログファイルを検索したり、CPU使用率を監視したりすることで問題を調査できました。しかし、現代のマイクロサービスやクラウド環境では、1つのユーザー操作に対して数十個以上の独立したサービスが通信し合って処理を完了します。

このような分散システムで障害や遅延が発生したとき、「どのサービスが原因か」「どの通信で時間がかかったのか」を突き止めることは非常に困難です。システムが出力する外部データから、内部の状態や原因を正確に理解できるようにする取り組みがオブザーバビリティです。

3大シグナル(トレース・メトリクス・ログ)

OpenTelemetry では、システムの状況を把握するためのデータを「シグナル」と呼びます。主要なシグナルは以下の 3 つです。

  • トレース(Traces)

    1 回のリクエストがシステム全体をどのように通り抜けたかを追跡するデータです。処理全体のまとまりを「トレース」、各サービス内で行われた個々の作業単位を「スパン(Span)」と呼びます。スパンには開始時刻、終了時刻、実行した処理名、属性情報(HTTP メソッドやユーザー ID など)が含まれます。これにより、処理の遅延箇所を一目で特定できます。

  • メトリクス(Metrics)

    システムの健康状態や処理量を数値として定期的に集計したデータです。リクエスト総数を数える「カウンター(Counter)」や、処理時間の分布を記録する「ヒストグラム(Histogram)」、現在のメモリ使用量を示す「ゲージ(Gauge)」などがあります。グラフ化することで異常な変化を素早く検知できます。

  • ログ(Logs)

    ある時点で発生した特定のイベントやエラーの詳細な記録です。タイムスタンプとメッセージ、エラーのスタックトレースなどが記録されます。トレースと連携させることで、「エラーが発生したときのスパン」に紐づく詳細ログを素早く確認できます。

OTLP(OpenTelemetry Protocol)の役割

OTLP は、トレース・メトリクス・ログを統一された形式で送受信するためのオープン標準プロトコルです。

従来の監視ツールは、ツールごとに専用のエージェントや通信形式が必要でした。そのため、監視ツールを別のサービスへ移行する際には、アプリケーションコードの書き換えが必要になるという「ベンダーロックイン」が発生していました。

OpenTelemetry では、OTLP という共通プロトコルを使用します。アプリケーションは OTLP 形式でデータを出力するだけで済み、データの保存先や可視化ツールを後から自由に変更できます。

OTLP は通信方式として主に以下の 2 つをサポートします。

  • gRPC(デフォルトポート: 4317)

    HTTP/2 と Protocol Buffers をベースにした高速・軽量なバイナリ通信です。常時接続による低オーバーヘッドが特徴で、バックエンド間の通信や高速処理が求められる環境に適しています。

  • HTTP/protobuf または HTTP/json(デフォルトポート: 4318)

    標準的な HTTP/1.1 または HTTP/2 を用いた通信です。ファイアウォールの制限がある環境や、gRPC を直接扱いにくい環境でも容易に利用できます。

SDK と Collector の役割分担

OpenTelemetry を利用するアーキテクチャには、「SDK 直接送信」と「Collector 経由送信」の 2 通りがあります。

  • OpenTelemetry SDK(直接送信)

    アプリケーションコード内にライブラリとして組み込みます。コード内の処理時間や数値を計測し、直接 Jaeger などのバックエンドへ OTLP で送信します。追加のサーバーを用意する必要がないため、開発環境や小規模なツール検証に最適です。

  • OpenTelemetry Collector(中継プロキシ)

    アプリケーションと可視化ツールの間に配置する中継サーバーです。アプリケーションからテレメトリデータを受信し、不要な情報の除外や加工、サンプリングを行った上で、複数のバックエンドへ振り分けて送信します。

実運用環境では、Collector の導入が推奨されます。Collector を介すことで、アプリケーション側を変更することなく送信先(Jaeger、Prometheus、クラウドサービスなど)の追加や変更が可能になります。また、暗号化通信の集約や、個人情報のマスキング処理などを一元管理できます。

第2章 単純構成:OpenTelemetry SDK と Jaeger v2 によるトレース構築

本章では、OpenTelemetry SDK から分散トレーシングシステム「Jaeger v2」へ直接トレースデータを送信する最小構成を構築します。中間サーバー(Collector)を挟まず、手元の開発マシンで手軽に動作確認できる手順を解説します。

アーキテクチャの概要

最小構成のデータフローは以下の通りです。

+-----------------------+
|  Python アプリケーション |
|  (OpenTelemetry SDK)  |----+
+-----------------------+    |
                             | OTLP (gRPC: 4317 / HTTP: 4318)
+-----------------------+    |
|   C# アプリケーション  |----+
|  (OpenTelemetry SDK)  |    |
+-----------------------+    v
                    +--------------------+
                    |     Jaeger v2      |
                    |  - OTLP Receiver   |
                    |  - UI (:16686)     |
                    +--------------------+

Jaeger v2 は、OpenTelemetry Collector のフレームワークを基盤として再構築された新世代の Jaeger です。従来の独自プロトコル(Thrift など)に代わり、OTLP(ポート 4317 / 4318)を第一級の受信口として標準サポートしています。

Jaeger v2 の起動(Docker Compose)

作業用ディレクトリを用意し、Jaeger v2 を起動するための docker-compose.yaml を作成します。

services:
  jaeger:
    image: jaegertracing/jaeger:2.3.0
    container_name: jaeger-v2
    ports:
      - "16686:16686" # Jaeger UI
      - "4317:4317"   # OTLP gRPC 受信ポート
      - "4318:4318"   # OTLP HTTP 受信ポート
    environment:
      - METRICS_STORAGE_TYPE=memory

以下のコマンドを実行してコンテナを起動します。

docker compose up -d

起動後、ブラウザで http://localhost:16686 にアクセスすると、Jaeger の Web UI が表示されます。

Python によるトレース送信実装

Python の OpenTelemetry SDK を使用し、Jaeger へトレースを直接送信するコードを作成します。

必要なパッケージのインストール

仮想環境を作成し、必要なライブラリをインストールします。

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp

送信スクリプト(trace_sample.py)

以下のスクリプトは、親スパン(Parent Span)と子スパン(Child Span)を作成し、属性(Attributes)やイベントを付加して送信します。

import time
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# 1. サービス名などのリソース情報を定義
resource = Resource.create(
    attributes={
        "service.name": "order-service-python",
        "service.version": "1.0.0",
        "deployment.environment": "development",
    }
)

# 2. TracerProvider を作成し、OTLP エクスポーターを登録
provider = TracerProvider(resource=resource)
# Jaeger v2 の gRPC エンドポイント (localhost:4317) を指定
processor = BatchSpanProcessor(
    OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# 3. トレーサーを取得
tracer = trace.get_tracer("order-service-tracer")


def process_order(order_id: str):
    # 親スパンの開始
    with tracer.start_as_current_span("create_order") as parent_span:
        parent_span.set_attribute("order.id", order_id)
        parent_span.set_attribute("customer.vip", True)
        time.sleep(0.1)

        # 子スパンの開始
        with tracer.start_as_current_span("validate_inventory") as child_span:
            child_span.set_attribute("item.sku", "ITEM-9981")
            child_span.set_attribute("item.quantity", 2)
            time.sleep(0.05)
            child_span.add_event("在庫の引き当てに成功しました")

        with tracer.start_as_current_span("charge_payment") as child_span:
            child_span.set_attribute("payment.method", "credit_card")
            child_span.set_attribute("payment.amount", 3500)
            time.sleep(0.08)

    print(f"注文処理が完了しました: {order_id}")


if __name__ == "__main__":
    process_order("ORD-2026-001")
    # バッファされたスパンを確実にフラッシュして終了
    provider.shutdown()

スクリプトの実行

python trace_sample.py

C# によるトレース送信実装

.NET では、ランタイム組み込みの System.Diagnostics.ActivitySource と OpenTelemetry .NET SDK を組み合わせてトレースを送信します。

プロジェクトの作成とパッケージ追加

.NET 8 または .NET 10 を想定したコンソールアプリケーションを作成します。

dotnet new console -n OtelSampleCSharp
cd OtelSampleCSharp
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

送信プログラム(Program.cs)

Program.cs を以下のように記述します。

using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

const string serviceName = "payment-service-csharp";
const string serviceVersion = "1.0.0";

// 1. .NET 組み込みの ActivitySource を定義
var activitySource = new ActivitySource(serviceName, serviceVersion);

// 2. OpenTelemetry TracerProvider を構築
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .ConfigureResource(resource => resource.AddService(
        serviceName: serviceName,
        serviceVersion: serviceVersion,
        serviceInstanceId: Environment.MachineName))
    .AddSource(serviceName)
    // Jaeger v2 の gRPC エンドポイント (http://localhost:4317) を指定
    .AddOtlpExporter(options =>
    {
        options.Endpoint = new Uri("http://localhost:4317");
        options.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
    })
    .Build();

// 3. 業務処理を実行し、スパン(Activity)を生成
ProcessPayment("PAY-2026-8871", 5200);

void ProcessPayment(string paymentId, decimal amount)
{
    // 親スパンの開始
    using (var parentActivity = activitySource.StartActivity("ProcessPayment"))
    {
        parentActivity?.SetTag("payment.id", paymentId);
        parentActivity?.SetTag("payment.amount", amount);
        Thread.Sleep(100);

        // 子スパンの開始
        using (var authActivity = activitySource.StartActivity("AuthorizeCard"))
        {
            authActivity?.SetTag("card.brand", "Visa");
            Thread.Sleep(60);
            authActivity?.AddEvent(new ActivityEvent("カード与信枠を確保しました"));
        }

        using (var settleActivity = activitySource.StartActivity("SettleTransaction"))
        {
            settleActivity?.SetTag("settlement.currency", "JPY");
            Thread.Sleep(80);
        }
    }

    Console.WriteLine($"決済処理が完了しました: {paymentId}");
}

プログラムの実行

dotnet run

Jaeger UI でのトレース確認

  1. ブラウザで http://localhost:16686 を開きます。
  2. 左メニューの Service ドロップダウンをクリックします。
  3. order-service-python または payment-service-csharp が表示されていることを確認し、選択します。
  4. Find Traces ボタンをクリックします。
  5. 検索結果一覧からトレースをクリックすると、タイムライン形式で各スパンの親子関係、実行所要時間、付与した属性(Tags)やイベントが表示されます。

第3章 フルスタック構成:Collector、Jaeger v2、Prometheus、Grafana の統合環境

本章では、OpenTelemetry Collector を中核に据え、トレースを Jaeger v2、メトリクスを Prometheus と Grafana へ集約・可視化するフルスタック運用環境を構築します。

アーキテクチャの全体像

システムの全体像とデータフローは以下の通りです。

+-----------------------+     +-----------------------+
|  Python アプリケーション |     |   C# アプリケーション  |
+-----------------------+     +-----------------------+
            |                             |
            +--------------+--------------+
                           | OTLP (4317 / 4318)
                           v
            +-----------------------------+
            |   OpenTelemetry Collector   |
            | - Receivers: OTLP           |
            | - Processors: Batch         |
            | - Exporters: OTLP / Prom    |
            +-----------------------------+
                  |                 |
     OTLP (:4317) |                 | Metrics Scrape (:8889)
                  v                 v
            +-----------+     +------------+
            | Jaeger v2 |     | Prometheus |
            |  (Traces) |     |  (Metrics) |
            +-----------+     +------------+
                  ^                 ^
                  |                 |
                  +--------+--------+
                           |
                     +-----------+
                     |  Grafana  |
                     | Dashboards|
                     +-----------+

各コンポーネントの役割は以下の通りです。

  • OpenTelemetry Collector: アプリケーションから OTLP でトレースとメトリクスを一括受信し、加工・中継します。
  • Jaeger v2: Collector から転送されたトレースデータ(スパン)を保存し、分散処理のタイムラインを提供します。
  • Prometheus: Collector が公開するエンドポイントからメトリクスデータを定期的に収集(スクレイプ)し、時系列データベースに保存します。
  • Grafana: Prometheus のメトリクスグラフと Jaeger のトレースタイムラインを 1 つの画面に統合して表示します。

ディレクトリ構成

環境構築に必要な設定ファイル群を以下の構成で配置します。

fullstack/
├── docker-compose.yaml
├── otel-collector-config.yaml
├── prometheus.yml
└── grafana/
    └── provisioning/
        └── datasources/
            └── datasources.yaml

設定ファイルの作成

1. Collector 設定(otel-collector-config.yaml)

Collector のパイプラインを定義します。受信(receivers)、加工(processors)、出力(exporters)を組み合わせて設定します。

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
    spike_limit_percentage: 20

exporters:
  # トレースを Jaeger v2 へ OTLP で転送
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true

  # メトリクスを Prometheus がスクレイプできるように公開
  prometheus:
    endpoint: 0.0.0.0:8889
    namespace: app

  # デバッグ用標準出力
  debug:
    verbosity: basic

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/jaeger, debug]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheus, debug]

2. Prometheus 設定(prometheus.yml)

Prometheus が Collector のエンドポイントから定期的にメトリクスを取得するように設定します。

global:
  scrape_interval: 5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: "otel-collector"
    static_configs:
      - targets: ["otel-collector:8889"]

3. Grafana データソース自動登録(grafana/provisioning/datasources/datasources.yaml)

Grafana の起動時に、Prometheus と Jaeger を自動登録します。

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: true

  - name: Jaeger
    type: jaeger
    access: proxy
    url: http://jaeger:16686
    isDefault: false
    editable: true

4. Docker Compose 設定(docker-compose.yaml)

すべてのコンポーネントを同一ネットワーク内で起動します。

services:
  # OpenTelemetry Collector Contrib
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.118.0
    container_name: otel-collector
    command: ["--config=/etc/otelcol-contrib/config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
    ports:
      - "4317:4317" # OTLP gRPC 受信
      - "4318:4318" # OTLP HTTP 受信
      - "8889:8889" # Prometheus メトリクス公開ポート
    depends_on:
      - jaeger

  # Jaeger v2
  jaeger:
    image: jaegertracing/jaeger:2.3.0
    container_name: jaeger-v2
    ports:
      - "16686:16686" # Web UI
    environment:
      - METRICS_STORAGE_TYPE=memory

  # Prometheus
  prometheus:
    image: prom/prometheus:v3.2.1
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090" # Prometheus Web UI

  # Grafana
  grafana:
    image: grafana/grafana:11.5.2
    container_name: grafana
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
    ports:
      - "3000:3000" # Grafana Web UI
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    depends_on:
      - prometheus
      - jaeger

環境の起動と疎通確認

以下のコマンドを実行して一括起動します。

docker compose up -d

起動後、各サービスの Web 画面にアクセスして正常性を確認します。

  • Jaeger v2: http://localhost:16686
  • Prometheus: http://localhost:9090(Status -> Targets を開き、otel-collector が UP になっていることを確認)
  • Grafana: http://localhost:3000(ユーザー名: admin、パスワード: admin でログインし、Connections -> Data Sources に Prometheus と Jaeger が登録されていることを確認)

これで、アプリケーションからのテレメトリデータを受け入れる準備が完了しました。

第4章 実践アプリケーション実装:Python と C# による計測

本章では、第 3 章で構築したフルスタック基盤(Collector)へ向けて、Python と C# のアプリケーションから「トレース」と「メトリクス」の双方を計測・送信する完全な実装例を解説します。

Python によるトレースとメトリクスの同時計測

OpenTelemetry の Python SDK を使用し、注文処理のトレース(Span)と、注文数・処理時間のメトリクス(Counter、Histogram)を計測します。

ライブラリの準備

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp

実装コード(app_order.py)

import random
import time
from opentelemetry import metrics, trace
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# 共通のリソース(サービス情報)を定義
resource = Resource.create(
    attributes={
        "service.name": "order-service-python",
        "service.version": "2.0.0",
        "deployment.environment": "production",
    }
)

# 1. トレース設定 (TracerProvider)
trace_provider = TracerProvider(resource=resource)
trace_processor = BatchSpanProcessor(
    OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
)
trace_provider.add_span_processor(trace_processor)
trace.set_tracer_provider(trace_provider)
tracer = trace.get_tracer("order.tracer")

# 2. メトリクス設定 (MeterProvider)
# 5秒ごとに Collector へメトリクスを送信
metric_reader = PeriodicExportingMetricReader(
    OTLPMetricExporter(endpoint="localhost:4317", insecure=True),
    export_interval_millis=5000,
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
meter = metrics.get_meter("order.meter")

# 計測器(Instruments)の定義
order_counter = meter.create_counter(
    name="order_requests_total",
    description="注文リクエストの総数",
    unit="1",
)
order_duration_histogram = meter.create_histogram(
    name="order_duration_seconds",
    description="注文処理の所要時間(秒)",
    unit="s",
)


def handle_order(order_id: str, item_category: str):
    start_time = time.time()

    with tracer.start_as_current_span("handle_order") as span:
        span.set_attribute("order.id", order_id)
        span.set_attribute("order.category", item_category)

        # 疑似的な業務処理と子スパン
        with tracer.start_as_current_span("check_stock"):
            time.sleep(random.uniform(0.05, 0.15))

        with tracer.start_as_current_span("record_order"):
            time.sleep(random.uniform(0.05, 0.10))

        # 処理結果のメトリクスを記録
        elapsed = time.time() - start_time
        span.set_attribute("order.duration", elapsed)

        order_counter.add(1, {"category": item_category, "status": "success"})
        order_duration_histogram.record(elapsed, {"category": item_category})

    print(f"[Python] 注文完了: {order_id} ({item_category}) - {elapsed:.3f}s")


if __name__ == "__main__":
    print("Python 注文サービスを開始します(10回のリクエストをシミュレート)...")
    categories = ["electronics", "books", "clothing"]

    for i in range(1, 11):
        handle_order(f"ORD-PY-{i:03d}", random.choice(categories))
        time.sleep(0.5)

    print("テレメトリデータを送信中...")
    # プロバイダーをシャットダウンして未送信データを Collector へフラッシュ
    meter_provider.shutdown()
    trace_provider.shutdown()
    print("Python アプリケーションが正常終了しました。")

C# によるトレースとメトリクスの同時計測

.NET では、標準の ActivitySource(トレース)と Meter(メトリクス)を使用し、OpenTelemetry SDK 経由で Collector へ送信します。

ライブラリの準備

dotnet new console -n OtelFullstackCSharp
cd OtelFullstackCSharp
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

実装コード(Program.cs)

using System.Diagnostics;
using System.Diagnostics.Metrics;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

const string serviceName = "payment-service-csharp";
const string serviceVersion = "2.0.0";

// 1. .NET 組み込みの ActivitySource(トレース)と Meter(メトリクス)を宣言
var activitySource = new ActivitySource(serviceName, serviceVersion);
var meter = new Meter(serviceName, serviceVersion);

// 計測器(Instruments)の作成
var transactionCounter = meter.CreateCounter<long>(
    name: "payment_transactions_total",
    unit: "1",
    description: "決済トランザクションの総数");

var transactionAmountHistogram = meter.CreateHistogram<double>(
    name: "payment_amount_yen",
    unit: "JPY",
    description: "決済金額の分布");

// 2. OpenTelemetry TracerProvider の構築
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .ConfigureResource(r => r.AddService(serviceName, serviceVersion: serviceVersion))
    .AddSource(serviceName)
    .AddOtlpExporter(opt =>
    {
        opt.Endpoint = new Uri("http://localhost:4317");
        opt.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
    })
    .Build();

// 3. OpenTelemetry MeterProvider の構築
using var meterProvider = Sdk.CreateMeterProviderBuilder()
    .ConfigureResource(r => r.AddService(serviceName, serviceVersion: serviceVersion))
    .AddMeter(serviceName)
    .AddOtlpExporter(opt =>
    {
        opt.Endpoint = new Uri("http://localhost:4317");
        opt.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
    })
    .Build();

Console.WriteLine("C# 決済サービスを開始します(10件の決済をシミュレート)...");

var random = new Random();
var methods = new[] { "credit_card", "qr_code", "bank_transfer" };

for (int i = 1; i <= 10; i++)
{
    ExecutePayment($"PAY-CS-{i:03d}", methods[random.Next(methods.Length)], random.Next(1000, 15000));
    Thread.Sleep(500);
}

Console.WriteLine("C# アプリケーションが正常終了しました。");

void ExecutePayment(string transactionId, string method, double amount)
{
    // トレーススパンの作成
    using (var activity = activitySource.StartActivity("ExecutePayment"))
    {
        activity?.SetTag("payment.transaction_id", transactionId);
        activity?.SetTag("payment.method", method);

        using (var authActivity = activitySource.StartActivity("ValidateAccount"))
        {
            Thread.Sleep(random.Next(40, 100));
            authActivity?.AddEvent(new ActivityEvent("口座確認完了"));
        }

        using (var transferActivity = activitySource.StartActivity("ExecuteTransfer"))
        {
            Thread.Sleep(random.Next(60, 120));
        }

        // メトリクスの記録
        var tags = new TagList { { "method", method }, { "status", "success" } };
        transactionCounter.Add(1, tags);
        transactionAmountHistogram.Record(amount, tags);
    }

    Console.WriteLine($"[C#] 決済完了: {transactionId} ({method}) - ¥{amount:N0}");
}

可視化とデータ確認

両アプリケーションを実行した後、バックエンドツールで収集結果を確認します。

1. Prometheus でメトリクスを確認

ブラウザで http://localhost:9090 にアクセスし、上部の検索欄に以下のクエリを入力して Execute をクリックします。

  • Python の注文総数: app_order_requests_total
  • C# の決済トランザクション総数: app_payment_transactions_total
  • 平均処理時間の計算: rate(app_order_duration_seconds_sum[1m]) / rate(app_order_duration_seconds_count[1m])

ラベル(category や method)ごとに分類された時系列データが取得できていることを確認します。

2. Jaeger v2 でトレースを確認

ブラウザで http://localhost:16686 にアクセスします。

  • Service に order-service-python および payment-service-csharp が表示されます。
  • トレースを検索すると、各関数の呼び出し関係(handle_order -> check_stock -> record_order など)がガントチャート形式で正確に可視化されます。

3. Grafana でダッシュボード作成

ブラウザで http://localhost:3000 にアクセスします。

  1. 左メニューの Explore を開きます。
  2. データソースとして Prometheus を選択し、app_order_requests_total をグラフ描画します。
  3. 次にデータソースを Jaeger に切り替え、Trace ID で検索を行うことで、メトリクスのスパイク発生箇所から該当トレースへ直接深掘り(ドリルダウン)することが可能になります。

第5章 運用と設計のベストプラクティス

本章では、OpenTelemetry を実際のプロダクション環境で安定運用するためのベストプラクティス(コンテキスト伝播、サンプリング戦略、機密情報の保護、トラブルシューティング)を解説します。

分散トレーシングとコンテキスト伝播(Context Propagation)

複数のサービスがネットワークを介して連携する場合、トレース情報を後続のサービスへ引き継ぐ必要があります。この仕組みを「コンテキスト伝播」と呼びます。

OpenTelemetry では、W3C が標準化した W3C Trace Context 仕様を採用しています。HTTP リクエストを送信する際、ヘッダーに以下の情報が付加されます。

  • traceparent: トレース全体の識別子(Trace ID)と現在のスパン識別子(Span ID)、サンプリングフラグを結合した文字列。
  • tracestate: ベンダー固有の追加情報を運ぶキーバリューストア。

HTTP クライアントライブラリ(Python の requests や C# の HttpClient)に対して自動インストルメンテーション(または手動でのヘッダーインジェクション)を適用することで、開発者が意識することなくサービス間で同一の Trace ID が共有されます。

サンプリング戦略(Sampling Strategy)

本番環境で大規模なトラフィックが発生する場合、すべてのリクエスト(100%)をトレースすると、ネットワーク帯域やストレージ費用を圧迫します。そのため、目的に応じたサンプリングが必要です。

  • Head-based サンプリング(リクエスト開始時に判定)

    リクエストが開始された瞬間に、SDK 側で「このトレースを記録するかどうか」を決定します(例: 全体の 5% のみを均等にサンプリング)。シンプルで軽量ですが、エラーが発生した重要なリクエストがサンプリングから漏れてしまう可能性があります。

  • Tail-based サンプリング(リクエスト完了時に判定)

    OpenTelemetry Collector 内の tail_sampling プロセッサーを使用して実現します。Collector 側でトレース全体が完了するまで一時的にバッファリングし、「HTTP ステータスが 500 番台のエラー」「処理時間が 2 秒を超えた遅延リクエスト」のみを 100% 保存し、正常なリクエストは 1% のみ保存するといった高度な制御が可能です。

セマンティック規約と機密情報の保護

テレメトリデータに付加する属性(Attributes)には、OpenTelemetry が定めた「セマンティック規約(Semantic Conventions)」に従った命名を使用します。

  • HTTP 通信: http.request.method, http.response.status_code, url.path
  • データベース: db.system, db.name, db.statement

これにより、異なるプログラミング言語やライブラリで作成されたサービス間でも、統一されたキーでダッシュボードやアラートを作成できます。

また、テレメトリデータにパスワード、クレジットカード番号、個人名(PII: Personally Identifiable Information)が混入しないよう徹底する必要があります。OpenTelemetry Collector の transform プロセッサーなどを利用し、送信前に機密属性をハッシュ化またはマスクする防御策を設けることが推奨されます。

トラブルシューティング

テレメトリデータが Jaeger や Prometheus に届かない場合は、以下の順序で原因を切り分けます。

  1. Collector のログを確認する

    Collector の設定ファイルで debug エクスポーターを有効にしておくと、受信したスパンやメトリクスの件数がコンテナログに出力されます。データが Collector まで届いているかを確認します。

    docker compose logs -f otel-collector
    
  2. ポートと通信プロトコルの一致を確認する

    クライアントコードが指定しているポート番号(gRPC: 4317、HTTP: 4318)が、Docker Compose で正しくホストへマッピングされているか確認します。

  3. SDK のフラッシュとシャットダウン

    コンソールアプリやバッチ処理などの短期実行スクリプトでは、provider.shutdown() または provider.force_flush() を呼ばずにプログラムが終了すると、バッファ内のデータが送信されずに破棄されます。プログラム終了時に確実にシャットダウンが行われているか確認します。