はじめに
七尾百合子さん、お誕生日 235日目 おめでとうございます! nikkieです。
過去に OpenTelemetry でクライアント(HTTPX)と Web アプリ(FastAPI)を計装し、同一のトレース ID が得られることに感動しました。
そこにリクエストボディを残すことを考えます。
目次
- はじめに
- 目次
- opentelemetry-instrumentation-httpx の「Request and response hooks」
- async_request_hookを設定して、リクエストボディもトレースに記録する
- 終わりに
opentelemetry-instrumentation-httpx の「Request and response hooks」
HTTPXClientInstrumentorインスタンスのinstrument()メソッドは、リクエストやレスポンスのフックを受け取れます。
HTTPXClientInstrumentor().instrument(
request_hook=request_hook,
response_hook=response_hook,
async_request_hook=async_request_hook,
async_response_hook=async_response_hook
)
These are functions that get called back by the instrumentation right after a span is created for a request and right before the span is finished while processing a response.
リクエストフック(関数)の引数を見ていくと
def request_hook(span, request): pass async def async_request_hook(span, request): pass
RequestHook = typing.Callable[[Span, "RequestInfo"], None] AsyncRequestHook = typing.Callable[ [Span, "RequestInfo"], typing.Awaitable[typing.Any] ]
opentelemetry.trace.span.Span1
https://github.com/open-telemetry/opentelemetry-python/blob/v1.38.0/opentelemetry-api/src/opentelemetry/trace/span.py#L56
https://opentelemetry-python.readthedocs.io/en/latest/api/trace.html#opentelemetry.trace.Span
RequestInfoは namedtuple です。
https://github.com/open-telemetry/opentelemetry-python-contrib/blob/v0.59b0/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py#L276-L281
class RequestInfo(typing.NamedTuple): method: bytes url: httpx.URL headers: httpx.Headers | None stream: httpx.SyncByteStream | httpx.AsyncByteStream | None extensions: dict[str, typing.Any] | None
async_request_hookを設定して、リクエストボディもトレースに記録する
FastAPI
Item を表す JSON を POST メソッドで送れるようにしました
Request Body - FastAPI
HTTPX
非同期なクライアントで実装し、async_request_hookを設定しました。
request.streamには HTTPX のByteStreamが来ます
https://github.com/encode/httpx/blob/0.28.1/httpx/_content.py#L31
class ByteStream(AsyncByteStream, SyncByteStream): def __init__(self, stream: bytes) -> None: # 省略 def __iter__(self) -> Iterator[bytes]: # 省略 async def __aiter__(self) -> AsyncIterator[bytes]: # 省略
HTTPX のコードを確認して見つけた扱い方にならっています。
https://github.com/encode/httpx/blob/0.28.1/tests/test_content.py#L32-L33
sync_content = b"".join(list(request.stream)) async_content = b"".join([part async for part in request.stream])
クライアントのトレースにリクエストボディが記録されています(attributes部分)!
(このID は Web アプリ側2と同一です!)
{ "name": "POST", "context": { "trace_id": "0x6b10f19f3bcf436e7903830f179eb778", "span_id": "0x548ceefdb8c4319b", "trace_state": "[]" }, "kind": "SpanKind.CLIENT", "parent_id": null, "start_time": "2025-11-07T12:32:32.251662Z", "end_time": "2025-11-07T12:32:32.268068Z", "status": { "status_code": "UNSET" }, "attributes": { "http.method": "POST", "http.url": "http://localhost:8000/items/", "http.request.body": "{\"name\":\"awesome\",\"price\":100}", "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": "" } }
終わりに
opentelemetry-instrumentation-httpx の request hook を使って、Web アプリへのリクエストボディをトレースに残すことができました。
本番利用は未検証ですが、Web アプリもクライアントコードも自分の持ち物の場合は、今回の実装で運用時に助かりそうです。
例えば過去に経験した何万と送ったリクエストのうち1件だけエラーという事象には、トレース ID を使ってエラーを吐いた原因のリクエストを特定し、リクエストボディを元に再現調査と進めそうです。