OpenTelemetry
OpenTelemetry 包括的技術ガイド — 機能・アーキテクチャ・設定例
目次
- OpenTelemetry とは
- アーキテクチャ概要
- テレメトリの三本柱 — トレース
- テレメトリの三本柱 — メトリクス
- テレメトリの三本柱 — ログ
- SDK インストルメンテーション例 (Python)
- 自動インストルメンテーション (Auto-Instrumentation)
- 自動 vs 手動インストルメンテーション比較
- コンテキスト伝播 (Context Propagation)
- OpenTelemetry Collector アーキテクチャ
- Collector 設定例(フル構成)
- Connectors
- OTLP プロトコル (gRPC vs HTTP)
- サンプリング戦略
- Kubernetes デプロイメント (OTel Operator)
- セマンティック規約 (Semantic Conventions)
- テレメトリ相関 (Exemplars)
- ベストプラクティス
1. OpenTelemetry とは
1.1 概要
OpenTelemetry(OTel)は、CNCF(Cloud Native Computing Foundation) がホストするオープンソースのオブザーバビリティフレームワークである。Kubernetes に次ぐ CNCF で2番目にアクティブなプロジェクトであり、テレメトリデータ(トレース・メトリクス・ログ)の生成・収集・エクスポートを標準化する。
OpenTelemetry は以下の2つの先行プロジェクトを統合して2019年に誕生した:
- OpenTracing: 分散トレーシングの標準 API
- OpenCensus: Google 発のトレース・メトリクス収集ライブラリ
1.2 設計思想
+----------------------------------------------------------+
| OpenTelemetry の位置づけ |
+----------------------------------------------------------+
| |
| アプリケーション |
| ┌──────────────────────────────────────────────────┐ |
| │ ビジネスロジック │ |
| │ │ │ |
| │ ▼ │ |
| │ ┌──────────────────────┐ │ |
| │ │ OTel SDK / API │ ← ベンダー非依存 │ |
| │ │ (Traces, Metrics, │ │ |
| │ │ Logs) │ │ |
| │ └──────────┬───────────┘ │ |
| └─────────────┼────────────────────────────────────┘ |
| │ OTLP (標準プロトコル) |
| ▼ |
| ┌──────────────────────┐ |
| │ OTel Collector │ ← 収集・加工・転送 |
| └──────────┬───────────┘ |
| │ |
| ┌────────┼────────┐ |
| ▼ ▼ ▼ |
| Jaeger Prometheus Grafana ← 任意のバックエンド |
| Datadog Splunk Loki |
+----------------------------------------------------------+
1.3 主要な特徴
| 特徴 | 説明 |
|---|---|
| ベンダー非依存 | 特定の監視ツールに依存せず、バックエンドを自由に切り替え可能 |
| W3C Trace Context | 分散トレーシングのコンテキスト伝播に W3C 標準を採用 |
| OTLP | テレメトリ転送の標準プロトコル(gRPC / HTTP) |
| 多言語対応 | Java, Python, Go, .NET, JavaScript, Rust, C++, Swift 等のSDK |
| 自動インストルメンテーション | コード変更なしでテレメトリを自動取得 |
| セマンティック規約 | テレメトリ属性の命名を標準化 |
1.4 W3C Trace Context
W3C Trace Context は、分散システム間でトレースコンテキストを伝播するための HTTP ヘッダー標準である。
# HTTP リクエストヘッダー例
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
│ │ │ └─ flags (01 = sampled)
│ │ └─ parent-id (8 bytes)
│ └─ trace-id (16 bytes)
└─ version
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7
└─ ベンダー固有の追加コンテキスト(キー=値のリスト)
- traceparent: トレースID、スパンID、サンプリングフラグを含む必須ヘッダー
- tracestate: ベンダー固有の追加情報を伝播するオプションヘッダー
1.5 プロジェクトの成熟度
OpenTelemetry のシグナルごとの成熟度(2025年時点):
| シグナル | API | SDK | プロトコル |
|---|---|---|---|
| Traces | Stable | Stable | Stable |
| Metrics | Stable | Stable | Stable |
| Logs | Stable | Stable | Stable |
| Profiling | Experimental | Experimental | Experimental |
2. アーキテクチャ概要
2.1 コンポーネント構成
OpenTelemetry は以下の主要コンポーネントで構成される:
┌─────────────────────────────────────────────────────────────────────┐
│ OpenTelemetry アーキテクチャ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Specification (仕様) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────────────────┐ │ │
│ │ │ API │ │ SDK │ │ Data Model (OTLP)│ │ │
│ │ └──────┘ └──────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Language SDKs (言語別実装) │ │
│ │ │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ Python │ │ Java │ │ Go │ │ .NET │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ Node │ │ Rust │ │ C++ │ ... │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ │ OTLP │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ OpenTelemetry Collector │ │
│ │ │ │
│ │ Receivers ──▶ Processors ──▶ Exporters │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Observability Backends │ │
│ │ Jaeger │ Tempo │ Prometheus │ Datadog │ Splunk │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Kubernetes Operator (自動化) │ │
│ │ Collector CRD │ Instrumentation CRD │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 API と SDK の分離
OpenTelemetry では API と SDK が明確に分離されている:
- API: インストルメンテーション(計測)のためのインターフェース定義。ライブラリ開発者はAPIのみに依存する。
- SDK: APIの具体的な実装。サンプリング、エクスポート、リソース検出等の機能を提供する。
# API のみを使用(ライブラリコード)
from opentelemetry import trace
tracer = trace.get_tracer("my-library", "1.0.0")
with tracer.start_as_current_span("operation") as span:
span.set_attribute("key", "value")
# SDK の設定(アプリケーションコード)
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="localhost:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
この分離により:
- ライブラリは API のみに依存し、SDK への直接依存を避けられる
- アプリケーション開発者がSDKの設定(バックエンド選択、サンプリング等)を制御できる
- SDK が未設定の場合、API 呼び出しは no-op(何もしない)として動作する
2.3 リソース (Resource)
Resource はテレメトリを生成するエンティティ(サービス、ホスト、コンテナ等)を記述する属性のセットである。
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "payment-service",
"service.version": "2.1.0",
"service.namespace": "production",
"deployment.environment": "prod",
"host.name": "payment-pod-abc123",
"cloud.provider": "aws",
"cloud.region": "ap-northeast-1",
"k8s.namespace.name": "payments",
"k8s.pod.name": "payment-service-7d9f8c6b5-x2k4m",
})
3. テレメトリの三本柱 — トレース (Traces)
3.1 分散トレーシングの概念
分散トレースは、リクエストが複数のサービスを横断する際の一連の処理を追跡する仕組みである。
Trace (trace-id: abc123)
┌──────────────────────────────────────────────────────────────────┐
│ │
│ Service A: API Gateway │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Span: HTTP GET /api/orders/42 [0ms ─────── 350ms] │ │
│ │ span-id: aaa111 │ │
│ └─────────────┬───────────────────────────────────────────┘ │
│ │ │
│ Service B: Order Service │
│ │ ┌──────────────────────────────────────┐ │
│ │ │ Span: getOrder [20ms ── 280ms] │ │
│ │ │ span-id: bbb222 │ │
│ │ │ parent: aaa111 │ │
│ │ └────────┬──────────────────────────────┘ │
│ │ │ │
│ Service C: Database │
│ │ ┌────────────────────────┐ │
│ │ │ Span: SELECT [50ms-120ms]│ │
│ │ │ span-id: ccc333 │ │
│ │ │ parent: bbb222 │ │
│ │ └──────────────────────────┘ │
│ │ │ │
│ Service D: Cache │
│ │ ┌───────────────────────────┐ │
│ │ │ Span: redis.get [130ms-140ms]│ │
│ │ │ span-id: ddd444 │ │
│ │ │ parent: bbb222 │ │
│ │ └─────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
3.2 Span(スパン)
Span は分散トレースにおける作業の最小単位である。各 Span は以下の情報を持つ:
| フィールド | 説明 |
|---|---|
| Name | スパンの名前(例: HTTP GET /api/orders) |
| SpanContext | trace_id, span_id, trace_flags, trace_state |
| Parent SpanContext | 親スパンの SpanContext |
| SpanKind | スパンの種類(下記参照) |
| Start/End Timestamp | 開始・終了タイムスタンプ |
| Attributes | キー=値ペアの属性 |
| Events | タイムスタンプ付きのイベント |
| Links | 他のスパンへのリンク |
| Status | OK, ERROR, UNSET |
3.3 SpanContext
SpanContext はスパンを一意に識別し、トレース全体で伝播される情報である。
from opentelemetry import trace
span = trace.get_current_span()
ctx = span.get_span_context()
print(f"Trace ID: {format(ctx.trace_id, '032x')}") # 128-bit
print(f"Span ID: {format(ctx.span_id, '016x')}") # 64-bit
print(f"Trace Flags: {ctx.trace_flags}") # 8-bit (01 = sampled)
print(f"Trace State: {ctx.trace_state}") # vendor-specific key-value pairs
print(f"Is Remote: {ctx.is_remote}") # リモートから伝播されたか
3.4 SpanKind(スパン種別)
SpanKind はスパンの役割を示し、トレースの構造を理解しやすくする。
| SpanKind | 説明 | 使用場面 |
|---|---|---|
| INTERNAL | 内部操作(デフォルト) | ローカルな関数呼び出し、内部処理 |
| SERVER | サーバー側のリクエスト処理 | HTTP サーバーがリクエストを受信 |
| CLIENT | クライアント側のリクエスト送信 | HTTP クライアントがリクエストを送信 |
| PRODUCER | 非同期メッセージの送信 | Kafka にメッセージを publish |
| CONSUMER | 非同期メッセージの受信 | Kafka からメッセージを consume |
from opentelemetry import trace
from opentelemetry.trace import SpanKind
tracer = trace.get_tracer("example")
# SERVER スパン: HTTP リクエストを受け取るサーバー側
with tracer.start_as_current_span("handle_request", kind=SpanKind.SERVER) as span:
span.set_attribute("http.request.method", "GET")
span.set_attribute("url.path", "/api/orders/42")
span.set_attribute("http.response.status_code", 200)
# CLIENT スパン: 別のサービスへのリクエスト
with tracer.start_as_current_span("call_inventory", kind=SpanKind.CLIENT) as client_span:
client_span.set_attribute("http.request.method", "POST")
client_span.set_attribute("server.address", "inventory-service")
# PRODUCER スパン: メッセージの送信
with tracer.start_as_current_span("send_notification", kind=SpanKind.PRODUCER) as producer_span:
producer_span.set_attribute("messaging.system", "kafka")
producer_span.set_attribute("messaging.destination.name", "order-events")
3.5 SpanEvent(スパンイベント)
SpanEvent はスパンのライフタイム中に発生する特定の時点のイベントを記録する。ログのような情報をスパンに関連付けられる。
import time
from opentelemetry import trace
tracer = trace.get_tracer("example")
with tracer.start_as_current_span("process_order") as span:
# イベント: 注文のバリデーション完了
span.add_event("validation_completed", attributes={
"order.items_count": 3,
"order.total": 15000,
})
# 処理を実行
time.sleep(0.1)
# イベント: 支払い処理開始
span.add_event("payment_processing_started", attributes={
"payment.method": "credit_card",
"payment.provider": "stripe",
})
# 例外イベント: エラー発生時
try:
raise ValueError("Insufficient inventory")
except Exception as e:
span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
span.record_exception(e, attributes={
"exception.escaped": False,
"order.id": "ORD-12345",
})
3.6 SpanLink(スパンリンク)
SpanLink は因果関係はないが関連性のあるスパン同士を紐付ける。バッチ処理や非同期処理で特に有用である。
from opentelemetry import trace
from opentelemetry.trace import Link
tracer = trace.get_tracer("example")
# シナリオ: バッチ処理で複数の個別リクエストを一括処理
# 各リクエストの SpanContext を取得
request_contexts = [
trace.get_current_span().get_span_context() # 実際には各リクエストから取得
for _ in range(3)
]
# バッチ処理のスパンに、元のリクエストスパンをリンク
links = [
Link(ctx, attributes={"request.index": i})
for i, ctx in enumerate(request_contexts)
]
with tracer.start_as_current_span("batch_process", links=links) as span:
span.set_attribute("batch.size", len(links))
# バッチ処理の実行
pass
SpanLink の主なユースケース:
- バッチ処理: 複数の個別リクエストをまとめて処理する場合
- 非同期 Fan-out/Fan-in: 一つのリクエストが複数の並列処理を起動する場合
- キュー消費: キューからのメッセージが複数のプロデューサーに由来する場合
- トレース再開: タイムアウト後にリトライされた処理
3.7 Span Status
from opentelemetry import trace
from opentelemetry.trace import StatusCode
tracer = trace.get_tracer("example")
with tracer.start_as_current_span("risky_operation") as span:
try:
result = perform_operation()
span.set_status(StatusCode.OK)
except Exception as e:
span.set_status(StatusCode.ERROR, str(e))
span.record_exception(e)
raise
Status の種類:
- UNSET (デフォルト): 明示的にステータスが設定されていない
- OK: 操作が正常に完了
- ERROR: エラーが発生
4. テレメトリの三本柱 — メトリクス (Metrics)
4.1 メトリクスの概要
OpenTelemetry のメトリクスは、システムの状態を数値で計測するためのシグナルである。Prometheus のメトリクスモデルと互換性があり、時系列データとして集約される。
4.2 メトリクスの種類(Instrument Types)
| Instrument | 同期/非同期 | 単調性 | 用途 | 例 |
|---|---|---|---|---|
| Counter | 同期 | 単調増加 | 累積カウント | リクエスト数、エラー数 |
| UpDownCounter | 同期 | 増減可 | 現在の数量 | アクティブ接続数、キューサイズ |
| Histogram | 同期 | - | 値の分布 | レイテンシ、リクエストサイズ |
| Gauge | 非同期 | - | 現在値のスナップショット | CPU使用率、メモリ使用量 |
| ObservableCounter | 非同期 | 単調増加 | コールバックで取得する累積値 | システムCPU時間 |
| ObservableUpDownCounter | 非同期 | 増減可 | コールバックで取得する現在数量 | プロセスメモリ使用量 |
| ObservableGauge | 非同期 | - | コールバックで取得する現在値 | 温度センサー |
4.3 Counter(カウンター)
単調増加のみ可能なカウンター。リクエスト数やエラー数の計測に使用する。
from opentelemetry import metrics
meter = metrics.get_meter("example", "1.0.0")
# Counter の作成
request_counter = meter.create_counter(
name="http.server.request.count",
description="HTTP リクエストの総数",
unit="1",
)
# 使用例
def handle_request(method: str, route: str, status_code: int):
# 属性(ラベル)付きでカウント
request_counter.add(1, attributes={
"http.request.method": method,
"http.route": route,
"http.response.status_code": status_code,
})
# 呼び出し
handle_request("GET", "/api/orders", 200)
handle_request("POST", "/api/orders", 201)
handle_request("GET", "/api/orders", 500)
4.4 UpDownCounter
増減両方が可能なカウンター。アクティブな接続数やキューの深さなど、増減する値に使用する。
meter = metrics.get_meter("example", "1.0.0")
# UpDownCounter の作成
active_connections = meter.create_up_down_counter(
name="http.server.active_connections",
description="アクティブな HTTP 接続数",
unit="1",
)
queue_depth = meter.create_up_down_counter(
name="message.queue.depth",
description="メッセージキューの現在の深さ",
unit="1",
)
# 使用例
def on_connection_open(service: str):
active_connections.add(1, attributes={"service.name": service})
def on_connection_close(service: str):
active_connections.add(-1, attributes={"service.name": service})
def on_message_enqueue(queue: str):
queue_depth.add(1, attributes={"queue.name": queue})
def on_message_dequeue(queue: str):
queue_depth.add(-1, attributes={"queue.name": queue})
4.5 Histogram(ヒストグラム)
値の分布を記録する。レイテンシやリクエストサイズなど、統計的な分析が必要な値に使用する。
import time
meter = metrics.get_meter("example", "1.0.0")
# Histogram の作成
request_duration = meter.create_histogram(
name="http.server.request.duration",
description="HTTP リクエストの処理時間",
unit="ms",
)
request_size = meter.create_histogram(
name="http.server.request.body.size",
description="HTTP リクエストボディのサイズ",
unit="By",
)
# 使用例
def handle_request(method: str, route: str):
start = time.time()
try:
result = process_request()
status_code = 200
except Exception:
status_code = 500
finally:
duration_ms = (time.time() - start) * 1000
request_duration.record(duration_ms, attributes={
"http.request.method": method,
"http.route": route,
"http.response.status_code": status_code,
})
# カスタムバケット境界の設定(SDK設定時)
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation
latency_view = View(
instrument_name="http.server.request.duration",
aggregation=ExplicitBucketHistogramAggregation(
boundaries=[0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 10000]
),
)
4.6 Gauge(ゲージ)
非同期のコールバックで現在の値を報告する。CPU使用率やメモリ使用量など、ポーリングで取得する値に使用する。
import psutil
meter = metrics.get_meter("example", "1.0.0")
# Gauge (Observable Gauge) の作成 — コールバック関数を登録
def cpu_usage_callback(options):
"""CPU使用率を返すコールバック"""
for i, percent in enumerate(psutil.cpu_percent(percpu=True)):
yield metrics.Observation(
value=percent,
attributes={"cpu.id": str(i)},
)
def memory_usage_callback(options):
"""メモリ使用量を返すコールバック"""
mem = psutil.virtual_memory()
yield metrics.Observation(
value=mem.used,
attributes={"state": "used"},
)
yield metrics.Observation(
value=mem.available,
attributes={"state": "available"},
)
meter.create_observable_gauge(
name="system.cpu.utilization",
description="CPU 使用率",
unit="1",
callbacks=[cpu_usage_callback],
)
meter.create_observable_gauge(
name="system.memory.usage",
description="メモリ使用量",
unit="By",
callbacks=[memory_usage_callback],
)
4.7 メトリクス集約パイプライン
┌──────────────────────────────────────────────────────────────────┐
│ メトリクスパイプライン │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Instrument (Counter, Histogram, etc.) │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Measurement │ 個々の計測値 │
│ │ (value + attrs) │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ View で集約方法を制御 │
│ │ Aggregation │ - Sum (Counter) │
│ │ │ - Histogram (Histogram) │
│ │ │ - LastValue (Gauge) │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ Temporality: │
│ │ MetricReader │ - Cumulative (Prometheus互換) │
│ │ │ - Delta (効率的な転送) │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ MetricExporter │ OTLP, Prometheus, Console 等 │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
5. テレメトリの三本柱 — ログ (Logs)
5.1 OTel Logs の概要
OpenTelemetry のログは、既存のログ基盤との統合を重視して設計されている。既存のログライブラリ(Python の logging、Java の Log4j 等)からのログを取り込み、トレースコンテキストと関連付ける。
5.2 ログデータモデル
┌──────────────────────────────────────────────────────────────────┐
│ LogRecord │
├──────────────────────────────────────────────────────────────────┤
│ Timestamp: 2025-01-15T10:30:45.123456Z │
│ ObservedTimestamp: 2025-01-15T10:30:45.123500Z │
│ SeverityNumber: 9 (INFO) │
│ SeverityText: "INFO" │
│ Body: "Order processed successfully" │
│ Attributes: │
│ order.id: "ORD-12345" │
│ customer.id: "CUST-67890" │
│ Resource: │
│ service.name: "order-service" │
│ service.version: "2.1.0" │
│ TraceId: 4bf92f3577b34da6a3ce929d0e0e4736 │
│ SpanId: 00f067aa0ba902b7 │
│ TraceFlags: 01 │
└──────────────────────────────────────────────────────────────────┘
5.3 トレースとの相関
ログとトレースの相関は、同じリクエストコンテキスト内で出力されたログに自動的に trace_id と span_id が付与されることで実現される。
import logging
from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
# ログプロバイダーのセットアップ
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(endpoint="localhost:4317"))
)
set_logger_provider(logger_provider)
# Python の標準 logging と統合
handler = LoggingHandler(
level=logging.INFO,
logger_provider=logger_provider,
)
logging.getLogger().addHandler(handler)
# 使用例: トレースコンテキスト内でのログ出力
logger = logging.getLogger("order-service")
tracer = trace.get_tracer("order-service")
def process_order(order_id: str):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
# このログは自動的に trace_id, span_id が付与される
logger.info(
"Order processing started",
extra={"order.id": order_id}
)
try:
validate_order(order_id)
logger.info("Order validation passed", extra={"order.id": order_id})
charge_payment(order_id)
logger.info("Payment charged successfully", extra={"order.id": order_id})
except Exception as e:
logger.error(
f"Order processing failed: {e}",
extra={"order.id": order_id},
exc_info=True,
)
raise
5.4 Severity Levels マッピング
| SeverityNumber | SeverityText | Python logging | Java Log4j |
|---|---|---|---|
| 1-4 | TRACE | - | TRACE |
| 5-8 | DEBUG | DEBUG (10) | DEBUG |
| 9-12 | INFO | INFO (20) | INFO |
| 13-16 | WARN | WARNING (30) | WARN |
| 17-20 | ERROR | ERROR (40) | ERROR |
| 21-24 | FATAL | CRITICAL (50) | FATAL |
6. SDK インストルメンテーション例 (Python)
6.1 完全なセットアップ例
以下は、トレース・メトリクス・ログの3つのシグナルすべてを設定する完全な Python アプリケーション例である。
# otel_setup.py — OpenTelemetry 完全セットアップ
from opentelemetry import trace, metrics
from opentelemetry._logs import set_logger_provider
# --- Resource(共通)---
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "order-service",
"service.version": "2.1.0",
"deployment.environment": "production",
})
# ===================================
# 1. トレースの設定
# ===================================
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://otel-collector:4317"),
max_queue_size=2048,
max_export_batch_size=512,
schedule_delay_millis=5000,
)
)
trace.set_tracer_provider(trace_provider)
# ===================================
# 2. メトリクスの設定
# ===================================
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint="http://otel-collector:4317"),
export_interval_millis=60000, # 60秒ごとにエクスポート
)
metrics_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(metrics_provider)
# ===================================
# 3. ログの設定
# ===================================
import logging
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(endpoint="http://otel-collector:4317")
)
)
set_logger_provider(logger_provider)
# Python 標準 logging に OTel ハンドラーを追加
handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)
# ===================================
# シャットダウン関数
# ===================================
def shutdown():
trace_provider.shutdown()
metrics_provider.shutdown()
logger_provider.shutdown()
6.2 トレースの実装例
# tracing_example.py
import time
from opentelemetry import trace
from opentelemetry.trace import StatusCode, SpanKind
tracer = trace.get_tracer("order-service", "2.1.0")
def create_order(request):
"""注文作成の処理 — トレーシング付き"""
with tracer.start_as_current_span(
"create_order",
kind=SpanKind.SERVER,
attributes={
"http.request.method": "POST",
"url.path": "/api/orders",
"user.id": request.user_id,
},
) as span:
try:
# ステップ1: バリデーション
with tracer.start_as_current_span("validate_order") as val_span:
validate_result = validate(request)
val_span.set_attribute("validation.items_count", len(request.items))
val_span.add_event("validation_complete", attributes={
"valid": True,
})
# ステップ2: 在庫チェック(外部サービス呼び出し)
with tracer.start_as_current_span(
"check_inventory",
kind=SpanKind.CLIENT,
) as inv_span:
inv_span.set_attribute("server.address", "inventory-service")
inv_span.set_attribute("rpc.system", "grpc")
inventory_ok = check_inventory(request.items)
# ステップ3: 支払い処理
with tracer.start_as_current_span(
"process_payment",
kind=SpanKind.CLIENT,
) as pay_span:
pay_span.set_attribute("payment.method", request.payment_method)
pay_span.set_attribute("payment.amount", request.total)
payment_result = process_payment(request)
pay_span.add_event("payment_completed", attributes={
"transaction.id": payment_result.transaction_id,
})
# ステップ4: 確認メール送信(非同期)
with tracer.start_as_current_span(
"send_confirmation",
kind=SpanKind.PRODUCER,
) as msg_span:
msg_span.set_attribute("messaging.system", "kafka")
msg_span.set_attribute("messaging.destination.name", "order-confirmations")
send_to_kafka("order-confirmations", {
"order_id": request.order_id,
"email": request.email,
})
span.set_status(StatusCode.OK)
return {"order_id": request.order_id, "status": "created"}
except Exception as e:
span.set_status(StatusCode.ERROR, str(e))
span.record_exception(e)
raise
6.3 メトリクスの実装例
# metrics_example.py
import time
from opentelemetry import metrics
meter = metrics.get_meter("order-service", "2.1.0")
# --- Counter ---
orders_created = meter.create_counter(
name="orders.created.total",
description="作成された注文の総数",
unit="1",
)
# --- UpDownCounter ---
orders_in_progress = meter.create_up_down_counter(
name="orders.in_progress",
description="現在処理中の注文数",
unit="1",
)
# --- Histogram ---
order_processing_duration = meter.create_histogram(
name="orders.processing.duration",
description="注文処理にかかった時間",
unit="ms",
)
order_total_amount = meter.create_histogram(
name="orders.total.amount",
description="注文の合計金額の分布",
unit="JPY",
)
# --- Observable Gauge ---
def active_workers_callback(options):
"""アクティブなワーカースレッド数を返す"""
import threading
yield metrics.Observation(
value=threading.active_count(),
attributes={"worker.type": "thread"},
)
meter.create_observable_gauge(
name="workers.active",
description="アクティブなワーカー数",
unit="1",
callbacks=[active_workers_callback],
)
# 使用例
def process_order(order):
orders_in_progress.add(1, attributes={"order.type": order.type})
start = time.time()
try:
result = _do_process(order)
orders_created.add(1, attributes={
"order.type": order.type,
"status": "success",
})
order_total_amount.record(order.total, attributes={
"order.type": order.type,
"currency": "JPY",
})
return result
except Exception as e:
orders_created.add(1, attributes={
"order.type": order.type,
"status": "failure",
})
raise
finally:
duration_ms = (time.time() - start) * 1000
order_processing_duration.record(duration_ms, attributes={
"order.type": order.type,
})
orders_in_progress.add(-1, attributes={"order.type": order.type})
6.4 ログの実装例
# logging_example.py
import logging
from opentelemetry import trace
logger = logging.getLogger("order-service")
tracer = trace.get_tracer("order-service")
def process_order(order_id: str, items: list):
"""トレースコンテキスト付きログの例"""
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
# INFO レベルのログ — 自動的に trace_id, span_id が付与される
logger.info(
"Starting order processing",
extra={
"order.id": order_id,
"items.count": len(items),
},
)
for item in items:
with tracer.start_as_current_span("process_item") as item_span:
item_span.set_attribute("item.sku", item["sku"])
logger.info(
f"Processing item: {item['sku']}",
extra={"item.sku": item["sku"], "item.quantity": item["qty"]},
)
if item["qty"] > 100:
logger.warning(
f"Large quantity order for item {item['sku']}",
extra={"item.sku": item["sku"], "item.quantity": item["qty"]},
)
logger.info(
"Order processing completed",
extra={"order.id": order_id},
)
6.5 環境変数による設定
SDK は環境変数で設定をオーバーライドできる:
# エクスポーター設定
export OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4317"
export OTEL_EXPORTER_OTLP_PROTOCOL="grpc" # grpc or http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token123"
export OTEL_EXPORTER_OTLP_TIMEOUT="10000" # ミリ秒
# リソース設定
export OTEL_SERVICE_NAME="order-service"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=2.1.0"
# トレース設定
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1" # 10% サンプリング
# メトリクス設定
export OTEL_METRICS_EXPORTER="otlp"
export OTEL_METRIC_EXPORT_INTERVAL="60000" # 60秒
# ログ設定
export OTEL_LOGS_EXPORTER="otlp"
# 伝播設定
export OTEL_PROPAGATORS="tracecontext,baggage"
# SDK の無効化(デバッグ用)
export OTEL_SDK_DISABLED="false"
7. 自動インストルメンテーション (Auto-Instrumentation)
7.1 概要
自動インストルメンテーションは、アプリケーションのソースコードを変更せずにテレメトリデータを自動的に取得する仕組みである。一般的なフレームワーク・ライブラリ(HTTP サーバー、データベースクライアント、メッセージキュー等)の呼び出しを自動的に計測する。
7.2 Python 自動インストルメンテーション
# インストール
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install # 検出されたライブラリ用のインストルメンテーションを自動インストール
# 実行 — コード変更不要
opentelemetry-instrument \
--service_name order-service \
--traces_exporter otlp \
--metrics_exporter otlp \
--logs_exporter otlp \
--exporter_otlp_endpoint http://otel-collector:4317 \
python app.py
自動的に計測されるライブラリの例:
| ライブラリ | 計測内容 |
|---|---|
| Flask / Django / FastAPI | HTTP リクエスト/レスポンスのスパン |
| requests / httpx / aiohttp | 外部 HTTP 呼び出しのスパン |
| SQLAlchemy / psycopg2 / pymysql | データベースクエリのスパン |
| redis | Redis コマンドのスパン |
| celery | タスクの実行スパン |
| boto3 (AWS SDK) | AWS API 呼び出しのスパン |
| kafka-python | Kafka 送受信のスパン |
| grpc | gRPC 呼び出しのスパン |
7.3 Java 自動インストルメンテーション
Java は Java Agent メカニズムを使用し、バイトコードレベルでインストルメンテーションを注入する。
# Java Agent のダウンロード
curl -L -o opentelemetry-javaagent.jar \
https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
# 実行 — コード変更不要
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=payment-service \
-Dotel.exporter.otlp.endpoint=http://otel-collector:4317 \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar payment-service.jar
Java Agent が自動計測する主なフレームワーク:
| カテゴリ | フレームワーク |
|---|---|
| Web フレームワーク | Spring Boot, Spring MVC, JAX-RS, Servlet, Netty |
| HTTP クライアント | HttpURLConnection, OkHttp, Apache HttpClient, WebClient |
| データベース | JDBC, Hibernate, MyBatis, R2DBC |
| メッセージング | Kafka, RabbitMQ, JMS, Amazon SQS |
| RPC | gRPC, Apache Dubbo |
| キャッシュ | Redis (Jedis, Lettuce), Memcached |
| ログ | Log4j2, Logback, JUL |
| クラウド | AWS SDK v1/v2, GCP, Azure |
7.4 Node.js 自動インストルメンテーション
# インストール
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-grpc \
@opentelemetry/exporter-metrics-otlp-grpc
// tracing.js — Node.js アプリ起動前にロード
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-grpc');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const sdk = new NodeSDK({
serviceName: 'frontend-service',
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4317',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: 'http://otel-collector:4317',
}),
exportIntervalMillis: 60000,
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
return req.url === '/health'; // ヘルスチェックを除外
},
},
'@opentelemetry/instrumentation-express': {
enabled: true,
},
'@opentelemetry/instrumentation-pg': {
enhancedDatabaseReporting: true,
},
}),
],
});
sdk.start();
// グレースフルシャットダウン
process.on('SIGTERM', () => {
sdk.shutdown().then(() => process.exit(0));
});
# 実行
node --require ./tracing.js app.js
# または環境変数で
export NODE_OPTIONS="--require ./tracing.js"
node app.js
8. 自動 vs 手動インストルメンテーション比較
8.1 自動インストルメンテーションで取得できるもの
Java Agent を例に、自動インストルメンテーションが捕捉する情報と捕捉できない情報を示す。
自動で取得できる情報:
┌──────────────────────────────────────────────────────────────────┐
│ 自動インストルメンテーションの可視範囲 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ✅ HTTP リクエスト/レスポンス │
│ - メソッド、URL、ステータスコード、レイテンシ │
│ - リクエスト/レスポンスヘッダー │
│ │
│ ✅ データベースクエリ │
│ - SQL 文、データベース名、接続情報、レイテンシ │
│ │
│ ✅ 外部 HTTP 呼び出し │
│ - 宛先ホスト、メソッド、ステータスコード │
│ │
│ ✅ メッセージング │
│ - Kafka/RabbitMQ のトピック、プロデュース/コンシューム │
│ │
│ ✅ gRPC 呼び出し │
│ - サービス名、メソッド名、ステータス │
│ │
│ ✅ キャッシュ操作 │
│ - Redis コマンド、キー、レイテンシ │
│ │
│ ✅ フレームワークのルーティング │
│ - Spring MVC のコントローラーマッピング │
│ - JAX-RS のリソースパス │
│ │
│ ✅ コンテキスト伝播 │
│ - HTTP ヘッダーからの自動抽出・注入 │
│ │
└──────────────────────────────────────────────────────────────────┘
8.2 自動インストルメンテーションで見えないもの
┌──────────────────────────────────────────────────────────────────┐
│ 自動インストルメンテーションでは見えない領域 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ❌ ビジネスロジックの内部処理 │
│ - 注文バリデーションの各ステップ │
│ - 価格計算のロジック │
│ - 割引適用ルール │
│ │
│ ❌ ビジネスメトリクス │
│ - 注文金額、商品数 │
│ - ユーザーセグメント別の処理件数 │
│ - コンバージョン率 │
│ │
│ ❌ 分岐・判断のコンテキスト │
│ - どの条件分岐が選択されたか │
│ - A/B テストのバリアント │
│ - フォールバックが発生したか │
│ │
│ ❌ バッチ処理の進捗 │
│ - 処理済み件数、スキップ件数 │
│ - 個別アイテムの処理結果 │
│ │
│ ❌ カスタムコンテキストの伝播 │
│ - テナントID、リクエスト元リージョン │
│ - ビジネスレベルの相関ID │
│ │
│ ❌ エラーの詳細コンテキスト │
│ - ビジネスバリデーションエラーの理由 │
│ - リトライ回数と各試行の結果 │
│ - 部分的な成功/失敗 │
│ │
└──────────────────────────────────────────────────────────────────┘
8.3 手動インストルメンテーションが必要なケース — Java コード例
ケース1: ビジネスロジックの内部処理
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.StatusCode;
public class OrderService {
private static final Tracer tracer =
GlobalOpenTelemetry.getTracer("order-service", "2.1.0");
public OrderResult processOrder(Order order) {
// 自動では見えない: 注文処理の内部ステップ
Span span = tracer.spanBuilder("processOrder")
.setAttribute("order.id", order.getId())
.setAttribute("order.total", order.getTotal().doubleValue())
.setAttribute("order.items.count", order.getItems().size())
.startSpan();
try (var scope = span.makeCurrent()) {
// ステップ1: ビジネスバリデーション
Span validationSpan = tracer.spanBuilder("validateBusinessRules")
.startSpan();
try (var vScope = validationSpan.makeCurrent()) {
ValidationResult result = validateBusinessRules(order);
validationSpan.setAttribute("validation.rules.checked", result.rulesChecked());
validationSpan.setAttribute("validation.passed", result.isPassed());
validationSpan.addEvent("validation_complete", Attributes.of(
AttributeKey.booleanKey("passed"), result.isPassed()
));
} finally {
validationSpan.end();
}
// ステップ2: 価格計算(自動では見えない内部処理)
Span pricingSpan = tracer.spanBuilder("calculatePricing")
.startSpan();
try (var pScope = pricingSpan.makeCurrent()) {
PricingResult pricing = calculatePricing(order);
pricingSpan.setAttribute("pricing.subtotal", pricing.getSubtotal());
pricingSpan.setAttribute("pricing.discount", pricing.getDiscount());
pricingSpan.setAttribute("pricing.tax", pricing.getTax());
pricingSpan.setAttribute("pricing.discount.reason", pricing.getDiscountReason());
pricingSpan.addEvent("discount_applied", Attributes.of(
AttributeKey.stringKey("discount.type"), pricing.getDiscountType(),
AttributeKey.doubleKey("discount.rate"), pricing.getDiscountRate()
));
} finally {
pricingSpan.end();
}
span.setStatus(StatusCode.OK);
return OrderResult.success(order.getId());
} catch (BusinessException e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
return OrderResult.failure(e.getErrorCode());
} finally {
span.end();
}
}
}
ケース2: ビジネスメトリクス
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.common.Attributes;
public class OrderMetrics {
private static final Meter meter =
GlobalOpenTelemetry.getMeter("order-service", "2.1.0");
// ビジネスメトリクス — 自動では取得できない
private static final LongCounter ordersPlaced = meter
.counterBuilder("business.orders.placed")
.setDescription("注文が確定された総数")
.setUnit("1")
.build();
private static final DoubleHistogram orderRevenue = meter
.histogramBuilder("business.orders.revenue")
.setDescription("注文金額の分布")
.setUnit("JPY")
.build();
private static final LongCounter conversionEvents = meter
.counterBuilder("business.conversion.events")
.setDescription("コンバージョンイベント数")
.setUnit("1")
.build();
public void recordOrderPlaced(Order order) {
Attributes attrs = Attributes.builder()
.put("order.type", order.getType().name())
.put("customer.segment", order.getCustomerSegment())
.put("channel", order.getChannel())
.put("region", order.getRegion())
.build();
ordersPlaced.add(1, attrs);
orderRevenue.record(order.getTotal().doubleValue(), attrs);
}
public void recordConversion(String funnelStep, String variant) {
conversionEvents.add(1, Attributes.of(
AttributeKey.stringKey("funnel.step"), funnelStep,
AttributeKey.stringKey("ab.variant"), variant
));
}
}
ケース3: 分岐・判断のトラッキング
public class FraudDetectionService {
private static final Tracer tracer =
GlobalOpenTelemetry.getTracer("fraud-detection", "1.0.0");
public FraudResult evaluate(Transaction tx) {
Span span = tracer.spanBuilder("evaluateFraud")
.setAttribute("transaction.id", tx.getId())
.setAttribute("transaction.amount", tx.getAmount())
.startSpan();
try (var scope = span.makeCurrent()) {
// 自動では見えない: どのルールがトリガーされたか
List<RuleResult> results = new ArrayList<>();
for (FraudRule rule : rules) {
Span ruleSpan = tracer.spanBuilder("evaluateRule." + rule.getName())
.setAttribute("rule.name", rule.getName())
.setAttribute("rule.version", rule.getVersion())
.startSpan();
try (var rScope = ruleSpan.makeCurrent()) {
RuleResult result = rule.evaluate(tx);
ruleSpan.setAttribute("rule.triggered", result.isTriggered());
ruleSpan.setAttribute("rule.score", result.getScore());
ruleSpan.setAttribute("rule.reason", result.getReason());
results.add(result);
} finally {
ruleSpan.end();
}
}
double totalScore = results.stream().mapToDouble(RuleResult::getScore).sum();
String decision = totalScore > THRESHOLD ? "BLOCK" : "ALLOW";
// 判断結果の記録
span.setAttribute("fraud.total_score", totalScore);
span.setAttribute("fraud.decision", decision);
span.setAttribute("fraud.rules_triggered",
results.stream().filter(RuleResult::isTriggered).count());
span.addEvent("fraud_decision", Attributes.of(
AttributeKey.stringKey("decision"), decision,
AttributeKey.doubleKey("score"), totalScore,
AttributeKey.longKey("rules_checked"), (long) results.size()
));
return new FraudResult(decision, totalScore, results);
} finally {
span.end();
}
}
}
ケース4: バッチ処理の進捗
public class BatchProcessor {
private static final Tracer tracer =
GlobalOpenTelemetry.getTracer("batch-processor", "1.0.0");
private static final Meter meter =
GlobalOpenTelemetry.getMeter("batch-processor", "1.0.0");
private static final LongCounter processedItems = meter
.counterBuilder("batch.items.processed")
.setDescription("バッチ処理済みアイテム数")
.build();
public BatchResult processBatch(List<Item> items) {
Span batchSpan = tracer.spanBuilder("processBatch")
.setAttribute("batch.size", items.size())
.startSpan();
int success = 0, failure = 0, skipped = 0;
try (var scope = batchSpan.makeCurrent()) {
for (int i = 0; i < items.size(); i++) {
Item item = items.get(i);
Span itemSpan = tracer.spanBuilder("processItem")
.setAttribute("item.id", item.getId())
.setAttribute("item.index", i)
.startSpan();
try (var iScope = itemSpan.makeCurrent()) {
if (shouldSkip(item)) {
skipped++;
itemSpan.setAttribute("item.result", "skipped");
itemSpan.setAttribute("item.skip.reason", getSkipReason(item));
} else {
processItem(item);
success++;
itemSpan.setAttribute("item.result", "success");
}
} catch (Exception e) {
failure++;
itemSpan.setAttribute("item.result", "failure");
itemSpan.recordException(e);
itemSpan.setStatus(StatusCode.ERROR);
} finally {
itemSpan.end();
}
// 定期的に進捗を記録
if (i % 100 == 0) {
batchSpan.addEvent("batch_progress", Attributes.of(
AttributeKey.longKey("processed"), (long) (i + 1),
AttributeKey.longKey("total"), (long) items.size(),
AttributeKey.longKey("success"), (long) success,
AttributeKey.longKey("failure"), (long) failure
));
}
}
// 最終結果を記録
batchSpan.setAttribute("batch.result.success", success);
batchSpan.setAttribute("batch.result.failure", failure);
batchSpan.setAttribute("batch.result.skipped", skipped);
processedItems.add(success, Attributes.of(
AttributeKey.stringKey("result"), "success"));
processedItems.add(failure, Attributes.of(
AttributeKey.stringKey("result"), "failure"));
processedItems.add(skipped, Attributes.of(
AttributeKey.stringKey("result"), "skipped"));
return new BatchResult(success, failure, skipped);
} finally {
batchSpan.end();
}
}
}
ケース5: カスタムコンテキスト伝播
import io.opentelemetry.api.baggage.Baggage;
public class MultiTenantService {
public void handleRequest(HttpServletRequest request) {
String tenantId = request.getHeader("X-Tenant-ID");
String region = request.getHeader("X-Region");
// Baggage にビジネスコンテキストを設定
// 下流サービスに自動的に伝播される
Baggage baggage = Baggage.builder()
.put("tenant.id", tenantId)
.put("request.region", region)
.put("request.priority", determinePriority(request))
.build();
try (var scope = baggage.makeCurrent()) {
// 以降の処理では、下流サービスでも tenant.id が利用可能
processRequest(request);
}
}
// 下流サービスで Baggage を読み取る
public void downstreamProcess() {
String tenantId = Baggage.current().getEntryValue("tenant.id");
String region = Baggage.current().getEntryValue("request.region");
Span.current().setAttribute("tenant.id", tenantId);
Span.current().setAttribute("request.region", region);
}
}
ケース6: エラーコンテキストの強化
public class PaymentService {
private static final Tracer tracer =
GlobalOpenTelemetry.getTracer("payment-service", "1.0.0");
private static final LongCounter retryCounter = meter
.counterBuilder("payment.retries")
.setDescription("支払いリトライ回数")
.build();
public PaymentResult processPayment(PaymentRequest request) {
Span span = tracer.spanBuilder("processPayment")
.setAttribute("payment.method", request.getMethod())
.setAttribute("payment.amount", request.getAmount())
.setAttribute("payment.currency", request.getCurrency())
.startSpan();
int maxRetries = 3;
try (var scope = span.makeCurrent()) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
PaymentResult result = gateway.charge(request);
span.setAttribute("payment.attempt", attempt);
span.setAttribute("payment.transaction_id", result.getTransactionId());
if (attempt > 1) {
span.addEvent("payment_succeeded_after_retry", Attributes.of(
AttributeKey.longKey("attempt"), (long) attempt
));
}
return result;
} catch (TransientException e) {
retryCounter.add(1, Attributes.of(
AttributeKey.stringKey("payment.method"), request.getMethod(),
AttributeKey.longKey("attempt"), (long) attempt
));
span.addEvent("payment_retry", Attributes.of(
AttributeKey.longKey("attempt"), (long) attempt,
AttributeKey.stringKey("error.type"), e.getClass().getSimpleName(),
AttributeKey.stringKey("error.message"), e.getMessage(),
AttributeKey.stringKey("retry.reason"), e.getRetryReason()
));
if (attempt == maxRetries) {
span.setStatus(StatusCode.ERROR,
"Payment failed after " + maxRetries + " attempts");
span.recordException(e);
throw new PaymentFailedException(
"Max retries exceeded", e, attempt);
}
backoff(attempt);
}
}
} finally {
span.end();
}
throw new IllegalStateException("Unreachable");
}
}
8.4 推奨アプローチ
┌───────────────────────────────────────────────────────────────────┐
│ 推奨: ハイブリッドアプローチ │
├───────────────────────────────────────────────────────────────────┤
│ │
│ レイヤー1: 自動インストルメンテーション(ベースライン) │
│ ───────────────────────────────────────────── │
│ • HTTP、DB、メッセージング等の基盤レベルのテレメトリ │
│ • コード変更不要、即座に可視性を獲得 │
│ • すべてのサービスに適用 │
│ │
│ レイヤー2: 手動インストルメンテーション(ビジネス価値) │
│ ───────────────────────────────────────────── │
│ • ビジネスロジックの重要なステップ │
│ • ビジネスメトリクス(売上、コンバージョン等) │
│ • 判断ロジックのトラッキング │
│ • エラーコンテキストの強化 │
│ • 重要なサービスに選択的に適用 │
│ │
│ ⚠️ 注意事項: │
│ • 過剰なインストルメンテーションはパフォーマンスに影響する │
│ • Span の粒度が細かすぎるとノイズになる │
│ • ビジネス上重要なパスに集中する │
│ • サンプリングと組み合わせてコストを制御する │
│ │
└───────────────────────────────────────────────────────────────────┘
9. コンテキスト伝播 (Context Propagation)
9.1 概要
コンテキスト伝播は、分散システムにおいてリクエストのコンテキスト情報(トレースID、Baggage 等)をサービス間で受け渡す仕組みである。
┌──────────────────────────────────────────────────────────────────┐
│ コンテキスト伝播の流れ │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Service A Service B │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Span (aaa111) │ HTTP │ Span (bbb222) │ │
│ │ │ ─────────▶ │ │ │
│ │ Inject context │ Headers: │ Extract context │ │
│ │ into headers │ traceparent │ from headers │ │
│ │ │ tracestate │ │ │
│ │ │ baggage │ parent=aaa111 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ Propagator が Inject/Extract を担当 │
│ │
└──────────────────────────────────────────────────────────────────┘
9.2 W3C Trace Context フォーマット
traceparent ヘッダー
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
フォーマット: {version}-{trace-id}-{parent-id}-{trace-flags}
version: 00 (固定: 現行バージョン)
trace-id: 4bf92f...4736 (128-bit, 32文字の16進数)
parent-id: 00f067...02b7 (64-bit, 16文字の16進数)
trace-flags: 01 (8-bit: 01 = sampled)
tracestate ヘッダー
tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
ベンダー固有のコンテキストをキー=値のリストで伝播:
- 複数ベンダーの情報を同時に伝播可能
- 左側ほど新しい(最後に更新したベンダーが先頭)
- 最大 32 エントリ、合計 512 バイト
9.3 Propagator の設定
from opentelemetry import propagate
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.propagators.textmap import TextMapPropagator
from opentelemetry.trace.propagation import TraceContextTextMapPropagator
from opentelemetry.baggage.propagation import W3CBaggagePropagator
# デフォルト: W3C Trace Context + Baggage
propagate.set_global_textmap(
CompositePropagator([
TraceContextTextMapPropagator(), # traceparent, tracestate
W3CBaggagePropagator(), # baggage ヘッダー
])
)
# B3 形式も追加する場合(Zipkin 互換)
from opentelemetry.propagators.b3 import B3MultiFormat
propagate.set_global_textmap(
CompositePropagator([
TraceContextTextMapPropagator(),
W3CBaggagePropagator(),
B3MultiFormat(), # x-b3-traceid, x-b3-spanid, x-b3-sampled
])
)
環境変数での設定:
# 使用する Propagator を指定
export OTEL_PROPAGATORS="tracecontext,baggage"
# B3 も追加
export OTEL_PROPAGATORS="tracecontext,baggage,b3multi"
# Jaeger 形式も追加
export OTEL_PROPAGATORS="tracecontext,baggage,jaeger"
9.4 Baggage(バゲージ)
Baggage はビジネスコンテキスト(テナントID、ユーザーリージョン等)をサービス間で伝播する仕組みである。
from opentelemetry import baggage, context
# Baggage の設定
ctx = baggage.set_baggage("tenant.id", "acme-corp")
ctx = baggage.set_baggage("user.region", "ap-northeast-1", context=ctx)
ctx = baggage.set_baggage("request.priority", "high", context=ctx)
# コンテキストをアクティブにする
token = context.attach(ctx)
try:
# 以降の HTTP 呼び出しで自動的に baggage ヘッダーとして伝播
# baggage: tenant.id=acme-corp,user.region=ap-northeast-1,request.priority=high
make_downstream_call()
finally:
context.detach(token)
# 下流サービスでの Baggage 読み取り
tenant_id = baggage.get_baggage("tenant.id")
region = baggage.get_baggage("user.region")
HTTP ヘッダーでの表現:
baggage: tenant.id=acme-corp,user.region=ap-northeast-1,request.priority=high
Baggage の注意点:
- Baggage はすべての下流サービスに伝播される
- 機密情報(パスワード、トークン等)を含めてはならない
- サイズが大きくなるとネットワークオーバーヘッドが増加する
- Baggage はテレメトリ属性に自動設定されない(明示的な設定が必要)
9.5 手動でのコンテキスト伝播
非標準のトランスポート(キュー、ファイル等)では手動で伝播する必要がある。
from opentelemetry import propagate, trace, context
# --- 送信側 (Producer) ---
def send_message_to_queue(message: dict):
"""メッセージキューにコンテキストを注入して送信"""
carrier = {}
propagate.inject(carrier) # 現在のコンテキストを carrier に注入
message["_otel_context"] = carrier # メッセージにコンテキストを含める
queue.send(message)
# --- 受信側 (Consumer) ---
def handle_message(message: dict):
"""メッセージキューからコンテキストを抽出して処理"""
carrier = message.get("_otel_context", {})
ctx = propagate.extract(carrier) # carrier からコンテキストを抽出
tracer = trace.get_tracer("consumer")
with tracer.start_as_current_span(
"process_message",
context=ctx,
kind=trace.SpanKind.CONSUMER,
) as span:
span.set_attribute("messaging.system", "custom_queue")
process(message)
10. OpenTelemetry Collector アーキテクチャ
10.1 概要
OpenTelemetry Collector は、テレメトリデータの受信・処理・エクスポートを行う中間コンポーネントである。ベンダー非依存のプロキシとして機能し、テレメトリパイプラインの中核を担う。
10.2 パイプラインアーキテクチャ
┌──────────────────────────────────────────────────────────────────────┐
│ OpenTelemetry Collector │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ Receivers │───▶│ Processors │───▶│ Exporters │ │
│ │ │ │ │ │ │ │
│ │ ・OTLP │ │ ・batch │ │ ・OTLP │ │
│ │ ・Prometheus│ │ ・memory_limiter│ │ ・Prometheus │ │
│ │ ・Kafka │ │ ・attributes │ │ ・Loki │ │
│ │ ・hostmetrics│ │ ・filter │ │ ・Kafka │ │
│ │ ・filelog │ │ ・tail_sampling│ │ ・debug │ │
│ └────────────┘ └────────────────┘ └──────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Extensions │ │
│ │ ・health_check ・pprof ・zpages │ │
│ │ ・memory_ballast ・oauth2client │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Connectors │ │
│ │ ・spanmetrics ・count ・routing │ │
│ └─────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘
10.3 デプロイパターン
パターン1: Agent(サイドカー / DaemonSet)
┌─────────────────────────────────────────────────────────┐
│ Node │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ App 1 │ │ App 2 │ │ App 3 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └────────────┼────────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ Collector │ ← Agent モード │
│ │ (DaemonSet)│ ノードごとに1つ │
│ └─────┬─────┘ │
│ │ │
└────────────────────┼─────────────────────────────────────┘
│ OTLP
▼
Observability Backend
メリット:
- アプリケーションに最も近い場所で処理
- ローカル通信のため低レイテンシ
- ノードレベルのメトリクス(hostmetrics)も収集可能
パターン2: Gateway(中央集約)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ App 1 │ │ App 2 │ │ App 3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼─────────────┘
│ OTLP
▼
┌─────────────────┐
│ Collector │ ← Gateway モード
│ (Deployment) │ クラスタで1-N個
│ │ ロードバランサー配下
└────────┬────────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
Jaeger Prometheus Loki
メリット:
- 中央での一括設定・管理
- サンプリング等の高度な処理を一か所で実行
- バックエンドへのコネクション数を削減
パターン3: Agent + Gateway(推奨構成)
┌─────────────────── Node 1 ────────────────────┐
│ ┌─────┐ ┌─────┐ │
│ │App 1│ │App 2│ │
│ └──┬──┘ └──┬──┘ │
│ └────┬───┘ │
│ ┌─────▼─────┐ │
│ │ Agent │ ← ローカル処理 │
│ │ Collector │ (バッチ、ベース属性付与) │
│ └─────┬─────┘ │
└──────────┼────────────────────────────────────┘
│
┌──────────┼─── Node 2 ────────────────────────┐
│ ┌─────┐ │ │
│ │App 3│ │ │
│ └──┬──┘ │ │
│ ┌──▼──────▼──┐ │
│ │ Agent │ │
│ │ Collector │ │
│ └─────┬──────┘ │
└────────┼──────────────────────────────────────┘
│
│ OTLP (全ノードから)
▼
┌──────────────────┐
│ Gateway Collector │ ← 中央処理
│ (Deployment) │ (サンプリング、ルーティング、
│ │ 高度な加工)
└────────┬─────────┘
│
┌────────┼────────┐
▼ ▼ ▼
Tempo Prometheus Loki
10.4 Collector のディストリビューション
| ディストリビューション | 説明 |
|---|---|
| otelcol (Core) | 最小限のコンポーネントのみ含む公式ビルド |
| otelcol-contrib | コミュニティ提供のレシーバー・プロセッサー・エクスポーターを含む |
| Custom build | ocb (OpenTelemetry Collector Builder) で必要なコンポーネントのみを含むカスタムビルド |
# カスタム Collector のビルド例 (builder-config.yaml)
cat <<'EOF' > builder-config.yaml
dist:
name: custom-otelcol
output_path: ./dist
otelcol_version: "0.96.0"
receivers:
- gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.96.0
- gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.96.0
processors:
- gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.96.0
- gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.96.0
exporters:
- gomod: go.opentelemetry.io/collector/exporter/otlpexporter v0.96.0
- gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.96.0
EOF
# ビルド実行
ocb --config builder-config.yaml
11. Collector 設定例(フル構成)
以下は、本番環境を想定した包括的な Collector 設定例である。
# otel-collector-config.yaml — 包括的な設定例
# ==============================================================
# Extensions: ヘルスチェック、デバッグ、パフォーマンス
# ==============================================================
extensions:
health_check:
endpoint: "0.0.0.0:13133"
path: "/health"
check_collector_pipeline:
enabled: true
exporter_failure_threshold: 5
pprof:
endpoint: "0.0.0.0:1777"
zpages:
endpoint: "0.0.0.0:55679"
memory_ballast:
size_mib: 512 # GC 圧力を軽減
# ==============================================================
# Receivers: テレメトリデータの受信
# ==============================================================
receivers:
# --- OTLP Receiver (gRPC + HTTP) ---
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
max_recv_msg_size_mib: 4
keepalive:
server_parameters:
max_connection_age: 60s
max_connection_age_grace: 5s
http:
endpoint: "0.0.0.0:4318"
cors:
allowed_origins:
- "https://*.example.com"
allowed_headers:
- "Content-Type"
- "X-Custom-Header"
# --- Prometheus Receiver ---
# Prometheus 形式のメトリクスをスクレイプ
prometheus:
config:
scrape_configs:
- job_name: "kubernetes-pods"
scrape_interval: 30s
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port, __meta_kubernetes_pod_ip]
action: replace
regex: (\d+);(([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})
replacement: "[$2]:$1"
target_label: __address__
- job_name: "otel-collector"
scrape_interval: 15s
static_configs:
- targets: ["localhost:8888"]
# --- Host Metrics Receiver ---
# ホストレベルのシステムメトリクスを収集
hostmetrics:
collection_interval: 30s
scrapers:
cpu:
metrics:
system.cpu.utilization:
enabled: true
memory:
metrics:
system.memory.utilization:
enabled: true
disk:
filesystem:
exclude_mount_points:
mount_points: ["/dev/*", "/proc/*", "/sys/*"]
match_type: regexp
load:
network:
paging:
processes:
# --- Kafka Receiver ---
# Kafka からテレメトリデータを受信
kafka:
brokers:
- "kafka-1:9092"
- "kafka-2:9092"
- "kafka-3:9092"
topic: "otlp-spans"
group_id: "otel-collector"
encoding: otlp_proto
protocol_version: "2.0.0"
auth:
sasl:
mechanism: PLAIN
username: "${env:KAFKA_USERNAME}"
password: "${env:KAFKA_PASSWORD}"
tls:
insecure: false
ca_file: /etc/ssl/certs/kafka-ca.pem
# --- Filelog Receiver ---
# ファイルからログを収集
filelog:
include:
- /var/log/pods/*/*/*.log
exclude:
- /var/log/pods/*/otel-collector/*.log
start_at: end
include_file_path: true
include_file_name: false
operators:
# Docker/CRI ログフォーマットのパース
- type: router
id: container_format
routes:
- output: parse_docker
expr: 'body matches "^\\{"'
- output: parse_cri
expr: 'body matches "^[^ Z]+ "'
- type: json_parser
id: parse_docker
output: extract_metadata
timestamp:
parse_from: attributes.time
layout: "%Y-%m-%dT%H:%M:%S.%LZ"
- type: regex_parser
id: parse_cri
regex: '^(?P<time>[^ Z]+) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
output: extract_metadata
timestamp:
parse_from: attributes.time
layout: "%Y-%m-%dT%H:%M:%S.%LZ"
- type: move
id: extract_metadata
from: attributes.log
to: body
# ==============================================================
# Processors: テレメトリデータの加工
# ==============================================================
processors:
# --- Batch Processor ---
# テレメトリをバッチ化してエクスポート効率を向上
batch:
send_batch_size: 8192
send_batch_max_size: 16384
timeout: 5s
# --- Memory Limiter Processor ---
# メモリ使用量を制限してOOMを防止
# ※ パイプラインの最初に配置すること
memory_limiter:
check_interval: 1s
limit_mib: 1536 # ハードリミット
spike_limit_mib: 512 # スパイク許容量
# limit_percentage: 75 # または割合で指定
# spike_limit_percentage: 25
# --- Attributes Processor ---
# スパン/メトリクス/ログの属性を追加・更新・削除
attributes:
actions:
# 環境情報を追加
- key: deployment.environment
value: "production"
action: upsert
# 機密情報をマスク
- key: http.request.header.authorization
action: delete
- key: db.statement
action: hash # SQL をハッシュ化
# 属性名の変換
- key: http.status_code
from_attribute: http.response.status_code
action: upsert
# --- Resource Processor ---
# リソース属性を追加
resource:
attributes:
- key: cloud.provider
value: "aws"
action: upsert
- key: cloud.region
value: "ap-northeast-1"
action: upsert
- key: k8s.cluster.name
value: "production-cluster"
action: upsert
# --- Filter Processor ---
# 不要なテレメトリを除外
filter:
error_mode: ignore
traces:
span:
# ヘルスチェックのスパンを除外
- 'attributes["http.route"] == "/health"'
- 'attributes["http.route"] == "/ready"'
- 'attributes["http.route"] == "/metrics"'
metrics:
metric:
# 特定のメトリクスを除外
- 'name == "http.server.duration" and resource.attributes["service.name"] == "debug-service"'
logs:
log_record:
# DEBUG レベルのログを除外
- 'severity_number < 9'
# --- Tail Sampling Processor ---
# トレース全体を見てからサンプリング判断
# ※ Gateway Collector でのみ使用推奨
tail_sampling:
decision_wait: 30s
num_traces: 100000
expected_new_traces_per_sec: 1000
policies:
# ポリシー1: エラーを含むトレースは100%保持
- name: errors-policy
type: status_code
status_code:
status_codes:
- ERROR
# ポリシー2: 高レイテンシのトレースを保持
- name: latency-policy
type: latency
latency:
threshold_ms: 1000
# ポリシー3: 特定のサービスは100%保持
- name: critical-services
type: string_attribute
string_attribute:
key: service.name
values:
- payment-service
- auth-service
# ポリシー4: その他は10%サンプリング
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 10
# 複合ポリシー: AND 条件
- name: composite-policy
type: and
and:
and_sub_policy:
- name: slow-traces
type: latency
latency:
threshold_ms: 500
- name: not-health-check
type: string_attribute
string_attribute:
key: http.route
values:
- "/health"
- "/ready"
invert_match: true
# --- Span Metrics Processor (Connector) ---
# スパンからメトリクスを自動生成(RED メトリクス)
# ※ 現在は Connector として実装
# spanmetrics:
# metrics_exporter: prometheus
# dimensions:
# - name: http.method
# - name: http.route
# - name: http.status_code
# --- K8s Attributes Processor ---
# Kubernetes のメタデータをテレメトリに付与
k8sattributes:
auth_type: "serviceAccount"
passthrough: false
filter:
node_from_env_var: KUBE_NODE_NAME
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.pod.uid
- k8s.deployment.name
- k8s.node.name
- k8s.container.name
labels:
- tag_name: app.label.team
key: team
from: pod
- tag_name: app.label.version
key: app.kubernetes.io/version
from: pod
annotations:
- tag_name: app.annotation.config
key: config-hash
from: pod
pod_association:
- sources:
- from: resource_attribute
name: k8s.pod.ip
- sources:
- from: resource_attribute
name: k8s.pod.uid
- sources:
- from: connection
# --- Transform Processor ---
# OTTL (OpenTelemetry Transformation Language) でデータ変換
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
- set(attributes["processed_by"], "otel-collector")
- truncate_all(attributes, 256)
log_statements:
- context: log
statements:
- set(severity_text, "INFO") where severity_number >= 9 and severity_number < 13
# ==============================================================
# Exporters: テレメトリデータの送信先
# ==============================================================
exporters:
# --- OTLP Exporter (gRPC) ---
otlp:
endpoint: "tempo.monitoring:4317"
tls:
insecure: false
ca_file: /etc/ssl/certs/ca.pem
cert_file: /etc/ssl/certs/client.pem
key_file: /etc/ssl/certs/client-key.pem
headers:
Authorization: "Bearer ${env:OTLP_AUTH_TOKEN}"
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
sending_queue:
enabled: true
num_consumers: 10
queue_size: 5000
compression: gzip
# --- OTLP/HTTP Exporter ---
otlphttp:
endpoint: "https://otlp-gateway.example.com:4318"
headers:
X-API-Key: "${env:API_KEY}"
compression: gzip
# --- Prometheus Remote Write Exporter ---
prometheusremotewrite:
endpoint: "http://prometheus.monitoring:9090/api/v1/write"
tls:
insecure: true
resource_to_telemetry_conversion:
enabled: true
add_metric_suffixes: true
# --- Loki Exporter ---
loki:
endpoint: "http://loki.monitoring:3100/loki/api/v1/push"
default_labels_enabled:
exporter: true
job: true
instance: true
level: true
# --- Debug Exporter ---
# 開発・デバッグ時にコンソール出力
debug:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
# --- Kafka Exporter ---
kafka:
brokers:
- "kafka-1:9092"
- "kafka-2:9092"
topic: "otlp-spans-processed"
encoding: otlp_proto
producer:
max_message_bytes: 10000000
compression: snappy
# ==============================================================
# Connectors: パイプライン間の接続
# ==============================================================
connectors:
# Span から RED メトリクスを生成
spanmetrics:
histogram:
explicit:
buckets: [5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s]
dimensions:
- name: http.request.method
- name: http.route
- name: http.response.status_code
- name: service.name
exemplars:
enabled: true
metrics_flush_interval: 30s
# カウントコネクター
count:
traces:
span.count:
description: "スパン数のカウント"
logs:
log.record.count:
description: "ログレコード数のカウント"
# ==============================================================
# Service: パイプライン定義
# ==============================================================
service:
extensions: [health_check, pprof, zpages, memory_ballast]
telemetry:
logs:
level: "info"
development: false
encoding: "json"
metrics:
level: "detailed"
address: "0.0.0.0:8888"
pipelines:
# --- トレースパイプライン ---
traces:
receivers: [otlp, kafka]
processors:
- memory_limiter # 最初に配置
- k8sattributes
- resource
- attributes
- filter
- tail_sampling
- batch # 最後に配置
exporters: [otlp, debug]
# --- メトリクスパイプライン ---
metrics:
receivers: [otlp, prometheus, hostmetrics]
processors:
- memory_limiter
- k8sattributes
- resource
- attributes
- filter
- batch
exporters: [prometheusremotewrite, debug]
# --- メトリクスパイプライン (spanmetrics から) ---
metrics/spanmetrics:
receivers: [spanmetrics]
processors: [batch]
exporters: [prometheusremotewrite]
# --- ログパイプライン ---
logs:
receivers: [otlp, filelog]
processors:
- memory_limiter
- k8sattributes
- resource
- attributes
- filter
- transform
- batch
exporters: [loki, debug]
# --- Connector: traces → spanmetrics → metrics ---
traces/spanmetrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [spanmetrics]
12. Connectors
12.1 概要
Connector は OpenTelemetry Collector v0.86.0 で導入された機能で、あるパイプラインの出力を別のパイプラインの入力に接続する。Exporter と Receiver の両方の役割を持つ。
┌──────────────────────────────────────────────────────────────────┐
│ Connector の役割 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Pipeline A (Traces) Pipeline B (Metrics) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Receiver │─▶│Processor │─▶ ┌────────────┐ ─▶│ Exporter │ │
│ └──────────┘ └──────────┘ │ Connector │ └──────────┘ │
│ │(spanmetrics)│ │
│ │ │ │
│ │ Exporter側: │ │
│ │ traces受取 │ │
│ │ Receiver側: │ │
│ │ metrics生成│ │
│ └────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
12.2 主な Connector
| Connector | 入力 → 出力 | 説明 |
|---|---|---|
| spanmetrics | Traces → Metrics | スパンから RED メトリクスを生成 |
| count | Any → Metrics | テレメトリアイテムのカウント |
| routing | Any → Any | 条件に基づくルーティング |
| forward | Any → Any | パイプライン間のデータ転送 |
| failover | Any → Any | フェイルオーバー処理 |
| servicegraph | Traces → Metrics | サービスグラフメトリクスを生成 |
12.3 spanmetrics Connector の設定例
connectors:
spanmetrics:
histogram:
explicit:
buckets: [2ms, 4ms, 6ms, 8ms, 10ms, 50ms, 100ms, 200ms, 500ms, 1s, 5s, 10s]
dimensions:
- name: http.request.method
- name: http.route
- name: http.response.status_code
- name: rpc.grpc.status_code
- name: service.name
dimensions_cache_size: 1000
aggregation_temporality: "AGGREGATION_TEMPORALITY_CUMULATIVE"
exemplars:
enabled: true
metrics_flush_interval: 15s
namespace: "traces.spanmetrics"
service:
pipelines:
# トレースを受信 → spanmetrics Connector にエクスポート
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp, spanmetrics] # spanmetrics は Exporter として動作
# spanmetrics Connector からメトリクスを受信
metrics/spanmetrics:
receivers: [spanmetrics] # spanmetrics は Receiver として動作
processors: [batch]
exporters: [prometheusremotewrite]
生成されるメトリクス:
traces.spanmetrics.calls— リクエスト数(Counter)traces.spanmetrics.duration— レイテンシ分布(Histogram)traces.spanmetrics.size— スパンサイズ(Histogram、オプション)
13. OTLP プロトコル (gRPC vs HTTP)
13.1 OTLP の概要
OTLP (OpenTelemetry Protocol) は、テレメトリデータを転送するためのOpenTelemetry 標準プロトコルである。
13.2 gRPC vs HTTP/protobuf vs HTTP/JSON
| 特性 | OTLP/gRPC | OTLP/HTTP (protobuf) | OTLP/HTTP (JSON) |
|---|---|---|---|
| ポート | 4317 | 4318 | 4318 |
| エンコーディング | Protocol Buffers | Protocol Buffers | JSON |
| パフォーマンス | 最高 | 高い | 低い(デバッグ用) |
| ストリーミング | 対応 | 非対応 | 非対応 |
| ロードバランサー | L4/L7 (HTTP/2) | L7 | L7 |
| ファイアウォール | HTTP/2 対応必要 | HTTP/1.1 対応 | HTTP/1.1 対応 |
| 圧縮 | gzip | gzip | gzip |
| ブラウザ対応 | 非対応 | 対応 | 対応 |
13.3 エンドポイント
# gRPC
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
→ traces: http://collector:4317 (gRPC service)
→ metrics: http://collector:4317 (gRPC service)
→ logs: http://collector:4317 (gRPC service)
# HTTP
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
→ traces: http://collector:4318/v1/traces (POST)
→ metrics: http://collector:4318/v1/metrics (POST)
→ logs: http://collector:4318/v1/logs (POST)
13.4 OTLP リクエスト/レスポンスの例
# HTTP/JSON リクエストの例 (POST /v1/traces)
{
"resourceSpans": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "order-service" } },
{ "key": "service.version", "value": { "stringValue": "2.1.0" } }
]
},
"scopeSpans": [
{
"scope": {
"name": "order-service",
"version": "2.1.0"
},
"spans": [
{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"parentSpanId": "",
"name": "HTTP GET /api/orders",
"kind": 2,
"startTimeUnixNano": "1704067200000000000",
"endTimeUnixNano": "1704067200350000000",
"attributes": [
{ "key": "http.request.method", "value": { "stringValue": "GET" } },
{ "key": "http.response.status_code", "value": { "intValue": "200" } }
],
"status": { "code": 1 }
}
]
}
]
}
]
}
13.5 推奨選択基準
選択フローチャート:
ブラウザ/フロントエンドから送信?
│
├─ Yes → OTLP/HTTP (protobuf or JSON)
│
└─ No
│
HTTP/2 がファイアウォールで制限?
│
├─ Yes → OTLP/HTTP (protobuf)
│
└─ No
│
高スループットが必要?
│
├─ Yes → OTLP/gRPC(推奨)
│
└─ No → OTLP/gRPC or HTTP(どちらでも可)
14. サンプリング戦略
14.1 サンプリングの種類
┌──────────────────────────────────────────────────────────────────┐
│ サンプリングポイント │
├──────────────────────────────────────────────────────────────────┤
│ │
│ アプリケーション (SDK) Collector (Gateway) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Head Sampling │ │ Tail Sampling │ │
│ │ │ │ │ │
│ │ • TraceIdRatio │ OTLP │ • 全スパン収集後に │ │
│ │ • ParentBased │ ──────▶│ 判断 │ │
│ │ • AlwaysOn/Off │ │ • エラー含むトレース │ │
│ │ │ │ を100%保持 │ │
│ │ ⚡ 低オーバーヘッド │ │ • 高レイテンシを保持│ │
│ │ ❌ 結果を見ずに判断 │ │ │ │
│ └─────────────────────┘ │ ⚡ 正確な判断 │ │
│ │ ❌ 高メモリ使用量 │ │
│ └─────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
14.2 Head Sampling(SDK側)
from opentelemetry.sdk.trace.sampling import (
TraceIdRatioBased,
ParentBased,
ALWAYS_ON,
ALWAYS_OFF,
)
# 1. TraceIdRatioBased: トレースIDに基づく確率的サンプリング
sampler_10_percent = TraceIdRatioBased(0.1) # 10%
# 2. ParentBased: 親スパンのサンプリング決定を尊重
sampler = ParentBased(
root=TraceIdRatioBased(0.1), # ルートスパン: 10%
remote_parent_sampled=ALWAYS_ON, # 親がサンプルされている: 常にON
remote_parent_not_sampled=ALWAYS_OFF, # 親がサンプルされていない: 常にOFF
local_parent_sampled=ALWAYS_ON,
local_parent_not_sampled=ALWAYS_OFF,
)
# TracerProvider に設定
from opentelemetry.sdk.trace import TracerProvider
provider = TracerProvider(sampler=sampler, resource=resource)
環境変数での設定:
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
export OTEL_TRACES_SAMPLER_ARG="0.1" # 10%
14.3 Tail Sampling(Collector側)
Tail Sampling は Collector の tail_sampling プロセッサーで実現する。トレース全体のスパンが揃ってからサンプリング判断を行うため、より賢い判断が可能である。
前述の Collector 設定例(セクション11)の tail_sampling プロセッサーを参照。
14.4 サンプリング戦略の選択指針
| 戦略 | 適用場面 | メリット | デメリット |
|---|---|---|---|
| Always On | 開発環境、低トラフィック | 全データ保持 | コスト大 |
| Head (比率) | 高トラフィック環境 | 低オーバーヘッド | エラーを見逃す可能性 |
| ParentBased | マイクロサービス | 一貫したサンプリング | ルートでの判断に依存 |
| Tail Sampling | 本番環境(推奨) | 最適な判断 | メモリ・CPU消費大 |
| Head + Tail | 大規模本番環境 | バランス良い | 設定が複雑 |
15. Kubernetes デプロイメント (OTel Operator)
15.1 OpenTelemetry Operator 概要
OpenTelemetry Operator は Kubernetes Operator パターンで実装され、以下の CRD(Custom Resource Definition)を提供する:
- OpenTelemetryCollector: Collector の自動デプロイ・管理
- Instrumentation: Pod への自動インストルメンテーション注入
15.2 Operator のインストール
# cert-manager のインストール(前提条件)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
# OpenTelemetry Operator のインストール
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml
# または Helm を使用
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm install opentelemetry-operator open-telemetry/opentelemetry-operator \
--namespace opentelemetry-operator-system \
--create-namespace \
--set "manager.collectorImage.repository=otel/opentelemetry-collector-contrib" \
--set admissionWebhooks.certManager.enabled=true
15.3 OpenTelemetryCollector CRD
DaemonSet モード(Agent)
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel-agent
namespace: monitoring
spec:
mode: daemonset # daemonset | deployment | statefulset | sidecar
image: otel/opentelemetry-collector-contrib:0.96.0
# リソース制限
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
# 環境変数
env:
- name: KUBE_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
# ボリュームマウント(ホストログ収集用)
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
# Tolerations(全ノードにデプロイ)
tolerations:
- operator: Exists
# Collector 設定
config:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
hostmetrics:
collection_interval: 30s
scrapers:
cpu: {}
memory: {}
disk: {}
network: {}
processors:
memory_limiter:
check_interval: 1s
limit_mib: 400
spike_limit_mib: 100
batch:
send_batch_size: 1024
timeout: 5s
k8sattributes:
auth_type: serviceAccount
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.deployment.name
- k8s.node.name
exporters:
otlp:
endpoint: "otel-gateway.monitoring:4317"
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlp]
metrics:
receivers: [otlp, hostmetrics]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlp]
Deployment モード(Gateway)
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel-gateway
namespace: monitoring
spec:
mode: deployment
replicas: 3
image: otel/opentelemetry-collector-contrib:0.96.0
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "500m"
memory: "1Gi"
# Horizontal Pod Autoscaler
autoscaler:
minReplicas: 2
maxReplicas: 10
targetCPUUtilization: 70
targetMemoryUtilization: 80
# PodDisruptionBudget
podDisruptionBudget:
minAvailable: 1
config:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
processors:
memory_limiter:
check_interval: 1s
limit_mib: 3072
spike_limit_mib: 1024
batch:
send_batch_size: 8192
timeout: 5s
tail_sampling:
decision_wait: 30s
num_traces: 100000
policies:
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
- name: slow
type: latency
latency:
threshold_ms: 1000
- name: default
type: probabilistic
probabilistic:
sampling_percentage: 10
exporters:
otlp/tempo:
endpoint: "tempo.monitoring:4317"
tls:
insecure: true
prometheusremotewrite:
endpoint: "http://prometheus.monitoring:9090/api/v1/write"
loki:
endpoint: "http://loki.monitoring:3100/loki/api/v1/push"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
15.4 Instrumentation CRD(自動インストルメンテーション)
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: otel-instrumentation
namespace: default
spec:
# 共通設定
exporter:
endpoint: "http://otel-agent.monitoring:4317"
propagators:
- tracecontext
- baggage
sampler:
type: parentbased_traceidratio
argument: "0.25" # 25% サンプリング
# リソース属性
resource:
addK8sUIDAttributes: true
# 環境変数(全言語共通)
env:
- name: OTEL_EXPORTER_OTLP_TIMEOUT
value: "10000"
# --- 言語別設定 ---
# Python
python:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:latest
env:
- name: OTEL_PYTHON_LOG_CORRELATION
value: "true"
- name: OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED
value: "true"
# Java
java:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
env:
- name: OTEL_JAVAAGENT_DEBUG
value: "false"
- name: OTEL_INSTRUMENTATION_JDBC_ENABLED
value: "true"
# Node.js
nodejs:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:latest
# .NET
dotnet:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-dotnet:latest
# Go (eBPF ベース)
go:
image: ghcr.io/open-telemetry/opentelemetry-go-instrumentation/autoinstrumentation-go:latest
15.5 Pod アノテーション
Pod またはNamespace にアノテーションを付与することで、自動インストルメンテーションを有効化する。
# --- Pod レベルでの有効化 ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
annotations:
# Python アプリケーションの自動インストルメンテーション
instrumentation.opentelemetry.io/inject-python: "true"
# Java アプリケーションの場合
# instrumentation.opentelemetry.io/inject-java: "true"
# Node.js アプリケーションの場合
# instrumentation.opentelemetry.io/inject-nodejs: "true"
# .NET アプリケーションの場合
# instrumentation.opentelemetry.io/inject-dotnet: "true"
# Go アプリケーションの場合
# instrumentation.opentelemetry.io/inject-go: "true"
# 特定の Instrumentation CR を指定する場合
# instrumentation.opentelemetry.io/inject-python: "monitoring/otel-instrumentation"
# コンテナ名を指定(マルチコンテナ Pod の場合)
# instrumentation.opentelemetry.io/container-names: "app,worker"
spec:
containers:
- name: app
image: order-service:2.1.0
ports:
- containerPort: 8080
env:
- name: OTEL_SERVICE_NAME
value: "order-service"
resources:
limits:
cpu: "500m"
memory: "256Mi"
# --- Namespace レベルでの有効化 ---
# Namespace 内のすべての Pod に自動適用
apiVersion: v1
kind: Namespace
metadata:
name: production
annotations:
instrumentation.opentelemetry.io/inject-java: "true"
15.6 Operator が行う処理
Operator は Pod アノテーションを検出すると、以下を自動的に行う:
- Init Container の注入: インストルメンテーションライブラリをダウンロード
- 環境変数の設定:
OTEL_*環境変数を Pod に注入 - ボリュームマウント: インストルメンテーションライブラリのマウント
- Java の場合:
-javaagentJVM 引数をJAVA_TOOL_OPTIONSに追加
Operator が変換する Pod:
Before: After:
┌──────────────┐ ┌──────────────────────────┐
│ Container: │ │ InitContainer: │
│ app │ │ otel-instrumentation │
│ │ │ (ライブラリDL) │
│ │ ├──────────────────────────┤
│ │ │ Container: │
│ │ │ app │
│ │ ──Operator──▶│ env: │
│ │ │ OTEL_SERVICE_NAME │
│ │ │ OTEL_EXPORTER_* │
│ │ │ JAVA_TOOL_OPTIONS │
│ │ │ volumeMounts: │
│ │ │ /otel-auto-instr │
└──────────────┘ └──────────────────────────┘
16. セマンティック規約 (Semantic Conventions)
16.1 概要
セマンティック規約は、テレメトリ属性の命名とデータ型を標準化する仕様である。これにより、異なるライブラリ・フレームワーク・言語間で一貫したテレメトリデータが生成される。
16.2 主要な属性カテゴリ
HTTP セマンティック規約
# HTTP サーバースパン属性(Stable)
http.request.method: "GET" # HTTP メソッド
http.response.status_code: 200 # ステータスコード
url.scheme: "https" # スキーム
url.path: "/api/orders/42" # パス
url.query: "include=items" # クエリ文字列
http.route: "/api/orders/{orderId}" # ルートテンプレート
server.address: "api.example.com" # サーバーホスト名
server.port: 443 # サーバーポート
http.request.body.size: 1234 # リクエストボディサイズ
http.response.body.size: 5678 # レスポンスボディサイズ
network.protocol.name: "http" # プロトコル名
network.protocol.version: "1.1" # プロトコルバージョン
user_agent.original: "Mozilla/5.0..." # User-Agent
client.address: "192.168.1.1" # クライアントIP
データベースセマンティック規約
# データベーススパン属性
db.system: "postgresql" # DB システム
db.namespace: "orders_db" # DB 名/スキーマ
db.operation.name: "SELECT" # 操作名
db.query.text: "SELECT * FROM orders WHERE id = $1" # クエリテキスト
db.query.parameter.id: "42" # クエリパラメータ
server.address: "db.example.com" # DB ホスト
server.port: 5432 # DB ポート
db.response.status_code: "00000" # SQLSTATE
メッセージングセマンティック規約
# メッセージングスパン属性
messaging.system: "kafka" # メッセージングシステム
messaging.destination.name: "order-events" # トピック/キュー名
messaging.operation.name: "publish" # 操作 (publish/receive/process)
messaging.message.id: "msg-123" # メッセージID
messaging.kafka.consumer.group: "order-processors"
messaging.kafka.message.offset: 42
messaging.kafka.destination.partition: 3
messaging.batch.message_count: 10 # バッチサイズ
リソースセマンティック規約
# サービス
service.name: "order-service" # 必須
service.version: "2.1.0"
service.namespace: "production"
service.instance.id: "order-service-7d9f8c6b5-x2k4m"
# Kubernetes
k8s.cluster.name: "production-cluster"
k8s.namespace.name: "orders"
k8s.deployment.name: "order-service"
k8s.pod.name: "order-service-7d9f8c6b5-x2k4m"
k8s.pod.uid: "abc-123-def-456"
k8s.container.name: "app"
k8s.node.name: "node-1"
# クラウド
cloud.provider: "aws" # aws / gcp / azure
cloud.region: "ap-northeast-1"
cloud.availability_zone: "ap-northeast-1a"
cloud.account.id: "123456789012"
cloud.platform: "aws_eks"
# ホスト
host.name: "ip-10-0-1-42"
host.id: "i-0abc123def456"
host.type: "m5.xlarge"
host.arch: "amd64"
# OS
os.type: "linux"
os.version: "5.15.0"
# プロセス
process.pid: 1234
process.command: "java"
process.runtime.name: "OpenJDK Runtime Environment"
process.runtime.version: "17.0.1"
# テレメトリ SDK
telemetry.sdk.name: "opentelemetry"
telemetry.sdk.language: "python"
telemetry.sdk.version: "1.22.0"
16.3 メトリクスセマンティック規約
# HTTP メトリクス(Stable)
http.server.request.duration: # Histogram (秒)
unit: "s"
description: "HTTP サーバーリクエストの処理時間"
http.server.active_requests: # UpDownCounter
unit: "1"
description: "アクティブな HTTP リクエスト数"
http.client.request.duration: # Histogram (秒)
unit: "s"
description: "HTTP クライアントリクエストの時間"
# RPC メトリクス
rpc.server.duration: # Histogram (ミリ秒)
unit: "ms"
rpc.client.duration: # Histogram (ミリ秒)
unit: "ms"
# システムメトリクス
system.cpu.utilization: # Gauge
unit: "1"
system.memory.usage: # UpDownCounter
unit: "By"
system.disk.io: # Counter
unit: "By"
17. テレメトリ相関 (Exemplars)
17.1 Exemplars の概要
Exemplar はメトリクスデータポイントにトレースへのリンクを埋め込む仕組みである。集約されたメトリクスから個別のトレースにドリルダウンできるようになる。
┌──────────────────────────────────────────────────────────────────┐
│ Exemplars の役割 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ メトリクス: http.server.request.duration │
│ │
│ Histogram: │
│ p50 = 45ms │
│ p90 = 120ms │
│ p99 = 850ms ← この値に対応するトレースは? │
│ │ │
│ ▼ Exemplar │
│ ┌──────────────────────────────────────┐ │
│ │ trace_id: 4bf92f...4736 │ │
│ │ span_id: 00f067...02b7 │ │
│ │ value: 847ms │ │
│ │ timestamp: 2025-01-15T10:30:45Z │ │
│ │ attributes: │ │
│ │ http.route: "/api/orders/{id}" │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ リンク先のトレース │
│ ┌──────────────────────────────────────┐ │
│ │ Trace: 4bf92f...4736 │ │
│ │ ├─ HTTP GET /api/orders/42 (847ms) │ │
│ │ │ ├─ getOrder (780ms) │ │
│ │ │ │ ├─ DB query (650ms) ← ボトルネック!│ │
│ │ │ │ └─ cache miss │ │
│ │ │ └─ serialize (60ms) │ │
│ └──────────────────────────────────────┘ │
│ │
│ メトリクス → Exemplar → トレース でドリルダウン │
│ │
└──────────────────────────────────────────────────────────────────┘
17.2 Exemplars の設定
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.view import View
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
# Exemplars はデフォルトで有効(サンプリングされたスパンに対して)
# SDK の設定時に自動的にトレースコンテキストが Exemplar として関連付けられる
# 環境変数での設定
# OTEL_METRICS_EXEMPLAR_FILTER=trace_based # デフォルト: サンプリングされたトレースのみ
# OTEL_METRICS_EXEMPLAR_FILTER=always_on # 常に Exemplar を記録
# OTEL_METRICS_EXEMPLAR_FILTER=always_off # Exemplar を無効化
17.3 Grafana での活用
Grafana では、Prometheus メトリクスの Exemplar をクリックすると、対応するトレース(Tempo/Jaeger)に直接遷移できる。
Grafana Dashboard:
┌──────────────────────────────────────────┐
│ HTTP Request Duration (p99) │
│ │
│ ▄▄▄▄▆▆▆▇▇█████▇▇▆▆▄▄▄▄ │
│ ▂▂▃▃▄▄▅▅▆▆▇▇████▇▇▆▆▅▅▄▄▃▃▂▂ │
│ ◆ ◆ ◆◆ ◆ ◆ ← Exemplar点 │
│ │ │
│ ▼ クリック │
│ ┌─────────────────────────────────┐ │
│ │ Trace: 4bf92f... │ │
│ │ Duration: 847ms │ │
│ │ Service: order-service │ │
│ │ [View in Tempo →] │ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────────┘
18. ベストプラクティス
18.1 SDK 設定のベストプラクティス
| カテゴリ | 推奨事項 |
|---|---|
| Resource | service.name は必ず設定する。deployment.environment、service.version も推奨 |
| サンプリング | 本番では parentbased_traceidratio を使用。100% サンプリングは開発環境のみ |
| バッチ処理 | BatchSpanProcessor を使用(SimpleSpanProcessor は開発環境のみ) |
| 環境変数 | コード内のハードコーディングを避け、環境変数で設定する |
| シャットダウン | アプリ終了時に必ず provider.shutdown() を呼ぶ(データ損失防止) |
18.2 Collector 設定のベストプラクティス
# 推奨: Processor の順序
processors:
# 1. memory_limiter: 最初に配置(OOM 防止)
# 2. k8sattributes: メタデータ付与
# 3. resource/attributes: 属性の追加・変更
# 4. filter: 不要データの除外
# 5. tail_sampling: サンプリング(Gateway のみ)
# 6. batch: 最後に配置(エクスポート効率化)
18.3 命名規約
# スパン名
✅ "HTTP GET /api/orders/{orderId}" # 低カーディナリティ
❌ "HTTP GET /api/orders/12345" # 高カーディナリティ(具体的なID)
✅ "process_order" # 操作名
❌ "OrderService.processOrder()" # クラス名.メソッド名は冗長
# 属性名
✅ "order.id" # セマンティック規約に準拠
✅ "http.request.method" # ドット区切り
❌ "order_id" # アンダースコア(非推奨)
❌ "orderID" # キャメルケース(非推奨)
# メトリクス名
✅ "http.server.request.duration" # ドット区切り、単位は別途指定
❌ "http_server_request_duration_ms" # 単位を名前に含めない
18.4 パフォーマンス考慮事項
| 項目 | 推奨 |
|---|---|
| 属性のカーディナリティ | 属性値の種類を制限する(user_id 等は避ける) |
| スパンの粒度 | ループ内で大量のスパンを作らない |
| Baggage のサイズ | 最小限に保つ(全リクエストに付加される) |
| メトリクスの次元数 | 属性の組み合わせ爆発に注意(カーディナリティ爆発) |
| ログの量 | 高頻度のログはサンプリングを検討 |
| Collector のリソース | memory_limiter は必ず設定する |
18.5 セキュリティ考慮事項
# NG: 機密情報を属性に含めない
span.set_attribute("user.password", password) # ❌
span.set_attribute("db.query.text", "... WHERE ssn='123-45-6789'") # ❌
baggage.set("auth.token", "Bearer abc123") # ❌
# OK: 機密情報をマスクまたは除外
# Collector の attributes processor でフィルタリング
processors:
attributes:
actions:
- key: http.request.header.authorization
action: delete
- key: http.request.header.cookie
action: delete
- key: db.query.text
action: hash
18.6 段階的な導入戦略
Phase 1: 基盤構築(1-2週間)
──────────────────────────
• OTel Collector を Agent + Gateway 構成でデプロイ
• 1-2 サービスで自動インストルメンテーションを有効化
• トレースバックエンド(Tempo/Jaeger)で確認
Phase 2: 可視性の拡大(2-4週間)
─────────────────────────────
• 全サービスに自動インストルメンテーションを展開
• メトリクスの収集を開始(Prometheus + OTel)
• ログの相関(trace_id 付きログ)を設定
Phase 3: 最適化(4-8週間)
────────────────────────
• Tail Sampling の導入
• 重要なサービスに手動インストルメンテーション追加
• ビジネスメトリクスの定義と収集
• ダッシュボードとアラートの構築
Phase 4: 高度な活用(継続的)
──────────────────────────
• Exemplars によるメトリクス → トレースの相関
• spanmetrics Connector で RED メトリクス自動生成
• SLO (Service Level Objectives) の監視
• 障害調査ワークフローの確立
まとめ
OpenTelemetry は、クラウドネイティブなオブザーバビリティの標準として急速に普及している。本ガイドで解説した主要なポイントを振り返る:
- ベンダー非依存の標準: 一度のインストルメンテーションで、任意のバックエンドにデータを送信可能
- 三本柱の統合: トレース・メトリクス・ログを統一的なフレームワークで扱う
- 自動 + 手動: 自動インストルメンテーションで迅速に可視性を獲得し、手動で補完
- Collector の柔軟性: Receiver → Processor → Exporter パイプラインで自在にデータを加工・転送
- Kubernetes ネイティブ: Operator と CRD による宣言的な管理
- 段階的な導入: 小さく始めて、段階的に深い可視性を獲得する
SRE チームにとって、OpenTelemetry は障害の早期検知・迅速な原因特定・システムの信頼性向上に不可欠なツールとなるだろう。