Repository: open-telemetry/opentelemetry-python-contrib

Issue:
HTTPXClientInstrumentor mixes async and non async hooks
**Describe your environment** 
Python 3.10.3
opentelemetry-api==1.12.0rc2
opentelemetry-sdk==1.12.0rc2
opentelemetry-instrumentation-httpx==0.32b0

**Steps to reproduce**
Run this example:
```
 from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
 import httpx, asyncio

 def httpx_request_hook(span, request):
     print("test")

 HTTPXClientInstrumentor().instrument(request_hook=httpx_request_hook)

 # This succeeds
 with httpx.Client() as client:
     client.get("http://example.com")

 async def test_func():
     async with httpx.AsyncClient() as client:
         await client.get("http://example.com")

 # this fails with `TypeError: object NoneType can't be used in 'await' expression`
 asyncio.run(test_func())
```

**What is the expected behavior?**
After instrumentation it should be possible to successfully use httpx in both sync and async contexts.

**What is the actual behavior?**
If a non-async hook is provided then an exception is raised when the AsyncClient is used. If an async hook is provided then it never executes when the non-async Client is used.

**Additional context**
It seems to me this should either allow both async and sync hooks to be provided separately. Or it should require that the hook be non-blocking and only support non-async hooks.


Discussion:
I'm willing to write up a PR with either of the suggestions in "Additional context" or some other option if someone closer to the project wants to weigh in on what the preferred approach would be.
+1 to former suggestion.
@alec-deason send a PR for the fix :)

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
@@ -131,7 +131,21 @@ def response_hook(span, request, response):
         # 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 @@ def response_hook(span, request, response):
 
 .. 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 @@ def response_hook(span, request, response):
         # 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 @@ def response_hook(span, request, response):
         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 @@ def __init__(
         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 @@ def _instrument(self, **kwargs):
         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 @@ def _uninstrument(self, **kwargs):
     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
 


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py
--- a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py
+++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py
@@ -421,6 +421,28 @@ def test_response_hook(self):
             )
             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 @@ def test_request_hook(self):
             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,