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
    },

何が起きたのかを順に見ると、仕組みがよくわかります。

  1. Flask 計装が /checkout の SERVER Span を作る(新しい trace_id が生まれる)
  2. requests 計装が CLIENT Span を作り、HTTP ヘッダー traceparent に trace_id と自分の span_id を書き込む
  3. 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?