From 3c04255a549b7ec45ad5a536b3e3601c1d17ca79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Tue, 27 Sep 2022 21:50:45 +0200 Subject: [PATCH 1/2] Instrument httpx >= 0.20 Fixes aws/aws-xray-sdk-python#248 --- aws_xray_sdk/core/patcher.py | 2 + aws_xray_sdk/ext/httpx/__init__.py | 3 + aws_xray_sdk/ext/httpx/patch.py | 85 ++++++++++++ tests/ext/httpx/__init__.py | 0 tests/ext/httpx/test_httpx.py | 198 ++++++++++++++++++++++++++++ tests/ext/httpx/test_httpx_async.py | 174 ++++++++++++++++++++++++ tox.ini | 7 + 7 files changed, 469 insertions(+) create mode 100644 aws_xray_sdk/ext/httpx/__init__.py create mode 100644 aws_xray_sdk/ext/httpx/patch.py create mode 100644 tests/ext/httpx/__init__.py create mode 100644 tests/ext/httpx/test_httpx.py create mode 100644 tests/ext/httpx/test_httpx_async.py diff --git a/aws_xray_sdk/core/patcher.py b/aws_xray_sdk/core/patcher.py index c8616b72..1a700dd9 100644 --- a/aws_xray_sdk/core/patcher.py +++ b/aws_xray_sdk/core/patcher.py @@ -26,6 +26,7 @@ 'psycopg2', 'pg8000', 'sqlalchemy_core', + 'httpx', ) NO_DOUBLE_PATCH = ( @@ -40,6 +41,7 @@ 'psycopg2', 'pg8000', 'sqlalchemy_core', + 'httpx', ) _PATCHED_MODULES = set() diff --git a/aws_xray_sdk/ext/httpx/__init__.py b/aws_xray_sdk/ext/httpx/__init__.py new file mode 100644 index 00000000..4e8acac6 --- /dev/null +++ b/aws_xray_sdk/ext/httpx/__init__.py @@ -0,0 +1,3 @@ +from .patch import patch + +__all__ = ['patch'] diff --git a/aws_xray_sdk/ext/httpx/patch.py b/aws_xray_sdk/ext/httpx/patch.py new file mode 100644 index 00000000..e67633b2 --- /dev/null +++ b/aws_xray_sdk/ext/httpx/patch.py @@ -0,0 +1,85 @@ +import httpx + +from aws_xray_sdk.core import xray_recorder +from aws_xray_sdk.core.models import http +from aws_xray_sdk.ext.util import UNKNOWN_HOSTNAME, inject_trace_header + + +def patch(): + httpx.Client = _InstrumentedClient + httpx.AsyncClient = _InstrumentedAsyncClient + httpx._api.Client = _InstrumentedClient + + +class _InstrumentedClient(httpx.Client): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._original_transport = self._transport + self._transport = SyncInstrumentedTransport(self._transport) + + +class _InstrumentedAsyncClient(httpx.AsyncClient): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._original_transport = self._transport + self._transport = AsyncInstrumentedTransport(self._transport) + + +class SyncInstrumentedTransport(httpx.BaseTransport): + def __init__(self, transport: httpx.BaseTransport): + self._wrapped_transport = transport + + def handle_request(self, request: httpx.Request) -> httpx.Response: + def httpx_processor(return_value, exception, subsegment, stack, **kwargs): + subsegment.put_http_meta(http.METHOD, request.method) + subsegment.put_http_meta( + http.URL, + str(request.url.copy_with(password=None, query=None, fragment=None)), + ) + + if return_value is not None: + subsegment.put_http_meta(http.STATUS, return_value.status_code) + elif exception: + subsegment.add_exception(exception, stack) + + inject_trace_header(request.headers, xray_recorder.current_subsegment()) + return xray_recorder.record_subsegment( + wrapped=self._wrapped_transport.handle_request, + instance=self._wrapped_transport, + args=(request,), + kwargs={}, + name=request.url.host or UNKNOWN_HOSTNAME, + namespace="remote", + meta_processor=httpx_processor, + ) + + +class AsyncInstrumentedTransport(httpx.AsyncBaseTransport): + def __init__(self, transport: httpx.AsyncBaseTransport): + self._wrapped_transport = transport + + async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + def httpx_processor(return_value, exception, subsegment, stack, **kwargs): + subsegment.put_http_meta(http.METHOD, request.method) + subsegment.put_http_meta( + http.URL, + str(request.url.copy_with(password=None, query=None, fragment=None)), + ) + + if return_value is not None: + subsegment.put_http_meta(http.STATUS, return_value.status_code) + elif exception: + subsegment.add_exception(exception, stack) + + inject_trace_header(request.headers, xray_recorder.current_subsegment()) + return await xray_recorder.record_subsegment_async( + wrapped=self._wrapped_transport.handle_async_request, + instance=self._wrapped_transport, + args=(request,), + kwargs={}, + name=request.url.host or UNKNOWN_HOSTNAME, + namespace="remote", + meta_processor=httpx_processor, + ) diff --git a/tests/ext/httpx/__init__.py b/tests/ext/httpx/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/ext/httpx/test_httpx.py b/tests/ext/httpx/test_httpx.py new file mode 100644 index 00000000..e80a21c1 --- /dev/null +++ b/tests/ext/httpx/test_httpx.py @@ -0,0 +1,198 @@ +import pytest + +import httpx +from aws_xray_sdk.core import patch +from aws_xray_sdk.core import xray_recorder +from aws_xray_sdk.core.context import Context +from aws_xray_sdk.ext.util import strip_url, get_hostname + + +patch(("httpx",)) + +# httpbin.org is created by the same author of requests to make testing http easy. +BASE_URL = "httpbin.org" + + +@pytest.fixture(autouse=True) +def construct_ctx(): + """ + Clean up context storage on each test run and begin a segment + so that later subsegment can be attached. After each test run + it cleans up context storage again. + """ + xray_recorder.configure(service="test", sampling=False, context=Context()) + xray_recorder.clear_trace_entities() + xray_recorder.begin_segment("name") + yield + xray_recorder.clear_trace_entities() + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_ok(use_client): + status_code = 200 + url = "http://{}/status/{}?foo=bar".format(BASE_URL, status_code) + if use_client: + with httpx.Client() as client: + client.get(url) + else: + httpx.get(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert get_hostname(url) == BASE_URL + assert subsegment.name == get_hostname(url) + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "GET" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_error(use_client): + status_code = 400 + url = "http://{}/status/{}".format(BASE_URL, status_code) + if use_client: + with httpx.Client() as client: + client.post(url) + else: + httpx.post(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.error + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "POST" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_throttle(use_client): + status_code = 429 + url = "http://{}/status/{}".format(BASE_URL, status_code) + if use_client: + with httpx.Client() as client: + client.head(url) + else: + httpx.head(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.error + assert subsegment.throttle + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "HEAD" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_fault(use_client): + status_code = 500 + url = "http://{}/status/{}".format(BASE_URL, status_code) + if use_client: + with httpx.Client() as client: + client.put(url) + else: + httpx.put(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.fault + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "PUT" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_nonexistent_domain(use_client): + with pytest.raises(httpx.ConnectError): + if use_client: + with httpx.Client() as client: + client.get("http://doesnt.exist") + else: + httpx.get("http://doesnt.exist") + + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.fault + + exception = subsegment.cause["exceptions"][0] + assert exception.type == "ConnectError" + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_invalid_url(use_client): + url = "KLSDFJKLSDFJKLSDJF" + with pytest.raises(httpx.UnsupportedProtocol): + if use_client: + with httpx.Client() as client: + client.get(url) + else: + httpx.get(url) + + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.fault + + http_meta = subsegment.http + assert http_meta["request"]["url"] == "/{}".format(strip_url(url)) + + exception = subsegment.cause["exceptions"][0] + assert exception.type == "UnsupportedProtocol" + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_name_uses_hostname(use_client): + if use_client: + client = httpx.Client() + else: + client = httpx + + try: + url1 = "http://{}/fakepath/stuff/koo/lai/ahh".format(BASE_URL) + client.get(url1) + subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.name == BASE_URL + http_meta1 = subsegment.http + assert http_meta1["request"]["url"] == strip_url(url1) + assert http_meta1["request"]["method"].upper() == "GET" + + url2 = "http://{}/".format(BASE_URL) + client.get(url2, params={"some": "payload", "not": "toBeIncluded"}) + subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.name == BASE_URL + http_meta2 = subsegment.http + assert http_meta2["request"]["url"] == strip_url(url2) + assert http_meta2["request"]["method"].upper() == "GET" + + url3 = "http://subdomain.{}/fakepath/stuff/koo/lai/ahh".format(BASE_URL) + try: + client.get(url3) + except httpx.ConnectError: + pass + subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.name == "subdomain." + BASE_URL + http_meta3 = subsegment.http + assert http_meta3["request"]["url"] == strip_url(url3) + assert http_meta3["request"]["method"].upper() == "GET" + finally: + if use_client: + client.close() + + +@pytest.mark.parametrize("use_client", (True, False)) +def test_strip_http_url(use_client): + status_code = 200 + url = "http://{}/get?foo=bar".format(BASE_URL) + if use_client: + with httpx.Client() as client: + client.get(url) + else: + httpx.get(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "GET" + assert http_meta["response"]["status"] == status_code diff --git a/tests/ext/httpx/test_httpx_async.py b/tests/ext/httpx/test_httpx_async.py new file mode 100644 index 00000000..2de63aa5 --- /dev/null +++ b/tests/ext/httpx/test_httpx_async.py @@ -0,0 +1,174 @@ +import pytest + +import httpx +from aws_xray_sdk.core import patch +from aws_xray_sdk.core import xray_recorder +from aws_xray_sdk.core.context import Context +from aws_xray_sdk.ext.util import strip_url, get_hostname + + +patch(("httpx",)) + +# httpbin.org is created by the same author of requests to make testing http easy. +BASE_URL = "httpbin.org" + + +@pytest.fixture(autouse=True) +def construct_ctx(): + """ + Clean up context storage on each test run and begin a segment + so that later subsegment can be attached. After each test run + it cleans up context storage again. + """ + xray_recorder.configure(service="test", sampling=False, context=Context()) + xray_recorder.clear_trace_entities() + xray_recorder.begin_segment("name") + yield + xray_recorder.clear_trace_entities() + + +@pytest.mark.asyncio +async def test_ok_async(): + status_code = 200 + url = "http://{}/status/{}?foo=bar".format(BASE_URL, status_code) + async with httpx.AsyncClient() as client: + await client.get(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert get_hostname(url) == BASE_URL + assert subsegment.name == get_hostname(url) + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "GET" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.asyncio +async def test_error_async(): + status_code = 400 + url = "http://{}/status/{}".format(BASE_URL, status_code) + async with httpx.AsyncClient() as client: + await client.post(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.error + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "POST" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.asyncio +async def test_throttle_async(): + status_code = 429 + url = "http://{}/status/{}".format(BASE_URL, status_code) + async with httpx.AsyncClient() as client: + await client.head(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.error + assert subsegment.throttle + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "HEAD" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.asyncio +async def test_fault_async(): + status_code = 500 + url = "http://{}/status/{}".format(BASE_URL, status_code) + async with httpx.AsyncClient() as client: + await client.put(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.fault + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "PUT" + assert http_meta["response"]["status"] == status_code + + +@pytest.mark.asyncio +async def test_nonexistent_domain_async(): + try: + async with httpx.AsyncClient() as client: + await client.get("http://doesnt.exist") + except Exception: + # prevent uncatch exception from breaking test run + pass + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.fault + + exception = subsegment.cause["exceptions"][0] + assert exception.type == "ConnectError" + + +@pytest.mark.asyncio +async def test_invalid_url_async(): + url = "KLSDFJKLSDFJKLSDJF" + try: + async with httpx.AsyncClient() as client: + await client.get(url) + except Exception: + # prevent uncatch exception from breaking test run + pass + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + assert subsegment.fault + + http_meta = subsegment.http + assert http_meta["request"]["url"] == "/{}".format(strip_url(url)) + + exception = subsegment.cause["exceptions"][0] + assert exception.type == "UnsupportedProtocol" + + +@pytest.mark.asyncio +async def test_name_uses_hostname_async(): + async with httpx.AsyncClient() as client: + url1 = "http://{}/fakepath/stuff/koo/lai/ahh".format(BASE_URL) + await client.get(url1) + subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.name == BASE_URL + http_meta1 = subsegment.http + assert http_meta1["request"]["url"] == strip_url(url1) + assert http_meta1["request"]["method"].upper() == "GET" + + url2 = "http://{}/".format(BASE_URL) + await client.get(url2, params={"some": "payload", "not": "toBeIncluded"}) + subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.name == BASE_URL + http_meta2 = subsegment.http + assert http_meta2["request"]["url"] == strip_url(url2) + assert http_meta2["request"]["method"].upper() == "GET" + + url3 = "http://subdomain.{}/fakepath/stuff/koo/lai/ahh".format(BASE_URL) + try: + await client.get(url3) + except Exception: + # This is an invalid url so we dont want to break the test + pass + subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.name == "subdomain." + BASE_URL + http_meta3 = subsegment.http + assert http_meta3["request"]["url"] == strip_url(url3) + assert http_meta3["request"]["method"].upper() == "GET" + + +@pytest.mark.asyncio +async def test_strip_http_url_async(): + status_code = 200 + url = "http://{}/get?foo=bar".format(BASE_URL) + async with httpx.AsyncClient() as client: + await client.get(url) + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.name == get_hostname(url) + + http_meta = subsegment.http + assert http_meta["request"]["url"] == strip_url(url) + assert http_meta["request"]["method"].upper() == "GET" + assert http_meta["response"]["status"] == status_code diff --git a/tox.ini b/tox.ini index e9dc394f..b4790e87 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,8 @@ envlist = py{27,34,35,36,37,38,39}-ext-httplib + py{37,38,39}-ext-httpx + py{27,34,35,36,37,38,39}-ext-pg8000 py{27,34,35,36,37,38,39}-ext-psycopg2 @@ -75,6 +77,9 @@ deps = ; Also, the stable version is only supported for Python 3.7+ ext-aiohttp: pytest-aiohttp < 1.0.0 + ext-httpx: httpx >= 0.20 + ext-httpx: pytest-asyncio >= 0.19 + ext-requests: requests ext-bottle: bottle >= 0.10 @@ -135,6 +140,8 @@ commands = ext-httplib: coverage run --append --source aws_xray_sdk -m pytest tests/ext/httplib + ext-httpx: coverage run --append --source aws_xray_sdk -m pytest tests/ext/httpx + ext-pg8000: coverage run --append --source aws_xray_sdk -m pytest tests/ext/pg8000 ext-psycopg2: coverage run --append --source aws_xray_sdk -m pytest tests/ext/psycopg2 From 244796719a6aacd27949e037595a091a2db699a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Tue, 11 Oct 2022 14:03:52 +0200 Subject: [PATCH 2/2] [ext.httpx] Call `inject_trace_header` with correct subsegment --- aws_xray_sdk/ext/httpx/patch.py | 76 ++++++++++++----------------- tests/ext/httpx/test_httpx.py | 40 +++++++++++---- tests/ext/httpx/test_httpx_async.py | 42 +++++++++++----- 3 files changed, 90 insertions(+), 68 deletions(-) diff --git a/aws_xray_sdk/ext/httpx/patch.py b/aws_xray_sdk/ext/httpx/patch.py index e67633b2..dfcd9bf8 100644 --- a/aws_xray_sdk/ext/httpx/patch.py +++ b/aws_xray_sdk/ext/httpx/patch.py @@ -2,7 +2,7 @@ from aws_xray_sdk.core import xray_recorder from aws_xray_sdk.core.models import http -from aws_xray_sdk.ext.util import UNKNOWN_HOSTNAME, inject_trace_header +from aws_xray_sdk.ext.util import inject_trace_header, get_hostname def patch(): @@ -32,28 +32,21 @@ def __init__(self, transport: httpx.BaseTransport): self._wrapped_transport = transport def handle_request(self, request: httpx.Request) -> httpx.Response: - def httpx_processor(return_value, exception, subsegment, stack, **kwargs): - subsegment.put_http_meta(http.METHOD, request.method) - subsegment.put_http_meta( - http.URL, - str(request.url.copy_with(password=None, query=None, fragment=None)), - ) - - if return_value is not None: - subsegment.put_http_meta(http.STATUS, return_value.status_code) - elif exception: - subsegment.add_exception(exception, stack) - - inject_trace_header(request.headers, xray_recorder.current_subsegment()) - return xray_recorder.record_subsegment( - wrapped=self._wrapped_transport.handle_request, - instance=self._wrapped_transport, - args=(request,), - kwargs={}, - name=request.url.host or UNKNOWN_HOSTNAME, - namespace="remote", - meta_processor=httpx_processor, - ) + with xray_recorder.in_subsegment( + get_hostname(str(request.url)), namespace="remote" + ) as subsegment: + if subsegment is not None: + subsegment.put_http_meta(http.METHOD, request.method) + subsegment.put_http_meta( + http.URL, + str(request.url.copy_with(password=None, query=None, fragment=None)), + ) + inject_trace_header(request.headers, subsegment) + + response = self._wrapped_transport.handle_request(request) + if subsegment is not None: + subsegment.put_http_meta(http.STATUS, response.status_code) + return response class AsyncInstrumentedTransport(httpx.AsyncBaseTransport): @@ -61,25 +54,18 @@ def __init__(self, transport: httpx.AsyncBaseTransport): self._wrapped_transport = transport async def handle_async_request(self, request: httpx.Request) -> httpx.Response: - def httpx_processor(return_value, exception, subsegment, stack, **kwargs): - subsegment.put_http_meta(http.METHOD, request.method) - subsegment.put_http_meta( - http.URL, - str(request.url.copy_with(password=None, query=None, fragment=None)), - ) - - if return_value is not None: - subsegment.put_http_meta(http.STATUS, return_value.status_code) - elif exception: - subsegment.add_exception(exception, stack) - - inject_trace_header(request.headers, xray_recorder.current_subsegment()) - return await xray_recorder.record_subsegment_async( - wrapped=self._wrapped_transport.handle_async_request, - instance=self._wrapped_transport, - args=(request,), - kwargs={}, - name=request.url.host or UNKNOWN_HOSTNAME, - namespace="remote", - meta_processor=httpx_processor, - ) + async with xray_recorder.in_subsegment_async( + get_hostname(str(request.url)), namespace="remote" + ) as subsegment: + if subsegment is not None: + subsegment.put_http_meta(http.METHOD, request.method) + subsegment.put_http_meta( + http.URL, + str(request.url.copy_with(password=None, query=None, fragment=None)), + ) + inject_trace_header(request.headers, subsegment) + + response = await self._wrapped_transport.handle_async_request(request) + if subsegment is not None: + subsegment.put_http_meta(http.STATUS, response.status_code) + return response diff --git a/tests/ext/httpx/test_httpx.py b/tests/ext/httpx/test_httpx.py index e80a21c1..3bfeb967 100644 --- a/tests/ext/httpx/test_httpx.py +++ b/tests/ext/httpx/test_httpx.py @@ -33,11 +33,14 @@ def test_ok(use_client): url = "http://{}/status/{}?foo=bar".format(BASE_URL, status_code) if use_client: with httpx.Client() as client: - client.get(url) + response = client.get(url) else: - httpx.get(url) + response = httpx.get(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] assert get_hostname(url) == BASE_URL + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) http_meta = subsegment.http @@ -52,10 +55,13 @@ def test_error(use_client): url = "http://{}/status/{}".format(BASE_URL, status_code) if use_client: with httpx.Client() as client: - client.post(url) + response = client.post(url) else: - httpx.post(url) + response = httpx.post(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.error @@ -71,10 +77,13 @@ def test_throttle(use_client): url = "http://{}/status/{}".format(BASE_URL, status_code) if use_client: with httpx.Client() as client: - client.head(url) + response = client.head(url) else: - httpx.head(url) + response = httpx.head(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.error assert subsegment.throttle @@ -91,10 +100,13 @@ def test_fault(use_client): url = "http://{}/status/{}".format(BASE_URL, status_code) if use_client: with httpx.Client() as client: - client.put(url) + response = client.put(url) else: - httpx.put(url) + response = httpx.put(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.fault @@ -114,6 +126,7 @@ def test_nonexistent_domain(use_client): httpx.get("http://doesnt.exist") subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.fault exception = subsegment.cause["exceptions"][0] @@ -131,6 +144,7 @@ def test_invalid_url(use_client): httpx.get(url) subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.fault @@ -152,6 +166,7 @@ def test_name_uses_hostname(use_client): url1 = "http://{}/fakepath/stuff/koo/lai/ahh".format(BASE_URL) client.get(url1) subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.namespace == "remote" assert subsegment.name == BASE_URL http_meta1 = subsegment.http assert http_meta1["request"]["url"] == strip_url(url1) @@ -160,6 +175,7 @@ def test_name_uses_hostname(use_client): url2 = "http://{}/".format(BASE_URL) client.get(url2, params={"some": "payload", "not": "toBeIncluded"}) subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.namespace == "remote" assert subsegment.name == BASE_URL http_meta2 = subsegment.http assert http_meta2["request"]["url"] == strip_url(url2) @@ -171,6 +187,7 @@ def test_name_uses_hostname(use_client): except httpx.ConnectError: pass subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.namespace == "remote" assert subsegment.name == "subdomain." + BASE_URL http_meta3 = subsegment.http assert http_meta3["request"]["url"] == strip_url(url3) @@ -186,10 +203,13 @@ def test_strip_http_url(use_client): url = "http://{}/get?foo=bar".format(BASE_URL) if use_client: with httpx.Client() as client: - client.get(url) + response = client.get(url) else: - httpx.get(url) + response = httpx.get(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) http_meta = subsegment.http diff --git a/tests/ext/httpx/test_httpx_async.py b/tests/ext/httpx/test_httpx_async.py index 2de63aa5..c5d0560a 100644 --- a/tests/ext/httpx/test_httpx_async.py +++ b/tests/ext/httpx/test_httpx_async.py @@ -32,9 +32,12 @@ async def test_ok_async(): status_code = 200 url = "http://{}/status/{}?foo=bar".format(BASE_URL, status_code) async with httpx.AsyncClient() as client: - await client.get(url) + response = await client.get(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] assert get_hostname(url) == BASE_URL + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) http_meta = subsegment.http @@ -48,8 +51,11 @@ async def test_error_async(): status_code = 400 url = "http://{}/status/{}".format(BASE_URL, status_code) async with httpx.AsyncClient() as client: - await client.post(url) + response = await client.post(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.error @@ -64,8 +70,11 @@ async def test_throttle_async(): status_code = 429 url = "http://{}/status/{}".format(BASE_URL, status_code) async with httpx.AsyncClient() as client: - await client.head(url) + response = await client.head(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.error assert subsegment.throttle @@ -81,8 +90,11 @@ async def test_fault_async(): status_code = 500 url = "http://{}/status/{}".format(BASE_URL, status_code) async with httpx.AsyncClient() as client: - await client.put(url) + response = await client.put(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.fault @@ -94,13 +106,12 @@ async def test_fault_async(): @pytest.mark.asyncio async def test_nonexistent_domain_async(): - try: + with pytest.raises(httpx.ConnectError): async with httpx.AsyncClient() as client: await client.get("http://doesnt.exist") - except Exception: - # prevent uncatch exception from breaking test run - pass + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.fault exception = subsegment.cause["exceptions"][0] @@ -110,13 +121,12 @@ async def test_nonexistent_domain_async(): @pytest.mark.asyncio async def test_invalid_url_async(): url = "KLSDFJKLSDFJKLSDJF" - try: + with pytest.raises(httpx.UnsupportedProtocol): async with httpx.AsyncClient() as client: await client.get(url) - except Exception: - # prevent uncatch exception from breaking test run - pass + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) assert subsegment.fault @@ -133,6 +143,7 @@ async def test_name_uses_hostname_async(): url1 = "http://{}/fakepath/stuff/koo/lai/ahh".format(BASE_URL) await client.get(url1) subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.namespace == "remote" assert subsegment.name == BASE_URL http_meta1 = subsegment.http assert http_meta1["request"]["url"] == strip_url(url1) @@ -141,6 +152,7 @@ async def test_name_uses_hostname_async(): url2 = "http://{}/".format(BASE_URL) await client.get(url2, params={"some": "payload", "not": "toBeIncluded"}) subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.namespace == "remote" assert subsegment.name == BASE_URL http_meta2 = subsegment.http assert http_meta2["request"]["url"] == strip_url(url2) @@ -153,6 +165,7 @@ async def test_name_uses_hostname_async(): # This is an invalid url so we dont want to break the test pass subsegment = xray_recorder.current_segment().subsegments[-1] + assert subsegment.namespace == "remote" assert subsegment.name == "subdomain." + BASE_URL http_meta3 = subsegment.http assert http_meta3["request"]["url"] == strip_url(url3) @@ -164,8 +177,11 @@ async def test_strip_http_url_async(): status_code = 200 url = "http://{}/get?foo=bar".format(BASE_URL) async with httpx.AsyncClient() as client: - await client.get(url) + response = await client.get(url) + assert "x-amzn-trace-id" in response._request.headers + subsegment = xray_recorder.current_segment().subsegments[0] + assert subsegment.namespace == "remote" assert subsegment.name == get_hostname(url) http_meta = subsegment.http