概要
TracerProvider を正しく初期化する
trace.get_tracer() は set_tracer_provider() 後に呼ぶ。起動時に Resource・BatchSpanProcessor・OTLPSpanExporter を一度だけ設定する。SimpleSpanProcessor は同期エクスポートでリクエストをブロックするため本番禁止。
record_exception + set_status(ERROR) でエラー span を可視化
span.record_exception(exc) はスタックトレースを span event として記録。span.set_status(Status(StatusCode.ERROR, msg)) を設定しないと Tempo / DataDog でエラー span が赤くならない。両方セットで初めて障害時のトレースが機能する。
MeterProvider でカウンタ + ヒストグラム
PeriodicExportingMetricReader を使い 30 秒間隔で OTLP へ push。campaigns.dispatched(Counter)で成功/エラー数、campaigns.dispatch_duration_ms(Histogram)で p50/p95/p99 レイテンシを取得し SLO 監視に活用する。
Propagator でマイクロサービス間トレースを繋ぐ
CompositePropagator([TraceContextTextMapPropagator(), BaggagePropagator()]) を設定しないと、Pub/Sub → Cloud Run 間で traceparent ヘッダーが伝わらずトレースが断絶する。structlog の _add_otel_context プロセッサで ログに trace_id を自動付与し、ログ↔トレース相互リンクを実現する。
問題
ECサイト MOps チームでは、キャンペーン配信 API(Cloud Run v2)と Argo Workflows バッチの可観測性が不十分で、「配信エラーが急増したが原因特定に 2 時間かかった」という障害が発生した。以下の「悪いコード」は現在の計装実装である。
問題点を全て洗い出し、OpenTelemetry 1.x(Python SDK)を使って Bad→Good にリファクタリングしてください。
制約・前提条件
- Python 3.12+、
opentelemetry-sdk 1.x、opentelemetry-exporter-otlp-proto-grpc - OTLP エンドポイント:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317(環境変数) - Propagator: W3C TraceContext + Baggage の両方を設定
MeterProvider:campaigns.dispatchedカウンタ(属性:campaign_id,status=success|error)とcampaigns.dispatch_duration_msヒストグラムを計測structlogで JSON ログ出力し、trace_id/span_idをログに自動付与BatchSpanProcessorを使用(SimpleSpanProcessorは本番禁止)record_exception+set_status(StatusCode.ERROR)でエラーを正しく記録
期待する回答形式: (1) 問題点の列挙(番号付き)、(2) 改善後コード(Google スタイル docstring・インラインコメント・名前付き定数含む)、(3) 実行例(input→output)、(4) 適用した OTel パターン名
悪いコード (Before)
# campaign_dispatcher.py ── BAD 実装
import logging
import time
import opentelemetry.trace as trace
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ① グローバルに tracer を直接取得(初期化なし)
# Provider 未設定 → NoopTracer になってトレース記録なし
tracer = trace.get_tracer("campaign")
def dispatch_campaign(
campaign_id: str,
user_ids: list[str],
) -> dict:
# ② span に set_attribute がない
# → campaign_id を検索できない
with tracer.start_as_current_span("dispatch") as span:
try:
results = []
for uid in user_ids:
# ③ 子 span がない
# → どのユーザーで失敗したか不明
ok = _send_push(uid, campaign_id)
results.append(ok)
success = sum(results)
# ④ print + 非構造化ログ
# → trace_id が付かない
print(f"done: {success}/{len(user_ids)}")
return {
"success": success,
"total": len(user_ids),
}
except Exception as e:
# ⑤ 例外握りつぶし
# → record_exception なし
# → set_status(ERROR) なし
# → Tempo でエラー span が赤くならない
logger.error(f"error: {e}")
return {"success": 0, "total": len(user_ids)}
def _send_push(uid: str, campaign_id: str) -> bool:
time.sleep(0.01) # mock
return True
# ⑥ MeterProvider 未設定
# → カウンタ / ヒストグラムが計測されない
# ⑦ Propagator 未設定
# → Pub/Sub → Cloud Run のトレースが切れる
- ①
configure_otel()を起動時一度だけ呼びset_tracer_provider()後にget_tracer() - ②
span.set_attribute("campaign.id", ...)で contextual attribute 付与 - ③ ユーザーごとに子 span →
user.idattribute → per-user 障害特定 - ④
structlogJSON ログ +_add_otel_contextプロセッサでtrace_id自動付与 - ⑤
child_span.record_exception(exc)+set_status(ERROR)でエラーを正式記録 - ⑥
MeterProvider+PeriodicExportingMetricReader+ カウンタ・ヒストグラム定義 - ⑦
CompositePropagator([TraceContextTextMapPropagator(), BaggagePropagator()])
ヒント
ヒント 1 — 方向性(答えを含まない)
計装の「初期化」「計測」「エラー処理」「ログ相関」「伝播」の 5 つの観点で問題を分類する。get_tracer() は TracerProvider が設定された 後 に呼ぶことが重要。
ヒント 2 — アプローチ(キーとなる構造)
TracerProvider(resource=Resource({...}))+BatchSpanProcessor(OTLPSpanExporter(...))→set_tracer_provider()MeterProvider(resource=..., metric_readers=[PeriodicExportingMetricReader(...)])→metrics.set_meter_provider()set_global_textmap(CompositePropagator([TraceContextTextMapPropagator(), BaggagePropagator()]))- structlog の
processorsに_add_otel_contextを追加してトレース ID を注入 - 子 span:
tracer.start_as_current_span("send_push", attributes={"user.id": uid})
ヒント 3 — 誘導(コードの骨格)
def configure_otel(service_name: str) -> None:
resource = Resource(attributes={
SERVICE_NAME: service_name,
"service.version": "1.0.0",
})
exporter = OTLPSpanExporter(
endpoint=os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"],
insecure=True,
)
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(exporter))
set_tracer_provider(provider)
# MeterProvider も同様に初期化 ...
set_global_textmap(CompositePropagator([
TraceContextTextMapPropagator(),
BaggagePropagator(),
]))
問題点分析(7 点)
get_tracer()
TracerProvider を設定する前に get_tracer() を呼ぶと NoopTracer が返り、全 span が無視される。configure_otel() → set_tracer_provider() 後にモジュール変数として取得する。
set_attribute なし
Tempo / DataDog で campaign.id を検索できない。span.set_attribute("campaign.id", campaign_id) と campaign.target_count を付与する。
「u003 だけ失敗」という per-user 問題を特定できない。ループ内で start_as_current_span("campaign.send_push", attributes={"user.id": uid}) として子 span を作成する。
print + 非構造化ログ
Loki 等でのフィルタ不可。structlog JSON + _add_otel_context プロセッサで trace_id / span_id を自動付与し、ログ↔トレース相互リンクを実現する。
record_exception + set_status(ERROR) なし
例外をキャッチしても OTel に記録しないと Tempo でエラー span が赤くならない。child_span.record_exception(exc) でスタックトレースを span event として記録し、set_status(Status(StatusCode.ERROR)) を設定する。
MeterProvider 未設定でメトリクスなし
配信成功率・レイテンシの Prometheus/DataDog 監視が不可能。MeterProvider + PeriodicExportingMetricReader を初期化し、カウンタ(campaigns.dispatched)とヒストグラム(campaigns.dispatch_duration_ms)を定義する。
Pub/Sub メッセージに traceparent ヘッダーが含まれていても伝播されない。set_global_textmap(CompositePropagator([TraceContextTextMapPropagator(), BaggagePropagator()])) を設定する。
OTel 計装アーキテクチャ図(SVG)
模範解答(GOOD 実装)
from __future__ import annotations
import os
from typing import Final
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.propagate import set_global_textmap
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor # ← SimpleSpanProcessor は本番 NG
from opentelemetry.trace import set_tracer_provider
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.baggage.propagation import BaggagePropagator
SERVICE_NAME_VALUE: Final[str] = "campaign-dispatcher"
METRIC_DISPATCHED: Final[str] = "campaigns.dispatched"
METRIC_DURATION_MS: Final[str] = "campaigns.dispatch_duration_ms"
def configure_otel(service_name: str = SERVICE_NAME_VALUE) -> None:
"""TracerProvider / MeterProvider / Propagator を初期化する(起動時一度)."""
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
resource = Resource(attributes={
SERVICE_NAME: service_name,
"service.version": "2.1.0",
"deployment.environment": os.environ.get("ENV", "development"),
})
# ── TracerProvider ─────────────────────────────────────────────
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint, insecure=True))
)
set_tracer_provider(tracer_provider) # ← これより後に get_tracer() を呼ぶ
# ── MeterProvider ──────────────────────────────────────────────
metrics.set_meter_provider(MeterProvider(
resource=resource,
metric_readers=[PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=endpoint, insecure=True),
export_interval_millis=30_000, # 30 秒ごとに push
)],
))
# ── Propagator(W3C TraceContext + Baggage)────────────────────
# Pub/Sub → Cloud Run の traceparent ヘッダーを引き継ぐ
set_global_textmap(CompositePropagator([
TraceContextTextMapPropagator(),
BaggagePropagator(),
]))
import logging
import structlog
def _add_otel_context(logger, method_name, event_dict: dict) -> dict:
"""structlog プロセッサ: 現在の trace_id / span_id を付与."""
span = trace.get_current_span()
ctx = span.get_span_context()
if ctx.is_valid:
event_dict["trace_id"] = format(ctx.trace_id, "032x") # Loki ↔ Tempo リンク用
event_dict["span_id"] = format(ctx.span_id, "016x")
return event_dict
def configure_structlog() -> None:
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
_add_otel_context, # ← trace_id / span_id を自動注入
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(), # JSON 出力 → Loki でフィルタ可能
],
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
logger_factory=structlog.PrintLoggerFactory(),
)
from opentelemetry.trace.status import Status, StatusCode
# ── Provider 初期化後にモジュール変数として取得 ──────────────────────────
_tracer = trace.get_tracer(__name__)
_meter = metrics.get_meter(__name__)
_log = structlog.get_logger(__name__)
# カウンタ: success / error 別に集計
_dispatched_counter = _meter.create_counter(
name=METRIC_DISPATCHED,
description="キャンペーン配信数",
unit="1",
)
# ヒストグラム: p50/p95/p99 レイテンシ
_duration_histogram = _meter.create_histogram(
name=METRIC_DURATION_MS,
description="配信所要時間",
unit="ms",
)
def dispatch_campaign(campaign_id: str, user_ids: list[str]) -> dict[str, int]:
"""キャンペーンを指定ユーザーにディスパッチする."""
import time
start_ms = time.monotonic() * 1000
with _tracer.start_as_current_span("campaign.dispatch") as span:
# ② contextual attribute: Tempo で campaign_id 検索可能になる
span.set_attribute("campaign.id", campaign_id)
span.set_attribute("campaign.target_count", len(user_ids))
success, errors = 0, []
for uid in user_ids:
# ③ 子 span で per-user レイテンシ + エラー特定
with _tracer.start_as_current_span(
"campaign.send_push",
attributes={"user.id": uid, "campaign.id": campaign_id},
) as child:
try:
_send_push(uid, campaign_id)
success += 1
child.set_attribute("send.status", "success")
except Exception as exc:
errors.append(uid)
child.record_exception(exc) # スタックトレースを event 記録
child.set_status(Status(StatusCode.ERROR, str(exc))) # ⑤ Tempo でエラー赤表示
_log.warning("push_send_failed", uid=uid, campaign_id=campaign_id, error=str(exc))
duration_ms = time.monotonic() * 1000 - start_ms
error_count = len(errors)
span.set_attribute("campaign.success_count", success)
span.set_attribute("campaign.error_count", error_count)
if error_count:
span.set_status(Status(StatusCode.ERROR, f"{error_count} users failed"))
# ⑥ メトリクス記録
_dispatched_counter.add(success, {"campaign_id": campaign_id, "status": "success"})
_dispatched_counter.add(error_count, {"campaign_id": campaign_id, "status": "error"})
_duration_histogram.record(duration_ms, {"campaign_id": campaign_id})
# ④ 構造化ログ(trace_id が _add_otel_context で自動付与)
_log.info("campaign_dispatched",
campaign_id=campaign_id, success=success,
total=len(user_ids), error_count=error_count,
duration_ms=round(duration_ms, 2))
return {"success": success, "total": len(user_ids), "error": error_count}
# ── エントリーポイント ────────────────────────────────────────────────────
if __name__ == "__main__":
configure_otel()
configure_structlog()
result = dispatch_campaign("camp_2026_summer_sale", ["u001", "u002", "u003"])
print(result)
# → {"success": 3, "total": 3, "error": 0}
Input: campaign_id="camp_2026_summer_sale", user_ids=["u001","u002","u003"]
Output: {"success": 3, "total": 3, "error": 0}
JSON log (structlog):
{
"event": "campaign_dispatched",
"level": "info",
"timestamp": "2026-07-05T10:30:00.123456Z",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", ← OTel 自動付与
"span_id": "00f067aa0ba902b7",
"campaign_id": "camp_2026_summer_sale",
"success": 3,
"total": 3,
"error_count": 0,
"duration_ms": 15.42
}
適用した OTel パターン:
- Resource-attached Provider:
service.name/service.versionを Resource に付与 - BatchSpanProcessor: バッファリング非同期エクスポート(本番必須)
- Parent-Child Span: ルート span → per-user 子 span でトレース階層化
- Exception Recording:
record_exception+set_status(ERROR)ペア - Log-Trace Correlation: structlog プロセッサで
trace_idを JSON ログに注入 - Composite Propagator: W3C TraceContext + Baggage でマイクロサービス間伝播
- RED Metrics: Rate(
campaigns.dispatched)+ Error(status=error)+ Duration(dispatch_duration_ms)
ポイント解説
- configure_otel() を起動時一度だけ呼ぶ:
get_tracer()/get_meter()はモジュール変数として一度だけ取得。呼ぶたびに新しいプロバイダーを作らない。 - BatchSpanProcessor が必須:
SimpleSpanProcessorは同期エクスポートでリクエストをブロックする。本番ではBatchSpanProcessorでバッファリング + 非同期送信。 - record_exception + set_status(ERROR): 例外を OTel に記録しないと、Tempo 等のトレースバックエンドでエラー span が赤くならない。
record_exceptionはeventsにスタックトレースを記録する。 - Propagator の設定:
CompositePropagatorを設定しないと Pub/Sub → Cloud Run 間でトレース文脈が切れる。W3Ctraceparentヘッダーを Pub/Sub メッセージ属性に入れることでトレースが繋がる。 - structlog + _add_otel_context プロセッサ:
trace_idをログに埋め込むことで、Grafana Loki 等でログ↔トレースを相互リンクできる(ログ→トレースのドリルダウン)。 - 子 span で user.id attribute: 「u003 だけ失敗」のような per-user 問題を Tempo のスパン一覧から即座に特定できる。
- RED メトリクス設計: Rate(dispatched counter)+ Error(status=error counter)+ Duration(dispatch_duration_ms histogram)の 3 指標で SLO を設計し、p99 > 500ms をアラート条件にする。
実務への応用
- MOps キャンペーン配信 API (Cloud Run v2):
configure_otel()をlifespanハンドラで呼び、dispatch_campaignの親 span は Pub/Sub push subscription から伝播されたtraceparentヘッダーで自動リンク - DataDog APM + OTLP Native Ingestion:
OTLPSpanExporter→ DataDog Agent v7(OTLP receiver 有効化)へ送信し、DataDog Service Map でサービス依存関係を可視化 - Argo Workflows バッチ: Step 単位で span を作成し、
workflow_run_id/step_nameを attribute に付与 → バッチ障害のステップ特定が数分で完了 - SLO 監視:
campaigns.dispatch_duration_msの p99 > 500ms をアラート条件に設定し、Error Budget 消費率を週次レビューで確認 - コスト観点: OTLP Collector の sampling rate を調整(Tail-based sampling)することで DataDog のトレース取り込みコストを 60〜80% 削減可能
今日のまとめ
record_exception + set_status)」「ログ相関(trace_id 自動付与)」「伝播(CompositePropagator)」の 5 点。BatchSpanProcessor と record_exception を省略すると、本番で「ログはあるがトレースが切れている」状態になり障害対応コストが跳ね上がる。