- 技術 💻
OpenTelemetry入門|Pythonで始めるトレース・メトリクス実装
OpenTelemetry(OTel)とは、トレース・メトリクス・ログといった「アプリの動きを外から理解するためのデータ(テレメトリー)」を、ベンダーに依存しない共通の方法で計測・送信するためのオープンな標準とツール群です。 計測するコードを一度書けば、送り先(Jaeger、Prometheus、各種クラウドの監視サービスなど)を設定だけで切り替えられます。
この記事では概念の紹介にとどまらず、Python SDK 1.45.0 で手動計装(親子Span・属性・イベント・例外)とメトリクス・ログを実際に動かし、出力をそのまま掲載しています。さらに opentelemetry-instrument による自動計装で、2つのFlaskサービス間を traceparent ヘッダーに乗ってトレースIDが伝わる様子を出力で確かめます。Collector への OTLP 送信設定とサンプリングの選び方も表でまとめました。
OpenTelemetryの早見表
| やりたいこと | 書き方(Python) | ポイント |
|---|---|---|
| インストール | pip install opentelemetry-sdk |
API と SDK が入る。最新は 1.45.0(2026-09-25) |
| サービス名を付ける | Resource.create({"service.name": "order-service"}) |
送り先で一覧に出る名前。必ず設定 |
| Tracer を取得 | trace.get_tracer("demo.orders") |
引数は計装しているライブラリ・モジュール名 |
| Span を作る | with tracer.start_as_current_span("create_order") as span: |
入れ子にすると自動で親子になる |
| 属性を付ける | span.set_attribute("app.order_id", "ord-001") |
検索・絞り込みに使うキー |
| イベントを記録 | span.add_event("validation.done", {...}) |
Span 内の「ある時点の出来事」 |
| 例外を記録 | span.record_exception(e) + span.set_status(Status(StatusCode.ERROR)) |
with を抜ける例外は自動で記録される |
| カウンター | meter.create_counter("app.orders").add(1, attrs) |
増える一方の数 |
| ヒストグラム | meter.create_histogram("app.order.duration", unit="ms").record(35) |
処理時間などの分布 |
| ログ連携 | logging に LoggingHandler を追加 |
ログに trace_id / span_id が付く |
| 自動計装 | opentelemetry-instrument python app.py |
Flask・requests などをコード変更なしで計測 |
| 必要な計装を調べる | opentelemetry-bootstrap -a requirements |
入っているライブラリに合う計装パッケージを表示 |
| 出力先を標準出力に | OTEL_TRACES_EXPORTER=console |
動作確認に便利 |
| Collector へ送る | OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 |
既定は gRPC(4317)、HTTP は 4318 |
| サンプリング | OTEL_TRACES_SAMPLER=parentbased_traceidratio + OTEL_TRACES_SAMPLER_ARG=0.1 |
10% だけ記録 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「OpenTelemetry入門|Pythonで始めるトレース・メトリクス実装」
バージョンと各シグナルの安定状況(2026年10月時点)
OpenTelemetry は「シグナル(トレース・メトリクス・ログ)ごと」「言語ごと」に成熟度が異なります。 PyPI と公式リポジトリで確認した情報です。
| パッケージ | 最新版 | 必要なPython |
|---|---|---|
| opentelemetry-api / opentelemetry-sdk | 1.45.0(2026-09-25) | 3.10以上 |
| opentelemetry-exporter-otlp | 1.45.0 | 3.10以上 |
| opentelemetry-distro / opentelemetry-instrumentation | 0.66b0 | 3.10以上 |
| opentelemetry-instrumentation-flask / -requests | 0.66b0 | 3.10以上 |
| シグナル | Python SDK の状態 | 補足 |
|---|---|---|
| Traces | Stable | 仕様も完全に安定し、長期サポート対象 |
| Metrics | Stable | |
| Logs | Development | 安定化に向けて非推奨化や名前の変更が入ることがある(後述の例あり) |
計装パッケージ(instrumentation)は 0.66b0 のように b(ベータ)付きの番号です。SDK の 1.45.0 と組で公開されるので、SDK と計装のバージョンはセットでそろえて入れるのが安全です。Collector は、コンポーネントごとに安定度が異なる「mixed」という扱いです。
用語一覧と全体像
flowchart LR
subgraph A[frontend サービス]
S1[Span: GET /checkout<br/>SERVER]
S2[Span: GET<br/>CLIENT]
S1 --> S2
end
subgraph B[backend サービス]
S3[Span: GET /stock/item_id<br/>SERVER]
end
S2 -- HTTPヘッダー traceparent --> S3
A -- OTLP --> C[Collector<br/>受信・加工・振り分け]
B -- OTLP --> C
C --> J[トレースの保存先<br/>Jaeger など]
C --> P[メトリクスの保存先<br/>Prometheus など]
| 用語 | 意味 | この記事での例 |
|---|---|---|
| Trace(トレース) | 1つのリクエストが複数の処理・サービスを通る全体の記録。Span の木構造 | /checkout の1回分 |
| Span(スパン) | トレースを構成する1つの処理単位。開始・終了時刻、名前、親を持つ | create_order、check_stock |
| Trace ID / Span ID | トレース全体の ID(32桁の16進)と、各 Span の ID(16桁) | 4bf92f35...、00f067aa... |
| SpanKind | Span の役割。SERVER / CLIENT / INTERNAL / PRODUCER / CONSUMER | 受信側は SERVER、呼び出し側は CLIENT |
| Attribute(属性) | Span やメトリクスに付ける key-value | app.order_id="ord-001" |
| Event(イベント) | Span 内の時刻付きの出来事 | validation.done、exception |
| Status | Span の結果。UNSET / OK / ERROR | 例外時に ERROR |
| Resource | テレメトリーを出している「主体」の情報 | service.name、service.version |
| Instrumentation Scope | どのライブラリ・モジュールが計測したか | get_tracer("demo.orders") の名前 |
| Exporter | データを送り出す部品 | Console、OTLP |
| SpanProcessor | 終了した Span を Exporter に渡す部品 | Simple(即時)、Batch(まとめて) |
| Collector | テレメトリーを受け取り、加工して各送り先へ振り分ける独立したサーバー | otelcol / otelcol-contrib |
| OTLP | OpenTelemetry 標準の送信プロトコル。gRPC(4317)と HTTP(4318) | SDK → Collector |
| Propagator | サービス間でトレース情報をヘッダーに載せて受け渡す仕組み | W3C Trace Context(traceparent) |
| Context | 「今どの Span の中にいるか」を保持する仕組み | start_as_current_span |
| Sampler | どのトレースを記録するか決める仕組み | parentbased_traceidratio |
| Semantic Conventions | 属性名の共通ルール | http.request.method、url.path |
手動計装:親子Span・属性・イベント・例外を記録する
まずは標準出力に出す ConsoleSpanExporter で、Span の中身を自分の目で確かめるのが近道です。 注文の受付(create_order)の中で在庫確認(check_stock)を呼び、在庫不足で例外になるケースを記録します。
python3 -m venv .venv && . .venv/bin/activate
pip install opentelemetry-api==1.45.0 opentelemetry-sdk==1.45.0
# trace_manual.py
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.trace import Status, StatusCode
# 1. Resource:このプログラム自身の情報
resource = Resource.create({"service.name": "order-service", "service.version": "1.0.0"})
# 2. TracerProvider と Exporter(標準出力へ)
provider = TracerProvider(resource=resource)
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("demo.orders", "0.1.0")
def check_stock(item_id: str, qty: int) -> None:
with tracer.start_as_current_span("check_stock") as span:
span.set_attribute("app.item_id", item_id)
span.set_attribute("app.quantity", qty)
if qty > 5:
raise ValueError(f"out of stock: {item_id}")
def create_order(order_id: str, item_id: str, qty: int) -> None:
with tracer.start_as_current_span("create_order") as span:
span.set_attribute("app.order_id", order_id)
span.add_event("validation.done", {"app.rule_count": 3})
try:
check_stock(item_id, qty)
except ValueError as e:
span.record_exception(e)
span.set_status(Status(StatusCode.ERROR, str(e)))
create_order("ord-001", "item-42", 10)
実行結果(Python 3.11、opentelemetry-sdk 1.45.0)
exception.stacktrace の長い行だけ省略しています。
{
"name": "check_stock",
"context": {
"trace_id": "0xa6d4969c1c551a4f36001cf8c9bbfafb",
"span_id": "0xca4fb530bba8db80",
"trace_state": "[]"
},
"kind": "SpanKind.INTERNAL",
"parent_id": "0x6815fafe9529e748",
"start_time": "2026-10-01T13:13:50.171075Z",
"end_time": "2026-10-01T13:13:50.171919Z",
"status": {
"status_code": "ERROR",
"description": "ValueError: out of stock: item-42"
},
"attributes": {
"app.item_id": "item-42",
"app.quantity": 10
},
"events": [
{
"name": "exception",
"timestamp": "2026-10-01T13:13:50.171904Z",
"attributes": {
"exception.type": "ValueError",
"exception.message": "out of stock: item-42",
"exception.escaped": "False"
}
}
],
"links": [],
"resource": {
"attributes": {
"telemetry.sdk.language": "python",
"telemetry.sdk.name": "opentelemetry",
"telemetry.sdk.version": "1.45.0",
"service.instance.id": "9d99639e-2e57-44d4-a06d-7106db2ecdbe",
"service.name": "order-service",
"service.version": "1.0.0"
},
"schema_url": ""
}
}
{
"name": "create_order",
"context": {
"trace_id": "0xa6d4969c1c551a4f36001cf8c9bbfafb",
"span_id": "0x6815fafe9529e748",
"trace_state": "[]"
},
"kind": "SpanKind.INTERNAL",
"parent_id": null,
"start_time": "2026-10-01T13:13:50.171013Z",
"end_time": "2026-10-01T13:13:50.172360Z",
"status": {
"status_code": "ERROR",
"description": "out of stock: item-42"
},
"attributes": {
"app.order_id": "ord-001"
},
"events": [
{
"name": "validation.done",
"timestamp": "2026-10-01T13:13:50.171040Z",
"attributes": {
"app.rule_count": 3
}
},
{
"name": "exception",
"timestamp": "2026-10-01T13:13:50.172341Z",
"attributes": {
"exception.type": "ValueError",
"exception.message": "out of stock: item-42",
"exception.escaped": "False"
}
}
],
"links": [],
"resource": { "...": "check_stock と同じ" }
}
出力から読み取れること
| 確認ポイント | 出力の該当箇所 | 意味 |
|---|---|---|
| 親子関係 | check_stock の parent_id = create_order の span_id(0x6815...) |
with を入れ子にするだけで自動的に親子になる |
| 同じトレース | 2つの trace_id が同じ 0xa6d4... |
1回の処理としてまとまる |
| 出力の順番 | 子の check_stock が先に出る |
Span は終了したときにエクスポートされる |
| 例外の自動記録 | check_stock に exception イベントと ERROR |
start_as_current_span は with を抜ける例外を自動で記録し、ステータスも ERROR にする |
| 手動の例外記録 | create_order は try で捕まえたので、record_exception と set_status を自分で書いた |
例外を握りつぶす箇所では明示的に記録する |
| Resource の自動項目 | telemetry.sdk.*、service.instance.id |
SDK が自動で付ける。service.instance.id は起動ごとに変わる |
例外を自分で処理する箇所では、record_exception と set_status(ERROR) の両方を書くのがポイントです。片方だけだと「エラーだが原因がわからない」「原因はあるがエラー扱いにならない」状態になります。
属性名の付け方
| 種類 | 例 | ルール |
|---|---|---|
| 標準の属性 | http.request.method、url.path、db.system.name |
Semantic Conventions の名前をそのまま使う |
| 独自の属性 | app.order_id、app.item_id |
自社・アプリ固有の接頭辞を付けて衝突を防ぐ |
| 避けたい値 | パスワード、メールアドレス、トークン | 送り先で多くの人が見るため入れない |
メトリクス:Counter と Histogram を出力する
メトリクスは MeterProvider → Meter → Instrument(計器)の順に作ります。 既定の ConsoleMetricExporter は1回の出力が長いので、ここでは整形関数(formatter)を渡して1データポイント1行にしました。
# metrics_demo.py
import json
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
def compact(data) -> str:
"""ConsoleMetricExporter の出力を1行のJSONにして見やすくする"""
d = json.loads(data.to_json())
out = []
for rm in d["resource_metrics"]:
for sm in rm["scope_metrics"]:
for m in sm["metrics"]:
for p in m["data"]["data_points"]:
row = {"name": m["name"], "unit": m["unit"], "attrs": p["attributes"]}
for k in ("value", "count", "sum", "min", "max", "bucket_counts"):
if k in p:
row[k] = p[k]
out.append(json.dumps(row, ensure_ascii=False))
return "\n".join(out) + "\n"
reader = PeriodicExportingMetricReader(
ConsoleMetricExporter(formatter=compact), export_interval_millis=60_000
)
provider = MeterProvider(
resource=Resource.create({"service.name": "order-service"}), metric_readers=[reader]
)
metrics.set_meter_provider(provider)
meter = metrics.get_meter("demo.orders")
orders = meter.create_counter("app.orders", unit="{order}", description="受け付けた注文数")
latency = meter.create_histogram("app.order.duration", unit="ms", description="注文処理時間")
queue = meter.create_up_down_counter("app.queue.size", unit="{task}", description="未処理タスク数")
for ms, status in [(12, "ok"), (35, "ok"), (80, "ok"), (420, "error"), (9, "ok")]:
orders.add(1, {"app.status": status})
latency.record(ms, {"app.status": status})
queue.add(5)
queue.add(-2)
provider.shutdown() # 終了時に残りを書き出す
{"name": "app.orders", "unit": "{order}", "attrs": {"app.status": "ok"}, "value": 4}
{"name": "app.orders", "unit": "{order}", "attrs": {"app.status": "error"}, "value": 1}
{"name": "app.order.duration", "unit": "ms", "attrs": {"app.status": "ok"}, "count": 4, "sum": 136, "min": 9, "max": 80, "bucket_counts": [0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
{"name": "app.order.duration", "unit": "ms", "attrs": {"app.status": "error"}, "count": 1, "sum": 420, "min": 420, "max": 420, "bucket_counts": [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]}
{"name": "app.queue.size", "unit": "{task}", "attrs": {}, "value": 3}
属性(app.status)の値ごとに別々のデータポイントとして集計されているのがわかります。Histogram の既定の境界値は SDK 内で次のように定義されています。
(0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, 5000.0, 7500.0, 10000.0)
bucket_counts は16個(境界15個+上限なし)で、9ms は「5〜10」、12ms は「10〜25」、35ms は「25〜50」、80ms は「75〜100」、420ms は「250〜500」に入っています。単位を秒にする場合は、この既定値だと粗すぎるため View で境界値を変更します。
| Instrument | 同期/非同期 | 用途 | 例 |
|---|---|---|---|
| Counter | 同期 | 増える一方の数 | 注文数、エラー数 |
| UpDownCounter | 同期 | 増減する数 | キューの長さ、接続数 |
| Histogram | 同期 | 値の分布 | 処理時間、ファイルサイズ |
| Gauge | 同期 | その時点の値を記録 | 設定値、温度 |
| ObservableCounter / ObservableUpDownCounter / ObservableGauge | 非同期(コールバック) | 定期的に読み取る値 | CPU時間、メモリ使用量 |
provider.shutdown() を呼ばないと、プログラム終了時に最後のデータが出力されないことがあります。 短いスクリプトやバッチでは必ず呼び出します。
ログ連携:logging に trace_id を付ける
標準の logging に LoggingHandler を追加すると、ログが OpenTelemetry のログとして扱われ、そのとき実行中の Span の trace_id / span_id が自動で付きます。
# logs_demo.py
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 ConsoleLogRecordExporter, SimpleLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
resource = Resource.create({"service.name": "order-service"})
trace.set_tracer_provider(TracerProvider(resource=resource))
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(SimpleLogRecordProcessor(ConsoleLogRecordExporter()))
set_logger_provider(logger_provider)
# 標準の logging を OpenTelemetry に橋渡しする
logging.getLogger().addHandler(LoggingHandler(level=logging.INFO, logger_provider=logger_provider))
logging.getLogger().setLevel(logging.INFO)
log = logging.getLogger("orders")
tracer = trace.get_tracer("demo.orders")
with tracer.start_as_current_span("create_order") as span:
ctx = span.get_span_context()
print(f"current trace_id={ctx.trace_id:032x} span_id={ctx.span_id:016x}")
log.warning("low stock", extra={"app.item_id": "item-42"})
logger_provider.shutdown()
current trace_id=9d264de96d0201749e56a75f845eb11c span_id=275ce988d54da062
{
"body": "low stock",
"severity_number": 13,
"severity_text": "WARN",
"attributes": {
"app.item_id": "item-42",
"code.file.path": ".../logs_demo.py",
"code.function.name": "<module>",
"code.line.number": 26
},
"dropped_attributes": 0,
"timestamp": "2026-10-01T13:14:19.521801Z",
"observed_timestamp": "2026-10-01T13:14:19.521896Z",
"trace_id": "0x9d264de96d0201749e56a75f845eb11c",
"span_id": "0x275ce988d54da062",
"trace_flags": 3,
...
}
ログの trace_id / span_id が、実行中の Span と一致しています。 これで「このエラーログはどのリクエストのものか」をトレースからたどれます。extra に渡した値は属性になり、ファイル名・関数名・行番号も自動で付きました。
Logs はまだ Development:実際に出た警告
最初に ConsoleLogExporter という名前で書いたところ、次の警告が出ました。
DeprecationWarning: Use ConsoleLogRecordExporter. Since logs are not stable yet this WILL be removed in future releases.
ログのシグナルは安定化の途中のため、クラス名が変わることがあります。 上のコードは警告に従って ConsoleLogRecordExporter に直した後のものです。ログ連携を使う場合は、バージョンを固定し、更新時に警告を確認するのがおすすめです。
自動計装:2つのサービス間でトレースIDが伝わる様子を見る
自動計装(ゼロコード計装)は、opentelemetry-instrument コマンドでアプリを起動するだけで、Flask や requests などの主要ライブラリを計測する仕組みです。 フロント(:7808)が在庫API(:7809)を呼ぶ2つのサービスで試します。
pip install opentelemetry-distro==0.66b0 flask requests \
opentelemetry-instrumentation-flask==0.66b0 opentelemetry-instrumentation-requests==0.66b0
# 入っているライブラリに合う計装パッケージを確認(抜粋)
opentelemetry-bootstrap -a requirements
opentelemetry-instrumentation-flask==0.66b0
opentelemetry-instrumentation-requests==0.66b0
opentelemetry-instrumentation-urllib3==0.66b0
opentelemetry-instrumentation-logging==0.66b0
...
(opentelemetry-bootstrap -a install にすると、表示されたものをまとめてインストールします)
# frontend.py(OpenTelemetry の import は一切なし)
import requests
from flask import Flask
app = Flask(__name__)
@app.get("/checkout")
def checkout():
r = requests.get("http://localhost:7809/stock/item-42", timeout=3)
return {"frontend": "ok", "backend": r.json()}
if __name__ == "__main__":
app.run(port=7808)
# backend.py(受け取ったヘッダーを確認用に表示)
from flask import Flask, request
app = Flask(__name__)
@app.get("/stock/<item_id>")
def stock(item_id: str):
tp = request.headers.get("traceparent")
print(f"[backend] received traceparent: {tp}", flush=True)
return {"item_id": item_id, "stock": 3}
if __name__ == "__main__":
app.run(port=7809)
export OTEL_TRACES_EXPORTER=console OTEL_METRICS_EXPORTER=none OTEL_LOGS_EXPORTER=none
OTEL_SERVICE_NAME=backend opentelemetry-instrument python backend.py &
OTEL_SERVICE_NAME=frontend opentelemetry-instrument python frontend.py &
curl -s localhost:7808/checkout
{"backend":{"item_id":"item-42","stock":3},"frontend":"ok"}
3つの Span の出力(主要な項目を抜粋)
[backend] received traceparent: 00-b9ff778b2de5d7a9254ab48c57ee24e7-8c7e4203b3494857-03
| サービス | Span名 | kind | trace_id | span_id | parent_id |
|---|---|---|---|---|---|
| frontend | GET /checkout |
SERVER | b9ff778b...24e7 |
cb284078db8b522b |
null(ルート) |
| frontend | GET |
CLIENT | b9ff778b...24e7 |
8c7e4203b3494857 |
cb284078db8b522b |
| backend | GET /stock/<item_id> |
SERVER | b9ff778b...24e7 |
0b8cac773f798ac3 |
8c7e4203b3494857 |
backend の SERVER Span の出力(属性部分)です。
"name": "GET /stock/<item_id>",
"kind": "SpanKind.SERVER",
"parent_id": "0x8c7e4203b3494857",
"attributes": {
"http.method": "GET",
"http.server_name": "127.0.0.1",
"http.scheme": "http",
"net.host.name": "localhost:7809",
"http.target": "/stock/item-42",
"http.user_agent": "python-requests/2.34.2",
"http.route": "/stock/<item_id>",
"http.status_code": 200
},
何が起きたのかを順に見ると、仕組みがよくわかります。
- Flask 計装が
/checkoutの SERVER Span を作る(新しい trace_id が生まれる) - requests 計装が CLIENT Span を作り、HTTP ヘッダー
traceparentに trace_id と自分の span_id を書き込む - backend の Flask 計装がヘッダーを読み取り、同じ trace_id のまま、CLIENT Span を親とする SERVER Span を作る
traceparent ヘッダーの読み方
00-b9ff778b2de5d7a9254ab48c57ee24e7-8c7e4203b3494857-03
│ │ │ └ trace-flags
│ │ └ parent-id(呼び出し元の span_id、16桁)
│ └ trace-id(32桁)
└ version
| trace-flags | 意味 |
|---|---|
01 |
サンプリング済み(記録対象) |
00 |
サンプリングされていない(記録しない) |
02 / 03 |
02 は「trace_id がランダムに生成された」ことを示すフラグ(W3C Trace Context Level 2)。03 は 01 と 02 の両方 |
受け取った parent-id(8c7e...)が、frontend の CLIENT Span の span_id と一致しているのが、伝播の証拠です。
外から traceparent を渡すとどうなるか
curl で自分で作ったヘッダーを付けて呼ぶと、frontend はそのトレースの続きとして記録しました。
curl -s -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' localhost:7808/checkout
[backend] received traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-c0d159a3b3b87bc7-01
frontend GET /checkout trace_id=0x4bf92f3577b34da6a3ce929d0e0e4736 parent_id=0x00f067aa0ba902b7
frontend GET trace_id=0x4bf92f3577b34da6a3ce929d0e0e4736 parent_id=0x5302f15bde22f11c
backend GET /stock/... trace_id=0x4bf92f3577b34da6a3ce929d0e0e4736 parent_id=0xc0d159a3b3b87bc7
(出力から trace_id と parent_id の行だけを並べ直しています)ルート Span の parent_id が、curl で渡した 00f067aa0ba902b7 になりました。 前段にロードバランサーや別言語のサービスがあっても、W3C Trace Context に対応していれば1本のトレースにつながります。
同じヘッダーの末尾を -00(サンプリングしない)にすると、どちらのサービスも Span を1つも出力しませんでした。 それでも backend には 00-4bf92f35...-672e3d8ad9104695-00 が届いており、「記録しない」という判断ごと下流へ伝わっています。
コードで伝播させる(inject / extract)
HTTP 以外(キュー、バッチ、独自プロトコル)では、Propagator を直接使います。
# propagate_demo.py
from opentelemetry import propagate, trace
from opentelemetry.sdk.trace import TracerProvider
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer("demo.propagate")
# 送る側:現在のコンテキストをHTTPヘッダー用の dict に書き込む
with tracer.start_as_current_span("send_task") as span:
headers: dict[str, str] = {}
propagate.inject(headers)
print("inject ->", headers)
sender_trace_id = span.get_span_context().trace_id
# 受け取る側:ヘッダーからコンテキストを取り出し、親として子Spanを作る
ctx = propagate.extract(headers)
with tracer.start_as_current_span("handle_task", context=ctx) as child:
c = child.get_span_context()
print("same trace_id:", c.trace_id == sender_trace_id, f"{c.trace_id:032x}")
inject -> {'traceparent': '00-8e2e5070c178c58e0c4f4740337af965-f6702383f6450676-03'}
same trace_id: True 8e2e5070c178c58e0c4f4740337af965
キューのメッセージ属性などに headers の中身を入れて送り、受け取り側で extract すると、非同期処理もトレースでつながります。
属性名を新しい Semantic Conventions にそろえる
上の出力の http.method・http.target は以前からの属性名です。環境変数 OTEL_SEMCONV_STABILITY_OPT_IN=http を付けて frontend を起動すると、安定版の名前に切り替わりました。
"attributes": {
"http.request.method": "GET",
"url.scheme": "http",
"server.address": "localhost:7808",
"server.port": 7808,
"url.path": "/checkout",
"client.address": "127.0.0.1",
"client.port": 47714,
"user_agent.original": "curl/8.5.0",
"network.protocol.version": "1.1",
"http.route": "/checkout",
"http.response.status_code": 200
},
| 以前の名前 | 安定版の名前 |
|---|---|
http.method |
http.request.method |
http.status_code |
http.response.status_code |
http.target |
url.path(+url.query) |
http.scheme |
url.scheme |
net.host.name / net.host.port |
server.address / server.port |
net.peer.ip |
client.address |
http.flavor |
network.protocol.version |
ダッシュボードやアラートで属性名を使う場合は、新しく作るなら安定版の名前に合わせておくと後から変更せずに済みます(http/dup を指定すると新旧両方を出力します)。
よく使う環境変数一覧
| 環境変数 | 例 | 意味 |
|---|---|---|
OTEL_SERVICE_NAME |
frontend |
service.name を設定 |
OTEL_RESOURCE_ATTRIBUTES |
service.version=1.0.0,deployment.environment.name=dev |
Resource の属性を追加 |
OTEL_TRACES_EXPORTER |
otlp / console / none |
トレースの送り先 |
OTEL_METRICS_EXPORTER |
otlp / console / none |
メトリクスの送り先 |
OTEL_LOGS_EXPORTER |
otlp / console / none |
ログの送り先 |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
OTLP の送信先 |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc / http/protobuf |
送信方式 |
OTEL_TRACES_SAMPLER |
parentbased_traceidratio |
サンプラーの種類 |
OTEL_TRACES_SAMPLER_ARG |
0.1 |
サンプリング率 |
OTEL_PROPAGATORS |
tracecontext,baggage |
使う Propagator |
OTEL_BSP_SCHEDULE_DELAY |
5000 |
BatchSpanProcessor の送信間隔(ミリ秒) |
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED |
true |
自動計装で logging も OTel に送る |
OTEL_PYTHON_EXCLUDED_URLS |
healthz,metrics |
計測しないURL |
opentelemetry-distro を入れると、エクスポーターの既定は otlp、プロトコルの既定は grpc になります(distro のソースで確認)。手元で試すときは console を明示すると、Collector なしで出力を確認できます。
OTLP で Collector に送る(未実行)
この節は本記事の検証環境では実行していません。公式ドキュメントに沿った設定例です。
アプリ側
pip install opentelemetry-exporter-otlp==1.45.0
export OTEL_SERVICE_NAME=frontend
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc # HTTP なら http/protobuf と :4318
opentelemetry-instrument python frontend.py
コードで設定する場合は、ConsoleSpanExporter を OTLPSpanExporter に、SimpleSpanProcessor を BatchSpanProcessor に置き換えます。
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True))
)
Collector 側(otelcol-contrib の config.yaml)
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 20
batch: {}
exporters:
debug:
verbosity: basic
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug, otlp/jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug, prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug]
| 部品 | 役割 | 例 |
|---|---|---|
| receivers | データを受け取る | otlp、prometheus、filelog |
| processors | 加工・まとめ・間引き | batch、memory_limiter、attributes、tail_sampling |
| exporters | 送り出す | otlp、otlphttp、prometheus、debug |
| service.pipelines | シグナルごとに受信→加工→送信をつなぐ | traces / metrics / logs |
アプリから直接各サービスへ送ることもできますが、Collector を挟むと、送り先の変更・認証情報の管理・サンプリングをアプリの外でまとめて行えます。 memory_limiter は processors の先頭に置くのが公式の推奨です。
サンプリングの選び方
すべてのリクエストを記録すると、データ量と費用が増えます。 どのトレースを残すかを決めるのがサンプリングです。
サンプラー(OTEL_TRACES_SAMPLER) |
動き | 向いている場面 |
|---|---|---|
always_on |
全部記録 | 開発・検証、リクエストが少ないサービス |
always_off |
何も記録しない | 一時的に止めたいとき |
traceidratio |
trace_id をもとに一定割合を記録(親の判断は見ない) | 単独で動くバッチなど |
parentbased_always_on(既定) |
親があれば親に従い、ルートは全部記録 | 小〜中規模の標準設定 |
parentbased_traceidratio |
親があれば親に従い、ルートは一定割合 | リクエストが多い本番の基本形 |
Collector の tail_sampling |
トレースが終わってから「エラーあり」「遅い」などで選ぶ | エラーと遅いリクエストを必ず残したい |
検証では frontend を OTEL_TRACES_SAMPLER=parentbased_traceidratio、OTEL_TRACES_SAMPLER_ARG=0.0(0%)で起動したところ、Span は出力されず、backend には末尾 -02(ランダムIDのフラグのみ、サンプリングなし)の traceparent が届き、backend も記録しませんでした。 親の判断に従う parentbased_ 系を使うと、サービスをまたいでも「記録する/しない」がそろい、途中が欠けたトレースになりません。
| 判断の目安 | おすすめ |
|---|---|
| まず試したい | 既定のまま(全件) |
| 1秒あたり数百リクエスト以上 | parentbased_traceidratio で 1〜10% |
| エラーは必ず残したい | アプリは全件送信し、Collector の tail_sampling で選別 |
| 費用を細かく調整したい | Collector 側でまとめて設定(アプリの再起動が不要) |
よくあるつまずき
| 症状 | 原因 | 対処 |
|---|---|---|
| 何も出力されない | エクスポーターの既定が otlp で、送信先がない | OTEL_TRACES_EXPORTER=console で確認 |
| サービス間でトレースがつながらない | 呼び出し側の HTTP クライアントが計装されていない | opentelemetry-bootstrap -a install で計装を追加 |
| スクリプトの最後の Span が出ない | BatchSpanProcessor の送信前に終了 | provider.shutdown() を呼ぶ |
| メトリクスの最後のデータが出ない | 同上 | meter_provider.shutdown() |
| Flask の debug モードで Span が出ない | リローダーで別プロセスが起動し、計装が効かない | app.run(debug=True, use_reloader=False) にする |
DeprecationWarning ... logs are not stable yet |
ログ API の名前変更 | 警告に書かれた新しい名前に置き換える |
| 属性名がダッシュボードと合わない | Semantic Conventions の新旧 | OTEL_SEMCONV_STABILITY_OPT_IN=http |
よくある質問
Q. OpenTelemetry と Prometheus はどう違いますか? A. Prometheus はメトリクスを集めて保存・検索するシステム、OpenTelemetry は計測と送信の標準です。役割が違うので組み合わせて使えます。OTel で計測したメトリクスを Collector 経由で Prometheus に保存する構成がよく使われます。
Q. 手動計装と自動計装はどちらから始めればよいですか?
A. まず自動計装で HTTP・DB などの入口と出口を計測し、足りない部分(業務上大事な処理)だけ手動で Span や属性を追加するのが効率的です。自動計装で作られた Span の中で start_as_current_span を使えば、手動の Span も同じトレースの子として記録されます。
Q. ログは OpenTelemetry で送るべきですか? A. Python のログ連携は 2026年10月時点で Development の段階です。まずはトレースとメトリクスを OTel で送り、ログは既存の仕組みのまま trace_id を出力しておく方法でも、トレースとログを結び付けられます。
Q. Collector は必須ですか? A. 必須ではありません。 アプリから送り先へ直接 OTLP で送れます。ただし、送り先の切り替え・サンプリング・機密情報の除去をアプリの外で行いたい場合は、Collector を置くと運用しやすくなります。
まとめ
- OpenTelemetry は計測と送信の共通規格で、送り先を設定だけで切り替えられる
- Python SDK 1.45.0 ではトレースとメトリクスが Stable、ログは Development。計装パッケージ(0.66b0)は SDK とセットでそろえる
- with を入れ子にすれば親子 Span になり、with を抜ける例外は自動で記録される。捕まえた例外は
record_exceptionとset_statusを書く opentelemetry-instrumentで起動するだけで、traceparentヘッダーによりサービス間のトレースがつながる- trace-flags の 01 は記録、00 は記録しない。
parentbased_系のサンプラーで判断をサービス間でそろえる - 本番は OTLP で Collector に送り、
memory_limiterとbatch、必要に応じてtail_samplingを組み合わせる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「OpenTelemetry入門|Pythonで始めるトレース・メトリクス実装」
株式会社RJCでは、OpenTelemetryによる計測とトレースの知識を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。