<aside> 📝 参考資料

<aside> 💡 ここで扱うのはトレースだけ https://opentelemetry.io/ja/docs/concepts/signals/traces/

トレースは、リクエストがアプリケーションに投げられたときに何が起こるかの全体像を教えてくれます。

</aside>

サーバとクライアント

計装していない状態

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "fastapi",
#     "uvicorn",
# ]
# ///
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def hello(param: str | None = None):
    return {"message": "Hello World", "param": param}

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

uv run app.pyで起動します。

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "httpx",
# ]
# ///
import httpx

async def get(url: str, param: str | None = None):
    async with httpx.AsyncClient() as client:
        response = await client.get(url, params={"param": param})
        response.raise_for_status()
        return response

if __name__ == "__main__":
    import asyncio

    url = "<http://localhost:8000/>"
    response = asyncio.run(get(url, "awesome"))
    assert response.json() == {"message": "Hello World", "param": "awesome"}

サーバを起動した状態で、別のターミナルでuv run client.py

サーバを計装

<aside> ✨ sys.executable(Python処理系のパス)をprintして「Select Interpreter」(VS CodeのPython拡張) https://docs.python.org/ja/3/library/sys.html#sys.executable シンタックスハイライトやジャンプが使えるようになります

</aside>

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "fastapi",
#     "opentelemetry-sdk",
#     "opentelemetry-instrumentation-fastapi",
#     "uvicorn",
# ]
# ///
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
    BatchSpanProcessor,
    ConsoleSpanExporter,
)
from opentelemetry.trace import get_tracer_provider, set_tracer_provider

set_tracer_provider(TracerProvider())
get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))

app = FastAPI()

@app.get("/")
async def hello(param: str | None = None):
    return {"message": "Hello World", "param": param}

FastAPIInstrumentor.instrument_app(app)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

<aside> 📝 計装コードのリファレンス トレーサープロバイダー https://opentelemetry.io/ja/docs/concepts/signals/traces/#tracer-provider

Tracerのファクトリー

トレーサープロバイダーの設定

クライアントのプログラムを実行すると、サーバを起動したターミナルに出力

{
    "name": "GET / http send",
    "context": {
        "trace_id": "0x98849e894c54dca1ccc09a55eeff99f5",
        "span_id": "0xc314092788163d3c",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x1b4bd118b2a1c3e9",
    "start_time": "2025-11-07T13:15:46.077006Z",
    "end_time": "2025-11-07T13:15:46.077028Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "asgi.event.type": "http.response.start",
        "http.status_code": 200
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.38.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "GET / http send",
    "context": {
        "trace_id": "0x98849e894c54dca1ccc09a55eeff99f5",
        "span_id": "0xcdacc254a2829d2b",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x1b4bd118b2a1c3e9",
    "start_time": "2025-11-07T13:15:46.077281Z",
    "end_time": "2025-11-07T13:15:46.077288Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "asgi.event.type": "http.response.body"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.38.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "GET /",
    "context": {
        "trace_id": "0x98849e894c54dca1ccc09a55eeff99f5",
        "span_id": "0x1b4bd118b2a1c3e9",
        "trace_state": "[]"
    },
    "kind": "SpanKind.SERVER",
    "parent_id": null,
    "start_time": "2025-11-07T13:15:46.076566Z",
    "end_time": "2025-11-07T13:15:46.077347Z",
    "status": {
        "status_code": "UNSET"
    },

    "attributes": {
        "http.scheme": "http",
        "http.host": "127.0.0.1:8000",
        "net.host.port": 8000,
        "http.flavor": "1.1",
        "http.target": "/",
        "http.url": "<http://localhost:8000/?param=awesome>",
        "http.method": "GET",
        "http.server_name": "localhost:8000",
        "http.user_agent": "python-httpx/0.28.1",
        "net.peer.ip": "127.0.0.1",
        "net.peer.port": 54388,
        "http.route": "/",
        "http.status_code": 200
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.38.0",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}

スパン https://opentelemetry.io/ja/docs/concepts/signals/traces/#spans

スパン は、作業や操作の単位を表します。 スパンはトレースの構成要素です。

クライアントを計装

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "httpx",
#     "opentelemetry-sdk",
#     "opentelemetry-instrumentation-httpx",
# ]
# ///
import httpx
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
    BatchSpanProcessor,
    ConsoleSpanExporter,
)
from opentelemetry.trace import get_tracer_provider, set_tracer_provider

set_tracer_provider(TracerProvider())
get_tracer_provider().add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))

HTTPXClientInstrumentor().instrument()

async def get(url: str, param: str | None = None):
    async with httpx.AsyncClient() as client:
        response = await client.get(url, params={"param": param})
        response.raise_for_status()
        return response

if __name__ == "__main__":
    import asyncio

    url = "<http://localhost:8000/>"
    response = asyncio.run(get(url, "awesome"))
    assert response.json() == {"message": "Hello World", "param": "awesome"}