diff --git a/logfire/_internal/integrations/httpx.py b/logfire/_internal/integrations/httpx.py index 44f5ea176..5d6a63b21 100644 --- a/logfire/_internal/integrations/httpx.py +++ b/logfire/_internal/integrations/httpx.py @@ -148,11 +148,11 @@ def instrument_httpx( ) tracer_provider = final_kwargs['tracer_provider'] - instrumentor.instrument_client(client, tracer_provider, request_hook, response_hook) # type: ignore[reportArgumentType] + instrumentor.instrument_client(client, tracer_provider, request_hook, response_hook) class LogfireHttpxInfoMixin: - headers: httpx.Headers + headers: httpx.Headers | None @property def content_type_header_object(self) -> ContentTypeHeader: @@ -160,14 +160,18 @@ def content_type_header_object(self) -> ContentTypeHeader: @property def content_type_header_string(self) -> str: - return self.headers.get('content-type', '') + if self.headers is not None: + return self.headers.get('content-type', '') + + raise TypeError('Content Type Header String returned None') class LogfireHttpxRequestInfo(RequestInfo, LogfireHttpxInfoMixin): span: Span def capture_headers(self): - capture_request_or_response_headers(self.span, self.headers, 'request') + if self.headers is not None: + capture_request_or_response_headers(self.span, self.headers, 'request') def capture_body(self): captured_form = self.capture_body_if_form() @@ -189,7 +193,7 @@ def capture_body_if_form(self, attr_name: str = 'http.request.body.form') -> boo return False data = self.form_data - if not (data and isinstance(data, Mapping)): # pragma: no cover # type: ignore + if not data: return False self.set_complex_span_attributes({attr_name: data}) return True @@ -234,7 +238,8 @@ class LogfireHttpxResponseInfo(ResponseInfo, LogfireHttpxInfoMixin): is_async: bool def capture_headers(self): - capture_request_or_response_headers(self.span, self.headers, 'response') + if self.headers is not None: + capture_request_or_response_headers(self.span, self.headers, 'response') def capture_body_if_text(self, attr_name: str = 'http.response.body.text'): def hook(span: LogfireSpan): diff --git a/logfire/integrations/httpx.py b/logfire/integrations/httpx.py index bbfc7c589..aeaf1a5d9 100644 --- a/logfire/integrations/httpx.py +++ b/logfire/integrations/httpx.py @@ -1,44 +1,12 @@ from __future__ import annotations -from typing import Any, Awaitable, Callable, NamedTuple - -import httpx -from opentelemetry.trace import Span - # TODO(Marcelo): When https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3098/ gets merged, # and the next version of `opentelemetry-instrumentation-httpx` is released, we can just do a reimport: -# from opentelemetry.instrumentation.httpx import RequestInfo as RequestInfo -# from opentelemetry.instrumentation.httpx import ResponseInfo as ResponseInfo -# from opentelemetry.instrumentation.httpx import RequestHook as RequestHook -# from opentelemetry.instrumentation.httpx import ResponseHook as ResponseHook - - -class RequestInfo(NamedTuple): - """Information about an HTTP request. - - This is the second parameter passed to the `RequestHook` function. - """ - - method: bytes - url: httpx.URL - headers: httpx.Headers - stream: httpx.SyncByteStream | httpx.AsyncByteStream | None - extensions: dict[str, Any] | None - - -class ResponseInfo(NamedTuple): - """Information about an HTTP response. - - This is the second parameter passed to the `ResponseHook` function. - """ - - status_code: int - headers: httpx.Headers - stream: httpx.SyncByteStream | httpx.AsyncByteStream | None - extensions: dict[str, Any] | None - - -RequestHook = Callable[[Span, RequestInfo], None] -ResponseHook = Callable[[Span, RequestInfo, ResponseInfo], None] -AsyncRequestHook = Callable[[Span, RequestInfo], Awaitable[None]] -AsyncResponseHook = Callable[[Span, RequestInfo, ResponseInfo], Awaitable[None]] +from opentelemetry.instrumentation.httpx import ( + AsyncRequestHook as AsyncRequestHook, + AsyncResponseHook as AsyncResponseHook, + RequestHook as RequestHook, + RequestInfo as RequestInfo, + ResponseHook as ResponseHook, + ResponseInfo as ResponseInfo, +) diff --git a/pyproject.toml b/pyproject.toml index a64860651..ecd2c0341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dependencies = [ "typing-extensions >= 4.1.0", "tomli >= 2.0.1; python_version < '3.11'", "executing >= 2.0.1", + "opentelemetry-instrumentation-urllib3>=0.54b1", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 0d3c8b1be..427c06fc3 100644 --- a/uv.lock +++ b/uv.lock @@ -2527,6 +2527,7 @@ dependencies = [ { name = "executing" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-urllib3" }, { name = "opentelemetry-sdk" }, { name = "protobuf" }, { name = "rich" }, @@ -2733,6 +2734,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-sqlite3", marker = "extra == 'sqlite3'", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-starlette", marker = "extra == 'starlette'", specifier = ">=0.42b0" }, { name = "opentelemetry-instrumentation-system-metrics", marker = "extra == 'system-metrics'", specifier = ">=0.42b0" }, + { name = "opentelemetry-instrumentation-urllib3", specifier = ">=0.54b1" }, { name = "opentelemetry-instrumentation-wsgi", marker = "extra == 'wsgi'", specifier = ">=0.42b0" }, { name = "opentelemetry-sdk", specifier = ">=1.21.0,<1.34.0" }, { name = "packaging", marker = "extra == 'psycopg'" }, @@ -4343,6 +4345,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/fd/e9bd23fd734bbdc028e7ebe3d25855381b696ceca214f80ad7fe74e9079c/opentelemetry_instrumentation_system_metrics-0.54b1-py3-none-any.whl", hash = "sha256:1b6f23cc8cf18b525bdb285c3664b521ce81b1e82c4f3db6a82210b8c37af1e4", size = 13093, upload-time = "2025-05-16T19:03:08.516Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-urllib3" +version = "0.54b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/6f/76a46806cd21002cac1bfd087f5e4674b195ab31ab44c773ca534b6bb546/opentelemetry_instrumentation_urllib3-0.54b1.tar.gz", hash = "sha256:0d30ba3b230e4100cfadaad29174bf7bceac70e812e4f5204e681e4b55a74cd9", size = 15697, upload-time = "2025-05-16T19:04:07.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7a/d75bec41edb6deaf1d2859bab66a84c8ba03e822e7eafdb245da205e53f6/opentelemetry_instrumentation_urllib3-0.54b1-py3-none-any.whl", hash = "sha256:e87958c297ddd36d30e1c9069f34a9690e845e4ccc2662dd80e99ed976d4c03e", size = 13123, upload-time = "2025-05-16T19:03:14.053Z" }, +] + [[package]] name = "opentelemetry-instrumentation-wsgi" version = "0.54b1"