[opentelemetry-instrumentation-httpx] fix mixing of sync and non async hooks (#1920)

* fix: sync response hooks being used on httpx.AsyncClient

* docs: add changelog

* docs: improved docs a bit more

* docs: fix variable name

---------

Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com>
Co-authored-by: Shalev Roda <65566801+shalevr@users.noreply.github.com>
This commit is contained in:
samypr100
2023-11-21 08:45:15 -05:00
committed by GitHub
parent 773e431bf5
commit 1b1c38d7cd
4 changed files with 122 additions and 14 deletions

View File

@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation` Added Otel semantic convention opt-in mechanism
([#1987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1987))
- `opentelemetry-instrumentation-httpx` Fix mixing async and non async hooks
([#1920](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1920))
### Fixed

View File

@ -136,7 +136,21 @@ The hooks can be configured as follows:
# status_code, headers, stream, extensions = response
pass
HTTPXClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
async def async_request_hook(span, request):
# method, url, headers, stream, extensions = request
pass
async def async_response_hook(span, request, response):
# method, url, headers, stream, extensions = request
# status_code, headers, stream, extensions = response
pass
HTTPXClientInstrumentor().instrument(
request_hook=request_hook,
response_hook=response_hook,
async_request_hook=async_request_hook,
async_response_hook=async_response_hook
)
Or if you are using the transport classes directly:
@ -144,7 +158,7 @@ Or if you are using the transport classes directly:
.. code-block:: python
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport
def request_hook(span, request):
# method, url, headers, stream, extensions = request
@ -155,6 +169,15 @@ Or if you are using the transport classes directly:
# status_code, headers, stream, extensions = response
pass
async def async_request_hook(span, request):
# method, url, headers, stream, extensions = request
pass
async def async_response_hook(span, request, response):
# method, url, headers, stream, extensions = request
# status_code, headers, stream, extensions = response
pass
transport = httpx.HTTPTransport()
telemetry_transport = SyncOpenTelemetryTransport(
transport,
@ -162,6 +185,13 @@ Or if you are using the transport classes directly:
response_hook=response_hook
)
async_transport = httpx.AsyncHTTPTransport()
async_telemetry_transport = AsyncOpenTelemetryTransport(
async_transport,
request_hook=async_request_hook,
response_hook=async_response_hook
)
References
----------

View File

@ -131,7 +131,21 @@ The hooks can be configured as follows:
# status_code, headers, stream, extensions = response
pass
HTTPXClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
async def async_request_hook(span, request):
# method, url, headers, stream, extensions = request
pass
async def async_response_hook(span, request, response):
# method, url, headers, stream, extensions = request
# status_code, headers, stream, extensions = response
pass
HTTPXClientInstrumentor().instrument(
request_hook=request_hook,
response_hook=response_hook,
async_request_hook=async_request_hook,
async_response_hook=async_response_hook
)
Or if you are using the transport classes directly:
@ -139,7 +153,7 @@ Or if you are using the transport classes directly:
.. code-block:: python
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport
def request_hook(span, request):
# method, url, headers, stream, extensions = request
@ -150,6 +164,15 @@ Or if you are using the transport classes directly:
# status_code, headers, stream, extensions = response
pass
async def async_request_hook(span, request):
# method, url, headers, stream, extensions = request
pass
async def async_response_hook(span, request, response):
# method, url, headers, stream, extensions = request
# status_code, headers, stream, extensions = response
pass
transport = httpx.HTTPTransport()
telemetry_transport = SyncOpenTelemetryTransport(
transport,
@ -157,6 +180,13 @@ Or if you are using the transport classes directly:
response_hook=response_hook
)
async_transport = httpx.AsyncHTTPTransport()
async_telemetry_transport = AsyncOpenTelemetryTransport(
async_transport,
request_hook=async_request_hook,
response_hook=async_response_hook
)
API
---
"""
@ -377,8 +407,8 @@ class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
self,
transport: httpx.AsyncBaseTransport,
tracer_provider: typing.Optional[TracerProvider] = None,
request_hook: typing.Optional[RequestHook] = None,
response_hook: typing.Optional[ResponseHook] = None,
request_hook: typing.Optional[AsyncRequestHook] = None,
response_hook: typing.Optional[AsyncResponseHook] = None,
):
self._transport = transport
self._tracer = get_tracer(
@ -511,21 +541,27 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
Args:
**kwargs: Optional arguments
``tracer_provider``: a TracerProvider, defaults to global
``request_hook``: A hook that receives the span and request that is called
right after the span is created
``response_hook``: A hook that receives the span, request, and response
that is called right before the span ends
``request_hook``: A ``httpx.Client`` hook that receives the span and request
that is called right after the span is created
``response_hook``: A ``httpx.Client`` hook that receives the span, request,
and response that is called right before the span ends
``async_request_hook``: Async ``request_hook`` for ``httpx.AsyncClient``
``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient``
"""
self._original_client = httpx.Client
self._original_async_client = httpx.AsyncClient
request_hook = kwargs.get("request_hook")
response_hook = kwargs.get("response_hook")
async_request_hook = kwargs.get("async_request_hook", request_hook)
async_response_hook = kwargs.get("async_response_hook", response_hook)
if callable(request_hook):
_InstrumentedClient._request_hook = request_hook
_InstrumentedAsyncClient._request_hook = request_hook
if callable(async_request_hook):
_InstrumentedAsyncClient._request_hook = async_request_hook
if callable(response_hook):
_InstrumentedClient._response_hook = response_hook
_InstrumentedAsyncClient._response_hook = response_hook
if callable(async_response_hook):
_InstrumentedAsyncClient._response_hook = async_response_hook
tracer_provider = kwargs.get("tracer_provider")
_InstrumentedClient._tracer_provider = tracer_provider
_InstrumentedAsyncClient._tracer_provider = tracer_provider
@ -546,8 +582,12 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
def instrument_client(
client: typing.Union[httpx.Client, httpx.AsyncClient],
tracer_provider: TracerProvider = None,
request_hook: typing.Optional[RequestHook] = None,
response_hook: typing.Optional[ResponseHook] = None,
request_hook: typing.Union[
typing.Optional[RequestHook], typing.Optional[AsyncRequestHook]
] = None,
response_hook: typing.Union[
typing.Optional[ResponseHook], typing.Optional[AsyncResponseHook]
] = None,
) -> None:
"""Instrument httpx Client or AsyncClient

View File

@ -421,6 +421,28 @@ class BaseTestCases:
)
HTTPXClientInstrumentor().uninstrument()
def test_response_hook_sync_async_kwargs(self):
HTTPXClientInstrumentor().instrument(
tracer_provider=self.tracer_provider,
response_hook=_response_hook,
async_response_hook=_async_response_hook,
)
client = self.create_client()
result = self.perform_request(self.URL, client=client)
self.assertEqual(result.text, "Hello!")
span = self.assert_span()
self.assertEqual(
span.attributes,
{
SpanAttributes.HTTP_METHOD: "GET",
SpanAttributes.HTTP_URL: self.URL,
SpanAttributes.HTTP_STATUS_CODE: 200,
HTTP_RESPONSE_BODY: "Hello!",
},
)
HTTPXClientInstrumentor().uninstrument()
def test_request_hook(self):
HTTPXClientInstrumentor().instrument(
tracer_provider=self.tracer_provider,
@ -434,6 +456,20 @@ class BaseTestCases:
self.assertEqual(span.name, "GET" + self.URL)
HTTPXClientInstrumentor().uninstrument()
def test_request_hook_sync_async_kwargs(self):
HTTPXClientInstrumentor().instrument(
tracer_provider=self.tracer_provider,
request_hook=_request_hook,
async_request_hook=_async_request_hook,
)
client = self.create_client()
result = self.perform_request(self.URL, client=client)
self.assertEqual(result.text, "Hello!")
span = self.assert_span()
self.assertEqual(span.name, "GET" + self.URL)
HTTPXClientInstrumentor().uninstrument()
def test_request_hook_no_span_update(self):
HTTPXClientInstrumentor().instrument(
tracer_provider=self.tracer_provider,