Repository: open-telemetry/opentelemetry-python-contrib

Issue:
httpx instrumentation doesn't work for `httpx.get`, `httpx.post`, etc.
**Describe your environment** Describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on main.
```
#Python
Python 3.8.16

# OTEL
opentelemetry-api==1.15.0 ; python_version >= '3.7'
opentelemetry-instrumentation==0.36b0 ; python_version >= '3.7'
opentelemetry-instrumentation-httpx==0.36b0
opentelemetry-instrumentation-requests==0.36b0
opentelemetry-sdk==1.15.0 ; python_version >= '3.7'

# 3rd Party
httpx==0.23.3
requests==2.28.2 ; 
python_version >= '3.6'
```

Azure Function run locally.

**Steps to reproduce**
Describe exactly how to reproduce the error. Include a code sample if applicable.
1. Install `requests` and `httpx` libs and instrumentations
2. Initialize instrumentors
3. Make a `get/post/etc` request to some api without initializing a client.
4. Make a `get/post/etc` request to some api with initializing a client.

```python
import httpx 
import requests

from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor

HTTPXClientInstrumentor().instrument()
RequestsInstrumentor().instrument()


requests.get("www.google.com")
httpx.get("www.google.com")


with httpx.Client() as client:
    response = client.get("https://www.google.com")
with requests.Session() as session:
    response = session.get("https://www.google.com")

```

**What is the expected behavior?**
What did you expect to see?

Whenever I use `httpx.get` (or similar) a new dependency/span should be created and exported - in my case to Azure Application Insights.

**What is the actual behavior?**
What did you see instead?

I don't see expected dependencies/spans/traces exported.

<img width="628" alt="Zrzut ekranu 2023-04-3 o 12 53 01" src="https://user-images.githubusercontent.com/82878433/229490157-edb23b78-a14e-4b21-80b8-f82171414449.png">

**Additional context**
Full example of the function. 

```python
import httpx
import logging
import requests

import azure.functions as func
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor

from tracing import OpenTelemetryExtension, TracedClass


OpenTelemetryExtension.configure(instrumentors=[
    HTTPXClientInstrumentor,
    RequestsInstrumentor
])


def main(req: func.HttpRequest, context: func.Context):
    with context.span():
        return _main(req)


class Greeter(TracedClass):
    trace_private_methods = True
    trace_exclude_methods = ["_private"]
        
    def __init__(self) -> None:
        super().__init__()
        
    def _private(self):
        pass
        
    def greet(self):
        self._private()
        response = httpx.get("https://api.namefake.com/")  # doesn't work with httpx
        response = requests.get("https://api.namefake.com/")  # works with requests
        body = response.json()
        return f"Hello {body['name']}"


def _main(req: func.HttpRequest) -> None:
    logger = logging.getLogger(__name__)
    logger.info("Hello world!")
    g = Greeter()
    greeting = g.greet()
    logger.info("%s", greeting)
    with httpx.Client() as client:
        response = client.get("https://www.google.com")  # works correctly
    with requests.Session() as session:
        response = session.get("https://www.google.com")  # works correctly
    return func.HttpResponse(response.content, headers={"Content-Type": "text/html"})
```

