fix(httpx): check if mount transport is None before wrap (#3022)

This commit is contained in:
Emídio Neto 2024-11-19 16:40:06 -03:00 committed by GitHub
parent 1c820ea96e
commit 19a59e4be7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 27 deletions

View File

@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-httpx`: instrument_client is a static method again - `opentelemetry-instrumentation-httpx`: instrument_client is a static method again
([#3003](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3003)) ([#3003](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3003))
- `opentelemetry-instrumentation-httpx`: Check if mount transport is none before wrap it
([#3022](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3022))
### Breaking changes ### Breaking changes

View File

@ -1005,17 +1005,18 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
), ),
) )
for transport in client._mounts.values(): for transport in client._mounts.values():
wrap_function_wrapper( if hasattr(transport, "handle_request"):
transport, wrap_function_wrapper(
"handle_request", transport,
partial( "handle_request",
cls._handle_request_wrapper, partial(
tracer=tracer, cls._handle_request_wrapper,
sem_conv_opt_in_mode=sem_conv_opt_in_mode, tracer=tracer,
request_hook=request_hook, sem_conv_opt_in_mode=sem_conv_opt_in_mode,
response_hook=response_hook, request_hook=request_hook,
), response_hook=response_hook,
) ),
)
client._is_instrumented_by_opentelemetry = True client._is_instrumented_by_opentelemetry = True
if hasattr(client._transport, "handle_async_request"): if hasattr(client._transport, "handle_async_request"):
wrap_function_wrapper( wrap_function_wrapper(
@ -1030,17 +1031,18 @@ class HTTPXClientInstrumentor(BaseInstrumentor):
), ),
) )
for transport in client._mounts.values(): for transport in client._mounts.values():
wrap_function_wrapper( if hasattr(transport, "handle_async_request"):
transport, wrap_function_wrapper(
"handle_async_request", transport,
partial( "handle_async_request",
cls._handle_async_request_wrapper, partial(
tracer=tracer, cls._handle_async_request_wrapper,
sem_conv_opt_in_mode=sem_conv_opt_in_mode, tracer=tracer,
async_request_hook=async_request_hook, sem_conv_opt_in_mode=sem_conv_opt_in_mode,
async_response_hook=async_response_hook, async_request_hook=async_request_hook,
), async_response_hook=async_response_hook,
) ),
)
client._is_instrumented_by_opentelemetry = True client._is_instrumented_by_opentelemetry = True
@staticmethod @staticmethod

View File

@ -741,6 +741,10 @@ class BaseTestCases:
def create_proxy_transport(self, url: str): def create_proxy_transport(self, url: str):
pass pass
@abc.abstractmethod
def get_transport_handler(self, transport):
pass
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.client = self.create_client() self.client = self.create_client()
@ -763,17 +767,15 @@ class BaseTestCases:
self.assertEqual(len(mounts), num_mounts) self.assertEqual(len(mounts), num_mounts)
for transport in mounts: for transport in mounts:
with self.subTest(transport): with self.subTest(transport):
if transport is None:
continue
if transport_type: if transport_type:
self.assertIsInstance( self.assertIsInstance(
transport, transport,
transport_type, transport_type,
) )
else: else:
handler = getattr(transport, "handle_request", None) handler = self.get_transport_handler(transport)
if not handler:
handler = getattr(
transport, "handle_async_request"
)
self.assertTrue( self.assertTrue(
isinstance(handler, ObjectProxy) isinstance(handler, ObjectProxy)
and getattr(handler, "__wrapped__") and getattr(handler, "__wrapped__")
@ -983,6 +985,21 @@ class BaseTestCases:
self.assertEqual(result.text, "Hello!") self.assertEqual(result.text, "Hello!")
self.assert_span() self.assert_span()
@mock.patch.dict(
"os.environ", {"NO_PROXY": "http://mock/status/200"}, clear=True
)
def test_instrument_with_no_proxy(self):
proxy_mounts = self.create_proxy_mounts()
HTTPXClientInstrumentor().instrument()
client = self.create_client(mounts=proxy_mounts)
result = self.perform_request(self.URL, client=client)
self.assert_span(num_spans=1)
self.assertEqual(result.text, "Hello!")
self.assert_proxy_mounts(
client._mounts.values(),
3,
)
def test_instrument_proxy(self): def test_instrument_proxy(self):
proxy_mounts = self.create_proxy_mounts() proxy_mounts = self.create_proxy_mounts()
HTTPXClientInstrumentor().instrument() HTTPXClientInstrumentor().instrument()
@ -994,6 +1011,27 @@ class BaseTestCases:
2, 2,
) )
@mock.patch.dict(
"os.environ", {"NO_PROXY": "http://mock/status/200"}, clear=True
)
def test_instrument_client_with_no_proxy(self):
proxy_mounts = self.create_proxy_mounts()
client = self.create_client(mounts=proxy_mounts)
self.assert_proxy_mounts(
client._mounts.values(),
3,
(httpx.HTTPTransport, httpx.AsyncHTTPTransport),
)
HTTPXClientInstrumentor.instrument_client(client)
result = self.perform_request(self.URL, client=client)
self.assertEqual(result.text, "Hello!")
self.assert_span(num_spans=1)
self.assert_proxy_mounts(
client._mounts.values(),
3,
)
HTTPXClientInstrumentor.uninstrument_client(client)
def test_instrument_client_with_proxy(self): def test_instrument_client_with_proxy(self):
proxy_mounts = self.create_proxy_mounts() proxy_mounts = self.create_proxy_mounts()
client = self.create_client(mounts=proxy_mounts) client = self.create_client(mounts=proxy_mounts)
@ -1188,6 +1226,9 @@ class TestSyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest):
def create_proxy_transport(self, url): def create_proxy_transport(self, url):
return httpx.HTTPTransport(proxy=httpx.Proxy(url)) return httpx.HTTPTransport(proxy=httpx.Proxy(url))
def get_transport_handler(self, transport):
return getattr(transport, "handle_request", None)
def test_can_instrument_subclassed_client(self): def test_can_instrument_subclassed_client(self):
class CustomClient(httpx.Client): class CustomClient(httpx.Client):
pass pass
@ -1241,6 +1282,9 @@ class TestAsyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest):
def create_proxy_transport(self, url): def create_proxy_transport(self, url):
return httpx.AsyncHTTPTransport(proxy=httpx.Proxy(url)) return httpx.AsyncHTTPTransport(proxy=httpx.Proxy(url))
def get_transport_handler(self, transport):
return getattr(transport, "handle_async_request", None)
def test_basic_multiple(self): def test_basic_multiple(self):
# We need to create separate clients because in httpx >= 0.19, # We need to create separate clients because in httpx >= 0.19,
# closing the client after "with" means the second http call fails # closing the client after "with" means the second http call fails