Skip to content

Commit 1b1c38d

Browse files
samypr100ocelotlshalevr
authoredNov 21, 2023
[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>
1 parent 773e431 commit 1b1c38d

File tree

4 files changed

+122
-14
lines changed

4 files changed

+122
-14
lines changed
 

‎CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- `opentelemetry-instrumentation` Added Otel semantic convention opt-in mechanism
1313
([#1987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1987))
14+
- `opentelemetry-instrumentation-httpx` Fix mixing async and non async hooks
15+
([#1920](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1920))
1416

1517
### Fixed
1618

‎instrumentation/opentelemetry-instrumentation-httpx/README.rst

+32-2
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,29 @@ The hooks can be configured as follows:
136136
# status_code, headers, stream, extensions = response
137137
pass
138138
139-
HTTPXClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
139+
async def async_request_hook(span, request):
140+
# method, url, headers, stream, extensions = request
141+
pass
142+
143+
async def async_response_hook(span, request, response):
144+
# method, url, headers, stream, extensions = request
145+
# status_code, headers, stream, extensions = response
146+
pass
147+
148+
HTTPXClientInstrumentor().instrument(
149+
request_hook=request_hook,
150+
response_hook=response_hook,
151+
async_request_hook=async_request_hook,
152+
async_response_hook=async_response_hook
153+
)
140154
141155
142156
Or if you are using the transport classes directly:
143157

144158

145159
.. code-block:: python
146160
147-
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport
161+
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport
148162
149163
def request_hook(span, request):
150164
# method, url, headers, stream, extensions = request
@@ -155,13 +169,29 @@ Or if you are using the transport classes directly:
155169
# status_code, headers, stream, extensions = response
156170
pass
157171
172+
async def async_request_hook(span, request):
173+
# method, url, headers, stream, extensions = request
174+
pass
175+
176+
async def async_response_hook(span, request, response):
177+
# method, url, headers, stream, extensions = request
178+
# status_code, headers, stream, extensions = response
179+
pass
180+
158181
transport = httpx.HTTPTransport()
159182
telemetry_transport = SyncOpenTelemetryTransport(
160183
transport,
161184
request_hook=request_hook,
162185
response_hook=response_hook
163186
)
164187
188+
async_transport = httpx.AsyncHTTPTransport()
189+
async_telemetry_transport = AsyncOpenTelemetryTransport(
190+
async_transport,
191+
request_hook=async_request_hook,
192+
response_hook=async_response_hook
193+
)
194+
165195
166196
References
167197
----------

‎instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py

+52-12
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,29 @@ def response_hook(span, request, response):
131131
# status_code, headers, stream, extensions = response
132132
pass
133133
134-
HTTPXClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
134+
async def async_request_hook(span, request):
135+
# method, url, headers, stream, extensions = request
136+
pass
137+
138+
async def async_response_hook(span, request, response):
139+
# method, url, headers, stream, extensions = request
140+
# status_code, headers, stream, extensions = response
141+
pass
142+
143+
HTTPXClientInstrumentor().instrument(
144+
request_hook=request_hook,
145+
response_hook=response_hook,
146+
async_request_hook=async_request_hook,
147+
async_response_hook=async_response_hook
148+
)
135149
136150
137151
Or if you are using the transport classes directly:
138152
139153
140154
.. code-block:: python
141155
142-
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport
156+
from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport
143157
144158
def request_hook(span, request):
145159
# method, url, headers, stream, extensions = request
@@ -150,13 +164,29 @@ def response_hook(span, request, response):
150164
# status_code, headers, stream, extensions = response
151165
pass
152166
167+
async def async_request_hook(span, request):
168+
# method, url, headers, stream, extensions = request
169+
pass
170+
171+
async def async_response_hook(span, request, response):
172+
# method, url, headers, stream, extensions = request
173+
# status_code, headers, stream, extensions = response
174+
pass
175+
153176
transport = httpx.HTTPTransport()
154177
telemetry_transport = SyncOpenTelemetryTransport(
155178
transport,
156179
request_hook=request_hook,
157180
response_hook=response_hook
158181
)
159182
183+
async_transport = httpx.AsyncHTTPTransport()
184+
async_telemetry_transport = AsyncOpenTelemetryTransport(
185+
async_transport,
186+
request_hook=async_request_hook,
187+
response_hook=async_response_hook
188+
)
189+
160190
API
161191
---
162192
"""
@@ -377,8 +407,8 @@ def __init__(
377407
self,
378408
transport: httpx.AsyncBaseTransport,
379409
tracer_provider: typing.Optional[TracerProvider] = None,
380-
request_hook: typing.Optional[RequestHook] = None,
381-
response_hook: typing.Optional[ResponseHook] = None,
410+
request_hook: typing.Optional[AsyncRequestHook] = None,
411+
response_hook: typing.Optional[AsyncResponseHook] = None,
382412
):
383413
self._transport = transport
384414
self._tracer = get_tracer(
@@ -511,21 +541,27 @@ def _instrument(self, **kwargs):
511541
Args:
512542
**kwargs: Optional arguments
513543
``tracer_provider``: a TracerProvider, defaults to global
514-
``request_hook``: A hook that receives the span and request that is called
515-
right after the span is created
516-
``response_hook``: A hook that receives the span, request, and response
517-
that is called right before the span ends
544+
``request_hook``: A ``httpx.Client`` hook that receives the span and request
545+
that is called right after the span is created
546+
``response_hook``: A ``httpx.Client`` hook that receives the span, request,
547+
and response that is called right before the span ends
548+
``async_request_hook``: Async ``request_hook`` for ``httpx.AsyncClient``
549+
``async_response_hook``: Async``response_hook`` for ``httpx.AsyncClient``
518550
"""
519551
self._original_client = httpx.Client
520552
self._original_async_client = httpx.AsyncClient
521553
request_hook = kwargs.get("request_hook")
522554
response_hook = kwargs.get("response_hook")
555+
async_request_hook = kwargs.get("async_request_hook", request_hook)
556+
async_response_hook = kwargs.get("async_response_hook", response_hook)
523557
if callable(request_hook):
524558
_InstrumentedClient._request_hook = request_hook
525-
_InstrumentedAsyncClient._request_hook = request_hook
559+
if callable(async_request_hook):
560+
_InstrumentedAsyncClient._request_hook = async_request_hook
526561
if callable(response_hook):
527562
_InstrumentedClient._response_hook = response_hook
528-
_InstrumentedAsyncClient._response_hook = response_hook
563+
if callable(async_response_hook):
564+
_InstrumentedAsyncClient._response_hook = async_response_hook
529565
tracer_provider = kwargs.get("tracer_provider")
530566
_InstrumentedClient._tracer_provider = tracer_provider
531567
_InstrumentedAsyncClient._tracer_provider = tracer_provider
@@ -546,8 +582,12 @@ def _uninstrument(self, **kwargs):
546582
def instrument_client(
547583
client: typing.Union[httpx.Client, httpx.AsyncClient],
548584
tracer_provider: TracerProvider = None,
549-
request_hook: typing.Optional[RequestHook] = None,
550-
response_hook: typing.Optional[ResponseHook] = None,
585+
request_hook: typing.Union[
586+
typing.Optional[RequestHook], typing.Optional[AsyncRequestHook]
587+
] = None,
588+
response_hook: typing.Union[
589+
typing.Optional[ResponseHook], typing.Optional[AsyncResponseHook]
590+
] = None,
551591
) -> None:
552592
"""Instrument httpx Client or AsyncClient
553593

‎instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py

+36
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,28 @@ def test_response_hook(self):
421421
)
422422
HTTPXClientInstrumentor().uninstrument()
423423

424+
def test_response_hook_sync_async_kwargs(self):
425+
HTTPXClientInstrumentor().instrument(
426+
tracer_provider=self.tracer_provider,
427+
response_hook=_response_hook,
428+
async_response_hook=_async_response_hook,
429+
)
430+
client = self.create_client()
431+
result = self.perform_request(self.URL, client=client)
432+
433+
self.assertEqual(result.text, "Hello!")
434+
span = self.assert_span()
435+
self.assertEqual(
436+
span.attributes,
437+
{
438+
SpanAttributes.HTTP_METHOD: "GET",
439+
SpanAttributes.HTTP_URL: self.URL,
440+
SpanAttributes.HTTP_STATUS_CODE: 200,
441+
HTTP_RESPONSE_BODY: "Hello!",
442+
},
443+
)
444+
HTTPXClientInstrumentor().uninstrument()
445+
424446
def test_request_hook(self):
425447
HTTPXClientInstrumentor().instrument(
426448
tracer_provider=self.tracer_provider,
@@ -434,6 +456,20 @@ def test_request_hook(self):
434456
self.assertEqual(span.name, "GET" + self.URL)
435457
HTTPXClientInstrumentor().uninstrument()
436458

459+
def test_request_hook_sync_async_kwargs(self):
460+
HTTPXClientInstrumentor().instrument(
461+
tracer_provider=self.tracer_provider,
462+
request_hook=_request_hook,
463+
async_request_hook=_async_request_hook,
464+
)
465+
client = self.create_client()
466+
result = self.perform_request(self.URL, client=client)
467+
468+
self.assertEqual(result.text, "Hello!")
469+
span = self.assert_span()
470+
self.assertEqual(span.name, "GET" + self.URL)
471+
HTTPXClientInstrumentor().uninstrument()
472+
437473
def test_request_hook_no_span_update(self):
438474
HTTPXClientInstrumentor().instrument(
439475
tracer_provider=self.tracer_provider,

0 commit comments

Comments
 (0)
Please sign in to comment.