It uses [OpenTelemetryExtension POC described here](https://github.com/Azure/azure-sdk-for-python/issues/29672) and `TracedClass` implemented as follows:
```python
class MetaTracer(type):
    def __new__(cls, name, bases, attrs):
        for key, value in attrs.items():
            if any([
                type(value) is not FunctionType,
                key in attrs.get("trace_exclude_methods", [])
            ]):
                continue
            method_type = cls._get_method_type(key)
            trace_setting = f"trace_{method_type.value}_methods"
            should_be_traced = attrs.get(trace_setting, getattr(bases[0], trace_setting, False))
            if not should_be_traced:
                continue
            setattr(value, "__trace_name__", (
                f"{value.__module__}::{name}::{value.__name__}"
            ))
            attrs[key] = trace()(value)
        return super().__new__(cls, name, bases, attrs)
    
    class _MethodType(str, Enum):
        PUBLIC = "public"
        PRIVATE = "private"
        MAGIC = "magic"
    
    @staticmethod
    def _get_method_type(name: str) -> _MethodType:
        if name.startswith("__") and name.endswith("__"):
            return MetaTracer._MethodType.MAGIC
        if name.startswith("_"):
            return MetaTracer._MethodType.PRIVATE
        return MetaTracer._MethodType.PUBLIC


class TracedClass(metaclass=MetaTracer):
    trace_magic_methods = False
    trace_private_methods = False
    trace_public_methods = True
    trace_exclude_methods = []
```

While I understand why we should use `httpx.Client` over `httpx.get` there are reasons why people are using latter more often than former, but current implementation doesn't allow to trace simple requests. In the OpenCensus httpx extensions there was an opposite issue: httpx.Client was not traced instead - https://github.com/census-instrumentation/opencensus-python/pull/1186.

I'm up for working on this issue and would love to hear your comments and tips.


Discussion:
@lzchen @jeremydvoss 
@macieyng funny that you opened this, I just hit the same thing where no httpx spans showed up. If you turn on debug logging in `opentelemetry` my guess is maybe you have a dependency conflict? I noticed you've got httpx 0.23:

```
[2023-04-06T15:58:29.753796-05:00] | DEBUG    | [service=tempus env=localdev version=no-rel-identifier-found trace_id=0 span_id=0] [opentelemetry.instrumentation.auto_instrumentation.sitecustomize] [sitecustomize.py:_load_instrumentors:76] - Skipping instrumentation httpx: DependencyConflict: requested: "httpx<=0.23.0,>=0.18.0" but found: "httpx 0.23.3"
```

This was caused by the changes at https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1460
@phillipuniverse I haven't check that, but seems like you're right about it. Thank you for linking the issues and making the PR! 
There were similar issues with OpenCensus in the past that, some versions compatibility was too strict. 


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
@@ -564,11 +564,13 @@ def _instrument(self, **kwargs):
         tracer_provider = kwargs.get("tracer_provider")
         _InstrumentedClient._tracer_provider = tracer_provider
         _InstrumentedAsyncClient._tracer_provider = tracer_provider
-        httpx.Client = _InstrumentedClient
+        # Intentionally using a private attribute here, see:
+        # https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2538#discussion_r1610603719
+        httpx.Client = httpx._api.Client = _InstrumentedClient
         httpx.AsyncClient = _InstrumentedAsyncClient
 
     def _uninstrument(self, **kwargs):
-        httpx.Client = self._original_client
+        httpx.Client = httpx._api.Client = self._original_client
         httpx.AsyncClient = self._original_async_client
         _InstrumentedClient._tracer_provider = None
         _InstrumentedClient._request_hook = None


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
@@ -532,12 +532,36 @@ def test_instrument_client(self):
             self.assertEqual(result.text, "Hello!")
             self.assert_span(num_spans=1)
 
+        def test_instrumentation_without_client(self):
+
+            HTTPXClientInstrumentor().instrument()
+            results = [
+                httpx.get(self.URL),
+                httpx.request("GET", self.URL),
+            ]
+            with httpx.stream("GET", self.URL) as stream:
+                stream.read()
+                results.append(stream)
+
+            spans = self.assert_span(num_spans=len(results))
+            for idx, res in enumerate(results):
+                with self.subTest(idx=idx, res=res):
+                    self.assertEqual(res.text, "Hello!")
+                    self.assertEqual(
+                        spans[idx].attributes[SpanAttributes.HTTP_URL],
+                        self.URL,
+                    )
+
+            HTTPXClientInstrumentor().uninstrument()
+
         def test_uninstrument(self):
             HTTPXClientInstrumentor().instrument()
             HTTPXClientInstrumentor().uninstrument()
             client = self.create_client()
             result = self.perform_request(self.URL, client=client)
+            result_no_client = httpx.get(self.URL)
             self.assertEqual(result.text, "Hello!")
+            self.assertEqual(result_no_client.text, "Hello!")
             self.assert_span(num_spans=0)
 
         def test_uninstrument_client(self):