From c439511aac6f39b5108cfe0762815557eaad606a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Moska=C5=82a?= Date: Thu, 13 May 2021 11:43:20 +0200 Subject: [PATCH] Added aiohttp integration --- contrib/opencensus-ext-aiohttp/CHANGELOG.md | 8 + contrib/opencensus-ext-aiohttp/README.rst | 53 ++ .../opencensus/__init__.py | 1 + .../opencensus/ext/__init__.py | 1 + .../opencensus/ext/aiohttp/__init__.py | 17 + .../opencensus/ext/aiohttp/trace.py | 131 +++++ contrib/opencensus-ext-aiohttp/setup.cfg | 2 + contrib/opencensus-ext-aiohttp/setup.py | 48 ++ .../tests/test_aiohttp_trace.py | 479 ++++++++++++++++++ contrib/opencensus-ext-aiohttp/version.py | 15 + 10 files changed, 755 insertions(+) create mode 100644 contrib/opencensus-ext-aiohttp/CHANGELOG.md create mode 100644 contrib/opencensus-ext-aiohttp/README.rst create mode 100644 contrib/opencensus-ext-aiohttp/opencensus/__init__.py create mode 100644 contrib/opencensus-ext-aiohttp/opencensus/ext/__init__.py create mode 100644 contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/__init__.py create mode 100644 contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/trace.py create mode 100644 contrib/opencensus-ext-aiohttp/setup.cfg create mode 100644 contrib/opencensus-ext-aiohttp/setup.py create mode 100644 contrib/opencensus-ext-aiohttp/tests/test_aiohttp_trace.py create mode 100644 contrib/opencensus-ext-aiohttp/version.py diff --git a/contrib/opencensus-ext-aiohttp/CHANGELOG.md b/contrib/opencensus-ext-aiohttp/CHANGELOG.md new file mode 100644 index 000000000..bf928f0e5 --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +- Added attributes following specs listed [here](https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/HTTP.md#attributes) + ([#746](https://github.com/census-instrumentation/opencensus-python/pull/746)) +- Support exporter changes in `opencensus>=0.7.0` +- Initial version diff --git a/contrib/opencensus-ext-aiohttp/README.rst b/contrib/opencensus-ext-aiohttp/README.rst new file mode 100644 index 000000000..b172deba6 --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/README.rst @@ -0,0 +1,53 @@ +OpenCensus requests Integration +============================================================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opencensus-ext-aiohttp.svg + :target: https://pypi.org/project/opencensus-ext-aiohttp/ + +OpenCensus can trace asynchronous HTTP requests made with the `aiohttp package`_. The request URL, +method, and status will be collected. + +You can enable aiohttp integration by specifying ``'aiohttp'`` to ``trace_integrations``. + +It's possible to configure a list of URL you don't want traced, anf it's configurable by giving an array of +hostname/port to the attribute ``excludelist_hostnames`` in OpenCensus context's attributes: + + +.. _aiohttp package: https://pypi.python.org/pypi/aiohttp + +Installation +------------ + +:: + + pip install opencensus-ext-requests + +Usage +----- + +.. code:: python + + import asyncio + from aiohttp import ClientSession + from opencensus.trace import config_integration + from opencensus.trace.tracer import Tracer + + + config_integration.trace_integrations(['aiohttp']) + + async def main(): + tracer = Tracer() + with tracer.span(name='parent'): + client_session = ClientSession() + async with client_session.get(url='https://www.wikipedia.org/wiki/Rabbit') as response: + await response.read() + + + asyncio.run(main()) + +References +---------- + +* `OpenCensus Project `_ diff --git a/contrib/opencensus-ext-aiohttp/opencensus/__init__.py b/contrib/opencensus-ext-aiohttp/opencensus/__init__.py new file mode 100644 index 000000000..8db66d3d0 --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/opencensus/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/contrib/opencensus-ext-aiohttp/opencensus/ext/__init__.py b/contrib/opencensus-ext-aiohttp/opencensus/ext/__init__.py new file mode 100644 index 000000000..8db66d3d0 --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/opencensus/ext/__init__.py @@ -0,0 +1 @@ +__path__ = __import__("pkgutil").extend_path(__path__, __name__) diff --git a/contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/__init__.py b/contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/__init__.py new file mode 100644 index 000000000..3b528cd4c --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2017, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opencensus.ext.aiohttp import trace + +__all__ = ["trace"] diff --git a/contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/trace.py b/contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/trace.py new file mode 100644 index 000000000..b5e2f5fb4 --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/opencensus/ext/aiohttp/trace.py @@ -0,0 +1,131 @@ +# Copyright 2017, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +import logging + +import wrapt +from aiohttp import InvalidURL, ServerTimeoutError +from yarl import URL + +from opencensus.trace import ( + attributes_helper, + exceptions_status, + execution_context, + utils, +) +from opencensus.trace.span import SpanKind + +logger = logging.getLogger(__name__) + +MODULE_NAME = "aiohttp" + +COMPONENT = attributes_helper.COMMON_ATTRIBUTES["COMPONENT"] +HTTP_COMPONENT = "HTTP" +HTTP_HOST = attributes_helper.COMMON_ATTRIBUTES["HTTP_HOST"] +HTTP_METHOD = attributes_helper.COMMON_ATTRIBUTES["HTTP_METHOD"] +HTTP_PATH = attributes_helper.COMMON_ATTRIBUTES["HTTP_PATH"] +HTTP_ROUTE = attributes_helper.COMMON_ATTRIBUTES["HTTP_ROUTE"] +HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES["HTTP_STATUS_CODE"] +HTTP_URL = attributes_helper.COMMON_ATTRIBUTES["HTTP_URL"] + + +def trace_integration(tracer=None): + """Wrap the aiohttp library to trace it.""" + logger.info("Integrated module: {}".format(MODULE_NAME)) + + if tracer is not None: + # The execution_context tracer should never be None - if it has not + # been set it returns a no-op tracer. Most code in this library does + # not handle None being used in the execution context. + execution_context.set_opencensus_tracer(tracer) + + # Wrap Session class + wrapt.wrap_function_wrapper( + module=MODULE_NAME, name="ClientSession._request", wrapper=wrap_session_request + ) + + +async def wrap_session_request(wrapped, _, args, kwargs): + """Wrap the session function to trace it.""" + if execution_context.is_exporter(): + return await wrapped(*args, **kwargs) + + method = kwargs.get("method") or args[0] + str_or_url = kwargs.get("str_or_url") or args[1] + try: + url = URL(str_or_url) + except ValueError as e: + raise InvalidURL(str_or_url) from e + + excludelist_hostnames = execution_context.get_opencensus_attr( + "excludelist_hostnames" + ) + url_host_with_port = url.host + (f":{url.port}" if url.port else "") + if utils.disable_tracing_hostname(url_host_with_port, excludelist_hostnames): + return await wrapped(*args, **kwargs) + + url_path = url.path or "/" + + tracer = execution_context.get_opencensus_tracer() + with tracer.span(name=url_path) as span: + span.span_kind = SpanKind.CLIENT + + try: + tracer_headers = tracer.propagator.to_headers( + span_context=tracer.span_context, + ) + kwargs.setdefault("headers", {}).update(tracer_headers) + except Exception: + pass + + span.add_attribute( + attribute_key=COMPONENT, + attribute_value=HTTP_COMPONENT, + ) + span.add_attribute( + attribute_key=HTTP_HOST, + attribute_value=url_host_with_port, + ) + span.add_attribute( + attribute_key=HTTP_METHOD, + attribute_value=method.upper(), + ) + span.add_attribute( + attribute_key=HTTP_PATH, + attribute_value=url_path, + ) + span.add_attribute( + attribute_key=HTTP_URL, + attribute_value=str(url), + ) + + try: + result = await wrapped(*args, **kwargs) + except (ServerTimeoutError, asyncio.TimeoutError): + span.set_status(exceptions_status.TIMEOUT) + raise + except InvalidURL: + span.set_status(exceptions_status.INVALID_URL) + raise + except Exception as e: + span.set_status(exceptions_status.unknown(e)) + raise + else: + status_code = int(result.status) + span.add_attribute( + attribute_key=HTTP_STATUS_CODE, + attribute_value=status_code, + ) + span.set_status(utils.status_from_http_code(http_code=status_code)) + return result diff --git a/contrib/opencensus-ext-aiohttp/setup.cfg b/contrib/opencensus-ext-aiohttp/setup.cfg new file mode 100644 index 000000000..2a9acf13d --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/contrib/opencensus-ext-aiohttp/setup.py b/contrib/opencensus-ext-aiohttp/setup.py new file mode 100644 index 000000000..fe377027e --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/setup.py @@ -0,0 +1,48 @@ +# Copyright 2019, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import find_packages, setup +from version import __version__ + +setup( + name="opencensus-ext-aiohttp", + version=__version__, # noqa + author="OpenCensus Authors", + author_email="census-developers@googlegroups.com", + classifiers=[ + "Intended Audience :: Developers", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + ], + description="OpenCensus Requests Integration", + include_package_data=True, + long_description=open("README.rst").read(), + install_requires=[ + "opencensus >= 0.8.dev0, < 1.0.0", + "aiohttp >= 3.7.4, < 4.0.0", + ], + extras_require={}, + license="Apache-2.0", + packages=find_packages(exclude=("tests",)), + namespace_packages=[], + url="https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-aiohttp", # noqa: E501 + zip_safe=False, +) diff --git a/contrib/opencensus-ext-aiohttp/tests/test_aiohttp_trace.py b/contrib/opencensus-ext-aiohttp/tests/test_aiohttp_trace.py new file mode 100644 index 000000000..99dd163ac --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/tests/test_aiohttp_trace.py @@ -0,0 +1,479 @@ +# Copyright 2017, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +from http import HTTPStatus +from unittest import IsolatedAsyncioTestCase + +import mock +from aiohttp import ClientPayloadError, InvalidURL + +from opencensus.ext.aiohttp import trace +from opencensus.trace import execution_context +from opencensus.trace import status as status_module +from opencensus.trace.span import SpanKind +from opencensus.trace.tracers import noop_tracer + + +class TestAiohttpTrace(IsolatedAsyncioTestCase): + def test_trace_integration(self): + mock_wrap = mock.Mock() + patch_wrapt = mock.patch("wrapt.wrap_function_wrapper", mock_wrap) + + with patch_wrapt: + trace.trace_integration() + + self.assertIsInstance( + execution_context.get_opencensus_tracer(), noop_tracer.NoopTracer + ) + mock_wrap.assert_called_once_with( + module="aiohttp", + name="ClientSession._request", + wrapper=trace.wrap_session_request, + ) + + def test_trace_integration_set_tracer(self): + mock_wrap = mock.Mock() + patch_wrapt = mock.patch("wrapt.wrap_function_wrapper", mock_wrap) + + TmpTracer = noop_tracer.NoopTracer + + with patch_wrapt: + trace.trace_integration(tracer=TmpTracer()) + + self.assertIsInstance(execution_context.get_opencensus_tracer(), TmpTracer) + mock_wrap.assert_called_once_with( + module="aiohttp", + name="ClientSession._request", + wrapper=trace.wrap_session_request, + ) + + async def test_wrap_session_request(self): + mock_tracer = MockTracer() + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + status = HTTPStatus.OK + kwargs = {} + + wrapped = mock.AsyncMock(return_value=mock.Mock(status=status)) + + with patch_tracer, patch_thread: + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + expected_attributes = { + "component": "HTTP", + "http.host": http_host, + "http.method": http_method, + "http.path": http_path, + "http.status_code": status, + "http.url": http_url, + } + expected_name = http_path + expected_status = status_module.Status(0) + + self.assertEqual(SpanKind.CLIENT, mock_tracer._span.span_kind) + self.assertEqual(expected_attributes, mock_tracer._span.attributes) + self.assertEqual(expected_name, mock_tracer._span.name) + self.assertEqual(expected_status.__dict__, mock_tracer._span.status.__dict__) + + async def test_wrap_session_request_excludelist_ok(self): + mock_tracer = MockTracer() + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_attr = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_attr", + return_value=None, + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + status = HTTPStatus.OK + kwargs = {} + + wrapped = mock.AsyncMock(return_value=mock.Mock(status=status)) + + with patch_tracer, patch_attr, patch_thread: + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + expected_attributes = { + "component": "HTTP", + "http.host": http_host, + "http.method": http_method, + "http.path": http_path, + "http.status_code": status, + "http.url": http_url, + } + expected_name = http_path + expected_status = status_module.Status(0) + + self.assertEqual(SpanKind.CLIENT, mock_tracer._span.span_kind) + self.assertEqual(expected_attributes, mock_tracer._span.attributes) + self.assertEqual(expected_name, mock_tracer._span.name) + self.assertEqual(expected_status.__dict__, mock_tracer._span.status.__dict__) + + async def test_wrap_session_request_excludelist_nok(self): + async def wrapped(*args, **kwargs): + result = mock.Mock() + result.status = HTTPStatus.OK + return result + + mock_tracer = MockTracer() + + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_attr = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_attr", + return_value=["localhost:8080"], + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + kwargs = {} + + with patch_tracer, patch_attr, patch_thread: + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + self.assertEqual(None, mock_tracer._span) + + async def test_wrap_session_request_exporter_thread(self): + async def wrapped(*args, **kwargs): + result = mock.Mock() + result.status = HTTPStatus.OK + return result + + mock_tracer = MockTracer() + + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_attr = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_attr", + return_value=["localhost:8080"], + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=True, + ) + + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + kwargs = {} + + with patch_tracer, patch_attr, patch_thread: + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + self.assertEqual(None, mock_tracer._span) + + async def test_header_is_passed_in(self): + wrapped = mock.AsyncMock(return_value=mock.Mock(status=HTTPStatus.OK)) + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda span_context: {"x-trace": "some-value"} + ) + ) + + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + kwargs = {} + + with patch_tracer, patch_thread: + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + self.assertEqual(kwargs["headers"]["x-trace"], "some-value") + + async def test_headers_are_preserved(self): + wrapped = mock.AsyncMock(return_value=mock.Mock(status=HTTPStatus.OK)) + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda span_context: {"x-trace": "some-value"} + ) + ) + + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + kwargs = {"headers": {"key": "value"}} + + with patch_tracer, patch_thread: + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + self.assertEqual(kwargs["headers"]["key"], "value") + self.assertEqual(kwargs["headers"]["x-trace"], "some-value") + + async def test_tracer_headers_are_overwritten(self): + wrapped = mock.AsyncMock(return_value=mock.Mock(status=HTTPStatus.OK)) + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda span_context: {"x-trace": "some-value"} + ) + ) + + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + kwargs = {"headers": {"x-trace": "original-value"}} + + with patch_tracer, patch_thread: + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + self.assertEqual(kwargs["headers"]["x-trace"], "some-value") + + async def test_wrap_session_request_timeout(self): + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + status = HTTPStatus.OK + kwargs = {} + + wrapped = mock.AsyncMock(return_value=mock.Mock(status=status)) + wrapped.side_effect = asyncio.TimeoutError() + + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda span_context: {"x-trace": "some-value"} + ) + ) + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + with patch_tracer, patch_thread: + with self.assertRaises(asyncio.TimeoutError): + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + expected_attributes = { + "component": "HTTP", + "http.host": http_host, + "http.method": http_method, + "http.path": http_path, + "http.url": http_url, + } + expected_name = http_path + expected_status = status_module.Status(4, "request timed out") + + self.assertEqual(SpanKind.CLIENT, mock_tracer._span.span_kind) + self.assertEqual(expected_attributes, mock_tracer._span.attributes) + self.assertEqual(kwargs["headers"]["x-trace"], "some-value") + self.assertEqual(expected_name, mock_tracer._span.name) + self.assertEqual(expected_status.__dict__, mock_tracer._span.status.__dict__) + + async def test_wrap_session_request_invalid_url(self): + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + status = HTTPStatus.OK + kwargs = {} + + wrapped = mock.AsyncMock(return_value=mock.Mock(status=status)) + wrapped.side_effect = InvalidURL(url=http_url) + + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda span_context: {"x-trace": "some-value"} + ) + ) + + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + with patch_tracer, patch_thread: + with self.assertRaises(InvalidURL): + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + expected_attributes = { + "component": "HTTP", + "http.host": http_host, + "http.method": http_method, + "http.path": http_path, + "http.url": http_url, + } + expected_name = http_path + expected_status = status_module.Status(3, "invalid URL") + + self.assertEqual(SpanKind.CLIENT, mock_tracer._span.span_kind) + self.assertEqual(expected_attributes, mock_tracer._span.attributes) + self.assertEqual(kwargs["headers"]["x-trace"], "some-value") + self.assertEqual(expected_name, mock_tracer._span.name) + self.assertEqual(expected_status.__dict__, mock_tracer._span.status.__dict__) + + async def test_wrap_session_request_exception(self): + http_host = "localhost:8080" + http_path = "/test" + http_url = f"http://{http_host}{http_path}" + http_method = "POST" + status = HTTPStatus.OK + kwargs = {} + + wrapped = mock.AsyncMock(return_value=mock.Mock(status=status)) + wrapped.side_effect = ClientPayloadError() + + mock_tracer = MockTracer( + propagator=mock.Mock( + to_headers=lambda span_context: {"x-trace": "some-value"} + ) + ) + patch_tracer = mock.patch( + "opencensus.ext.requests.trace.execution_context." "get_opencensus_tracer", + return_value=mock_tracer, + ) + patch_thread = mock.patch( + "opencensus.ext.requests.trace.execution_context." "is_exporter", + return_value=False, + ) + + with patch_tracer, patch_thread: + with self.assertRaises(ClientPayloadError): + await trace.wrap_session_request( + wrapped, "ClientSession._request", (http_method, http_url), kwargs + ) + + expected_attributes = { + "component": "HTTP", + "http.host": http_host, + "http.method": http_method, + "http.path": http_path, + "http.url": http_url, + } + expected_name = http_path + expected_status = status_module.Status(2, "") + + self.assertEqual(SpanKind.CLIENT, mock_tracer._span.span_kind) + self.assertEqual(expected_attributes, mock_tracer._span.attributes) + self.assertEqual(kwargs["headers"]["x-trace"], "some-value") + self.assertEqual(expected_name, mock_tracer._span.name) + self.assertEqual(expected_status.__dict__, mock_tracer._span.status.__dict__) + + +class MockSpan(object): + def __init__(self, name: str): + self.name = name + self.attributes = {} + self.span_kind = None + self.status = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def set_status(self, status): + self.status = status + + def add_attribute(self, attribute_key, attribute_value) -> None: + self.attributes[attribute_key] = attribute_value + + +class MockTracer(object): + def __init__(self, propagator=None): + self._span = None + self.span_context = {} + self.propagator = propagator + + def span(self, name): + if self._span is None: + self._span = MockSpan(name=name) + return self._span diff --git a/contrib/opencensus-ext-aiohttp/version.py b/contrib/opencensus-ext-aiohttp/version.py new file mode 100644 index 000000000..ed46ecc52 --- /dev/null +++ b/contrib/opencensus-ext-aiohttp/version.py @@ -0,0 +1,15 @@ +# Copyright 2019, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.7.dev0"