From 1c970e614f47cb9dacb801cd83acb0b62fba9223 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 3 Aug 2020 10:10:45 -0700 Subject: [PATCH 1/9] Rename web framework packages from "ext" to "instrumentation" (#961) --- .../CHANGELOG.md | 12 + .../LICENSE | 201 ++++++++++++ .../MANIFEST.in | 9 + .../README.rst | 24 ++ .../setup.cfg | 50 +++ .../setup.py | 31 ++ .../aiohttp_client/__init__.py | 205 ++++++++++++ .../instrumentation/aiohttp_client/version.py | 15 + .../tests/__init__.py | 0 .../tests/test_aiohttp_client_integration.py | 304 ++++++++++++++++++ 10 files changed, 851 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md new file mode 100644 index 0000000000..d7dce5f65c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +- Change package name to opentelemetry-instrumentation-aiohttp-client + ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) + +## 0.7b1 + +Released 2020-05-12 + +- Initial release diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE b/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst b/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst new file mode 100644 index 0000000000..bc44e0e262 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry aiohttp client Integration +======================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aiohttp-client.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client/ + +This library allows tracing HTTP requests made by the +`aiohttp client `_ library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-aiohttp-client + + +References +---------- + +* `OpenTelemetry Project `_ +* `aiohttp client Tracing `_ diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg new file mode 100644 index 0000000000..318721ba64 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -0,0 +1,50 @@ +# Copyright 2020, OpenTelemetry 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. +# +[metadata] +name = opentelemetry-instrumentation-aiohttp-client +description = OpenTelemetry aiohttp client instrumentation +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/opentelemetry-instrumentation-aiohttp-client +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.5.3 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api >= 0.12.dev0 + opentelemetry-instrumentation == 0.12.dev0 + aiohttp ~= 3.0 + +[options.packages.find] +where = src + +[options.extras_require] +test = diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py new file mode 100644 index 0000000000..fe74e23235 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.py @@ -0,0 +1,31 @@ +# Copyright 2020, OpenTelemetry 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 os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "aiohttp_client", + "version.py", +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"]) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py new file mode 100644 index 0000000000..2d9b8bd7a5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -0,0 +1,205 @@ +# Copyright 2020, OpenTelemetry 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. + +""" +The opentelemetry-instrumentation-aiohttp-client package allows tracing HTTP +requests made by the aiohttp client library. + +Usage +----- + + .. code:: python + + import aiohttp + from opentelemetry.instrumentation.aiohttp_client import ( + create_trace_config, + url_path_span_name + ) + import yarl + + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) + + async with aiohttp.ClientSession(trace_configs=[create_trace_config( + # Remove all query params from the URL attribute on the span. + url_filter=strip_query_params, + # Use the URL's path as the span name. + span_name=url_path_span_name + )]) as session: + async with session.get(url) as response: + await response.text() + +""" + +import contextlib +import socket +import types +import typing + +import aiohttp + +from opentelemetry import context as context_api +from opentelemetry import propagators, trace +from opentelemetry.instrumentation.aiohttp_client.version import __version__ +from opentelemetry.instrumentation.utils import http_status_to_canonical_code +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCanonicalCode + + +def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: + """Extract a span name from the request URL path. + + A simple callable to extract the path portion of the requested URL + for use as the span name. + + :param aiohttp.TraceRequestStartParams params: Parameters describing + the traced request. + + :return: The URL path. + :rtype: str + """ + return params.url.path + + +def create_trace_config( + url_filter: typing.Optional[typing.Callable[[str], str]] = None, + span_name: typing.Optional[ + typing.Union[ + typing.Callable[[aiohttp.TraceRequestStartParams], str], str + ] + ] = None, +) -> aiohttp.TraceConfig: + """Create an aiohttp-compatible trace configuration. + + One span is created for the entire HTTP request, including initial + TCP/TLS setup if the connection doesn't exist. + + By default the span name is set to the HTTP request method. + + Example usage: + + .. code:: python + + import aiohttp + from opentelemetry.instrumentation.aiohttp_client import create_trace_config + + async with aiohttp.ClientSession(trace_configs=[create_trace_config()]) as session: + async with session.get(url) as response: + await response.text() + + + :param url_filter: A callback to process the requested URL prior to adding + it as a span attribute. This can be useful to remove sensitive data + such as API keys or user personal information. + + :param str span_name: Override the default span name. + + :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. + :rtype: :py:class:`aiohttp.TraceConfig` + """ + # `aiohttp.TraceRequestStartParams` resolves to `aiohttp.tracing.TraceRequestStartParams` + # which doesn't exist in the aiottp intersphinx inventory. + # Explicitly specify the type for the `span_name` param and rtype to work + # around this issue. + + tracer = trace.get_tracer_provider().get_tracer(__name__, __version__) + + def _end_trace(trace_config_ctx: types.SimpleNamespace): + context_api.detach(trace_config_ctx.token) + trace_config_ctx.span.end() + + async def on_request_start( + unused_session: aiohttp.ClientSession, + trace_config_ctx: types.SimpleNamespace, + params: aiohttp.TraceRequestStartParams, + ): + http_method = params.method.upper() + if trace_config_ctx.span_name is None: + request_span_name = http_method + elif callable(trace_config_ctx.span_name): + request_span_name = str(trace_config_ctx.span_name(params)) + else: + request_span_name = str(trace_config_ctx.span_name) + + trace_config_ctx.span = trace_config_ctx.tracer.start_span( + request_span_name, + kind=SpanKind.CLIENT, + attributes={ + "component": "http", + "http.method": http_method, + "http.url": trace_config_ctx.url_filter(params.url) + if callable(trace_config_ctx.url_filter) + else str(params.url), + }, + ) + + trace_config_ctx.token = context_api.attach( + trace.set_span_in_context(trace_config_ctx.span) + ) + + propagators.inject(type(params.headers).__setitem__, params.headers) + + async def on_request_end( + unused_session: aiohttp.ClientSession, + trace_config_ctx: types.SimpleNamespace, + params: aiohttp.TraceRequestEndParams, + ): + trace_config_ctx.span.set_status( + Status(http_status_to_canonical_code(int(params.response.status))) + ) + trace_config_ctx.span.set_attribute( + "http.status_code", params.response.status + ) + trace_config_ctx.span.set_attribute( + "http.status_text", params.response.reason + ) + _end_trace(trace_config_ctx) + + async def on_request_exception( + unused_session: aiohttp.ClientSession, + trace_config_ctx: types.SimpleNamespace, + params: aiohttp.TraceRequestExceptionParams, + ): + if isinstance( + params.exception, + (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), + ): + status = StatusCanonicalCode.DEADLINE_EXCEEDED + # Assume any getaddrinfo error is a DNS failure. + elif isinstance( + params.exception, aiohttp.ClientConnectorError + ) and isinstance(params.exception.os_error, socket.gaierror): + # DNS resolution failed + status = StatusCanonicalCode.UNKNOWN + else: + status = StatusCanonicalCode.UNAVAILABLE + + trace_config_ctx.span.set_status(Status(status)) + _end_trace(trace_config_ctx) + + def _trace_config_ctx_factory(**kwargs): + kwargs.setdefault("trace_request_ctx", {}) + return types.SimpleNamespace( + span_name=span_name, tracer=tracer, url_filter=url_filter, **kwargs + ) + + trace_config = aiohttp.TraceConfig( + trace_config_ctx_factory=_trace_config_ctx_factory + ) + + trace_config.on_request_start.append(on_request_start) + trace_config.on_request_end.append(on_request_end) + trace_config.on_request_exception.append(on_request_exception) + + return trace_config diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py new file mode 100644 index 0000000000..8d947df443 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -0,0 +1,15 @@ +# Copyright 2020, OpenTelemetry 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.12.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py new file mode 100644 index 0000000000..f44e3df2da --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -0,0 +1,304 @@ +# Copyright 2020, OpenTelemetry 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 contextlib +import typing +import urllib.parse +from http import HTTPStatus + +import aiohttp +import aiohttp.test_utils +import yarl + +import opentelemetry.instrumentation.aiohttp_client +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace.status import StatusCanonicalCode + + +class TestAioHttpIntegration(TestBase): + maxDiff = None + + def assert_spans(self, spans): + self.assertEqual( + [ + ( + span.name, + (span.status.canonical_code, span.status.description), + dict(span.attributes), + ) + for span in self.memory_exporter.get_finished_spans() + ], + spans, + ) + + def test_url_path_span_name(self): + for url, expected in ( + ( + yarl.URL("http://hostname.local:1234/some/path?query=params"), + "/some/path", + ), + (yarl.URL("http://hostname.local:1234"), "/"), + ): + with self.subTest(url=url): + params = aiohttp.TraceRequestStartParams("METHOD", url, {}) + actual = opentelemetry.instrumentation.aiohttp_client.url_path_span_name( + params + ) + self.assertEqual(actual, expected) + self.assertIsInstance(actual, str) + + @staticmethod + def _http_request( + trace_config, + url: str, + method: str = "GET", + status_code: int = HTTPStatus.OK, + request_handler: typing.Callable = None, + **kwargs + ) -> typing.Tuple[str, int]: + """Helper to start an aiohttp test server and send an actual HTTP request to it.""" + + async def do_request(): + async def default_handler(request): + assert "traceparent" in request.headers + return aiohttp.web.Response(status=int(status_code)) + + handler = request_handler or default_handler + + app = aiohttp.web.Application() + parsed_url = urllib.parse.urlparse(url) + app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) + + with contextlib.suppress(aiohttp.ClientError): + async with aiohttp.test_utils.TestServer(app) as server: + netloc = (server.host, server.port) + async with aiohttp.test_utils.TestClient( + server, trace_configs=[trace_config] + ) as client: + await client.start_server() + await client.request( + method, url, trace_request_ctx={}, **kwargs + ) + return netloc + + loop = asyncio.get_event_loop() + return loop.run_until_complete(do_request()) + + def test_status_codes(self): + for status_code, span_status in ( + (HTTPStatus.OK, StatusCanonicalCode.OK), + (HTTPStatus.TEMPORARY_REDIRECT, StatusCanonicalCode.OK), + (HTTPStatus.SERVICE_UNAVAILABLE, StatusCanonicalCode.UNAVAILABLE), + ( + HTTPStatus.GATEWAY_TIMEOUT, + StatusCanonicalCode.DEADLINE_EXCEEDED, + ), + ): + with self.subTest(status_code=status_code): + host, port = self._http_request( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + url="/test-path?query=param#foobar", + status_code=status_code, + ) + + self.assert_spans( + [ + ( + "GET", + (span_status, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/test-path?query=param#foobar".format( + host, port + ), + "http.status_code": int(status_code), + "http.status_text": status_code.phrase, + }, + ) + ] + ) + + self.memory_exporter.clear() + + def test_span_name_option(self): + for span_name, method, path, expected in ( + ("static", "POST", "/static-span-name", "static"), + ( + lambda params: "{} - {}".format( + params.method, params.url.path + ), + "PATCH", + "/some/path", + "PATCH - /some/path", + ), + ): + with self.subTest(span_name=span_name, method=method, path=path): + host, port = self._http_request( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( + span_name=span_name + ), + method=method, + url=path, + status_code=HTTPStatus.OK, + ) + + self.assert_spans( + [ + ( + expected, + (StatusCanonicalCode.OK, None), + { + "component": "http", + "http.method": method, + "http.url": "http://{}:{}{}".format( + host, port, path + ), + "http.status_code": int(HTTPStatus.OK), + "http.status_text": HTTPStatus.OK.phrase, + }, + ) + ] + ) + self.memory_exporter.clear() + + def test_url_filter_option(self): + # Strips all query params from URL before adding as a span attribute. + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) + + host, port = self._http_request( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( + url_filter=strip_query_params + ), + url="/some/path?query=param&other=param2", + status_code=HTTPStatus.OK, + ) + + self.assert_spans( + [ + ( + "GET", + (StatusCanonicalCode.OK, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/some/path".format( + host, port + ), + "http.status_code": int(HTTPStatus.OK), + "http.status_text": HTTPStatus.OK.phrase, + }, + ) + ] + ) + + def test_connection_errors(self): + trace_configs = [ + opentelemetry.instrumentation.aiohttp_client.create_trace_config() + ] + + for url, expected_status in ( + ("http://this-is-unknown.local/", StatusCanonicalCode.UNKNOWN), + ("http://127.0.0.1:1/", StatusCanonicalCode.UNAVAILABLE), + ): + with self.subTest(expected_status=expected_status): + + async def do_request(url): + async with aiohttp.ClientSession( + trace_configs=trace_configs + ) as session: + async with session.get(url): + pass + + loop = asyncio.get_event_loop() + with self.assertRaises(aiohttp.ClientConnectorError): + loop.run_until_complete(do_request(url)) + + self.assert_spans( + [ + ( + "GET", + (expected_status, None), + { + "component": "http", + "http.method": "GET", + "http.url": url, + }, + ) + ] + ) + self.memory_exporter.clear() + + def test_timeout(self): + async def request_handler(request): + await asyncio.sleep(1) + assert "traceparent" in request.headers + return aiohttp.web.Response() + + host, port = self._http_request( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + url="/test_timeout", + request_handler=request_handler, + timeout=aiohttp.ClientTimeout(sock_read=0.01), + ) + + self.assert_spans( + [ + ( + "GET", + (StatusCanonicalCode.DEADLINE_EXCEEDED, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/test_timeout".format( + host, port + ), + }, + ) + ] + ) + + def test_too_many_redirects(self): + async def request_handler(request): + # Create a redirect loop. + location = request.url + assert "traceparent" in request.headers + raise aiohttp.web.HTTPFound(location=location) + + host, port = self._http_request( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + url="/test_too_many_redirects", + request_handler=request_handler, + max_redirects=2, + ) + + self.assert_spans( + [ + ( + "GET", + (StatusCanonicalCode.DEADLINE_EXCEEDED, None), + { + "component": "http", + "http.method": "GET", + "http.url": "http://{}:{}/test_too_many_redirects".format( + host, port + ), + }, + ) + ] + ) From a43d088c3f93da24e4d2671a6d266c7e61178f46 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Sat, 15 Aug 2020 18:06:27 -0700 Subject: [PATCH 2/9] chore: 0.13.dev0 version update (#991) --- .../opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-aiohttp-client/setup.cfg | 2 +- .../opentelemetry/instrumentation/aiohttp_client/version.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index d7dce5f65c..cdbc621d31 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.12b0 + +Released 2020-08-14 + - Change package name to opentelemetry-instrumentation-aiohttp-client ([#961](https://github.com/open-telemetry/opentelemetry-python/pull/961)) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 318721ba64..557b8d9a08 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.12.dev0 - opentelemetry-instrumentation == 0.12.dev0 + opentelemetry-instrumentation == 0.13dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index 8d947df443..1a40cfa0f1 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.12.dev0" +__version__ = "0.13dev0" From c9a0fe761b86de13acd1b92085f778d16118b682 Mon Sep 17 00:00:00 2001 From: alrex Date: Mon, 17 Aug 2020 23:50:09 -0500 Subject: [PATCH 3/9] Span name updated to follow semantic conventions to reduce cardinality (#972) --- .../CHANGELOG.md | 3 +++ .../instrumentation/aiohttp_client/__init__.py | 2 +- .../tests/test_aiohttp_client_integration.py | 10 +++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index cdbc621d31..78b989563f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Updating span name to match semantic conventions + ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) + ## Version 0.12b0 Released 2020-08-14 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 2d9b8bd7a5..397d5dc80e 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -126,7 +126,7 @@ async def on_request_start( ): http_method = params.method.upper() if trace_config_ctx.span_name is None: - request_span_name = http_method + request_span_name = "HTTP {}".format(http_method) elif callable(trace_config_ctx.span_name): request_span_name = str(trace_config_ctx.span_name(params)) else: diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index f44e3df2da..4a48c38ff7 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -118,7 +118,7 @@ def test_status_codes(self): self.assert_spans( [ ( - "GET", + "HTTP GET", (span_status, None), { "component": "http", @@ -192,7 +192,7 @@ def strip_query_params(url: yarl.URL) -> str: self.assert_spans( [ ( - "GET", + "HTTP GET", (StatusCanonicalCode.OK, None), { "component": "http", @@ -232,7 +232,7 @@ async def do_request(url): self.assert_spans( [ ( - "GET", + "HTTP GET", (expected_status, None), { "component": "http", @@ -260,7 +260,7 @@ async def request_handler(request): self.assert_spans( [ ( - "GET", + "HTTP GET", (StatusCanonicalCode.DEADLINE_EXCEEDED, None), { "component": "http", @@ -290,7 +290,7 @@ async def request_handler(request): self.assert_spans( [ ( - "GET", + "HTTP GET", (StatusCanonicalCode.DEADLINE_EXCEEDED, None), { "component": "http", From ff33a269f84691a70f72afb3535287f2e38da1db Mon Sep 17 00:00:00 2001 From: Gunnlaugur Thor Briem Date: Fri, 28 Aug 2020 21:45:06 +0000 Subject: [PATCH 4/9] docs: fix outdated alpha statement (#1047) `README.md` and the opentelemetry website say this library is in beta, and releases have been called betas since March, so update `docs/index.rst` to be consistent with that. --- .../opentelemetry-instrumentation-aiohttp-client/setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 557b8d9a08..a222f323c0 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -23,7 +23,7 @@ url = https://github.com/open-telemetry/opentelemetry-python/instrumentation/ope platforms = any license = Apache-2.0 classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python From 44dc5319e65a95f9d2a7531d745df9d362a1101f Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 17 Sep 2020 08:23:52 -0700 Subject: [PATCH 5/9] release: updating changelogs and version to 0.13b0 (#1129) * updating changelogs and version to 0.13b0 --- .../opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md | 4 ++++ .../opentelemetry-instrumentation-aiohttp-client/setup.cfg | 2 +- .../opentelemetry/instrumentation/aiohttp_client/version.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index 78b989563f..286d4ca642 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Version 0.13b0 + +Released 2020-09-17 + - Updating span name to match semantic conventions ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index a222f323c0..a3e3d27065 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.12.dev0 - opentelemetry-instrumentation == 0.13dev0 + opentelemetry-instrumentation == 0.13b0 aiohttp ~= 3.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index 1a40cfa0f1..cbee121c0b 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13dev0" +__version__ = "0.13b0" From ee1805d9846e1138c84a6419912d3176b3fa887b Mon Sep 17 00:00:00 2001 From: alrex Date: Thu, 17 Sep 2020 12:21:39 -0700 Subject: [PATCH 6/9] chore: bump dev version (#1131) --- .../opentelemetry-instrumentation-aiohttp-client/setup.cfg | 2 +- .../src/opentelemetry/instrumentation/aiohttp_client/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index a3e3d27065..196b33087d 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -40,7 +40,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api >= 0.12.dev0 - opentelemetry-instrumentation == 0.13b0 + opentelemetry-instrumentation == 0.14.dev0 aiohttp ~= 3.0 [options.packages.find] diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index cbee121c0b..404790dad7 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.13b0" +__version__ = "0.14.dev0" From 2b3713655c458ca9f9c42bebf7eb1738d4595b4f Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 22 Sep 2020 10:30:39 -0700 Subject: [PATCH 7/9] Use is_recording flag in asgi, pyramid, aiohttp instrumentation (#1142) --- .../aiohttp_client/__init__.py | 65 ++++++++++--------- .../tests/test_aiohttp_client_integration.py | 17 +++++ 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 397d5dc80e..5c48bbd58a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -133,16 +133,19 @@ async def on_request_start( request_span_name = str(trace_config_ctx.span_name) trace_config_ctx.span = trace_config_ctx.tracer.start_span( - request_span_name, - kind=SpanKind.CLIENT, - attributes={ + request_span_name, kind=SpanKind.CLIENT, + ) + + if trace_config_ctx.span.is_recording(): + attributes = { "component": "http", "http.method": http_method, "http.url": trace_config_ctx.url_filter(params.url) if callable(trace_config_ctx.url_filter) else str(params.url), - }, - ) + } + for key, value in attributes.items(): + trace_config_ctx.span.set_attribute(key, value) trace_config_ctx.token = context_api.attach( trace.set_span_in_context(trace_config_ctx.span) @@ -155,15 +158,18 @@ async def on_request_end( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestEndParams, ): - trace_config_ctx.span.set_status( - Status(http_status_to_canonical_code(int(params.response.status))) - ) - trace_config_ctx.span.set_attribute( - "http.status_code", params.response.status - ) - trace_config_ctx.span.set_attribute( - "http.status_text", params.response.reason - ) + if trace_config_ctx.span.is_recording(): + trace_config_ctx.span.set_status( + Status( + http_status_to_canonical_code(int(params.response.status)) + ) + ) + trace_config_ctx.span.set_attribute( + "http.status_code", params.response.status + ) + trace_config_ctx.span.set_attribute( + "http.status_text", params.response.reason + ) _end_trace(trace_config_ctx) async def on_request_exception( @@ -171,21 +177,22 @@ async def on_request_exception( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestExceptionParams, ): - if isinstance( - params.exception, - (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), - ): - status = StatusCanonicalCode.DEADLINE_EXCEEDED - # Assume any getaddrinfo error is a DNS failure. - elif isinstance( - params.exception, aiohttp.ClientConnectorError - ) and isinstance(params.exception.os_error, socket.gaierror): - # DNS resolution failed - status = StatusCanonicalCode.UNKNOWN - else: - status = StatusCanonicalCode.UNAVAILABLE - - trace_config_ctx.span.set_status(Status(status)) + if trace_config_ctx.span.is_recording(): + if isinstance( + params.exception, + (aiohttp.ServerTimeoutError, aiohttp.TooManyRedirects), + ): + status = StatusCanonicalCode.DEADLINE_EXCEEDED + # Assume any getaddrinfo error is a DNS failure. + elif isinstance( + params.exception, aiohttp.ClientConnectorError + ) and isinstance(params.exception.os_error, socket.gaierror): + # DNS resolution failed + status = StatusCanonicalCode.UNKNOWN + else: + status = StatusCanonicalCode.UNAVAILABLE + + trace_config_ctx.span.set_status(Status(status)) _end_trace(trace_config_ctx) def _trace_config_ctx_factory(**kwargs): diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 4a48c38ff7..90af17f9e0 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -17,6 +17,7 @@ import typing import urllib.parse from http import HTTPStatus +from unittest import mock import aiohttp import aiohttp.test_utils @@ -135,6 +136,22 @@ def test_status_codes(self): self.memory_exporter.clear() + def test_not_recording(self): + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + with mock.patch("opentelemetry.trace.get_tracer"): + # pylint: disable=W0612 + host, port = self._http_request( + trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + url="/test-path?query=param#foobar", + ) + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + def test_span_name_option(self): for span_name, method, path, expected in ( ("static", "POST", "/static-span-name", "static"), From a7144ba72207f8c817546ccff74d9ad598d3acc4 Mon Sep 17 00:00:00 2001 From: Mario Jonke Date: Fri, 9 Oct 2020 16:18:22 +0200 Subject: [PATCH 8/9] Add instrumentor and auto instrumentation support for aiohttp (#1075) --- .../CHANGELOG.md | 2 + .../setup.cfg | 7 +- .../aiohttp_client/__init__.py | 173 ++++++++++-- .../tests/test_aiohttp_client_integration.py | 266 +++++++++++++++--- 4 files changed, 378 insertions(+), 70 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md index 286d4ca642..8b1d3ee2c1 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/CHANGELOG.md @@ -8,6 +8,8 @@ Released 2020-09-17 - Updating span name to match semantic conventions ([#972](https://github.com/open-telemetry/opentelemetry-python/pull/972)) +- Add instrumentor and auto instrumentation support for aiohttp + ([#1075](https://github.com/open-telemetry/opentelemetry-python/pull/1075)) ## Version 0.12b0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index 196b33087d..eae097a62a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -39,12 +39,17 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.12.dev0 + opentelemetry-api == 0.14.dev0 opentelemetry-instrumentation == 0.14.dev0 aiohttp ~= 3.0 + wrapt >= 1.0.0, < 2.0.0 [options.packages.find] where = src [options.extras_require] test = + +[options.entry_points] +opentelemetry_instrumentor = + aiohttp-client = opentelemetry.instrumentation.aiohttp_client:AioHttpClientInstrumentor \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py index 5c48bbd58a..6606c48331 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py @@ -18,44 +18,73 @@ Usage ----- +Explicitly instrumenting a single client session: - .. code:: python +.. code:: python - import aiohttp - from opentelemetry.instrumentation.aiohttp_client import ( - create_trace_config, - url_path_span_name - ) - import yarl + import aiohttp + from opentelemetry.instrumentation.aiohttp_client import ( + create_trace_config, + url_path_span_name + ) + import yarl - def strip_query_params(url: yarl.URL) -> str: - return str(url.with_query(None)) + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) - async with aiohttp.ClientSession(trace_configs=[create_trace_config( - # Remove all query params from the URL attribute on the span. - url_filter=strip_query_params, - # Use the URL's path as the span name. - span_name=url_path_span_name - )]) as session: - async with session.get(url) as response: - await response.text() + async with aiohttp.ClientSession(trace_configs=[create_trace_config( + # Remove all query params from the URL attribute on the span. + url_filter=strip_query_params, + # Use the URL's path as the span name. + span_name=url_path_span_name + )]) as session: + async with session.get(url) as response: + await response.text() + +Instrumenting all client sessions: + +.. code:: python + + import aiohttp + from opentelemetry.instrumentation.aiohttp_client import ( + AioHttpClientInstrumentor + ) + # Enable instrumentation + AioHttpClientInstrumentor().instrument() + + # Create a session and make an HTTP get request + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + await response.text() + +API +--- """ -import contextlib import socket import types import typing import aiohttp +import wrapt from opentelemetry import context as context_api from opentelemetry import propagators, trace from opentelemetry.instrumentation.aiohttp_client.version import __version__ -from opentelemetry.instrumentation.utils import http_status_to_canonical_code -from opentelemetry.trace import SpanKind +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import ( + http_status_to_canonical_code, + unwrap, +) +from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.trace.status import Status, StatusCanonicalCode +_UrlFilterT = typing.Optional[typing.Callable[[str], str]] +_SpanNameT = typing.Optional[ + typing.Union[typing.Callable[[aiohttp.TraceRequestStartParams], str], str] +] + def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: """Extract a span name from the request URL path. @@ -73,12 +102,9 @@ def url_path_span_name(params: aiohttp.TraceRequestStartParams) -> str: def create_trace_config( - url_filter: typing.Optional[typing.Callable[[str], str]] = None, - span_name: typing.Optional[ - typing.Union[ - typing.Callable[[aiohttp.TraceRequestStartParams], str], str - ] - ] = None, + url_filter: _UrlFilterT = None, + span_name: _SpanNameT = None, + tracer_provider: TracerProvider = None, ) -> aiohttp.TraceConfig: """Create an aiohttp-compatible trace configuration. @@ -104,6 +130,7 @@ def create_trace_config( such as API keys or user personal information. :param str span_name: Override the default span name. + :param tracer_provider: optional TracerProvider from which to get a Tracer :return: An object suitable for use with :py:class:`aiohttp.ClientSession`. :rtype: :py:class:`aiohttp.TraceConfig` @@ -113,7 +140,7 @@ def create_trace_config( # Explicitly specify the type for the `span_name` param and rtype to work # around this issue. - tracer = trace.get_tracer_provider().get_tracer(__name__, __version__) + tracer = get_tracer(__name__, __version__, tracer_provider) def _end_trace(trace_config_ctx: types.SimpleNamespace): context_api.detach(trace_config_ctx.token) @@ -124,6 +151,10 @@ async def on_request_start( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestStartParams, ): + if context_api.get_value("suppress_instrumentation"): + trace_config_ctx.span = None + return + http_method = params.method.upper() if trace_config_ctx.span_name is None: request_span_name = "HTTP {}".format(http_method) @@ -158,6 +189,9 @@ async def on_request_end( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestEndParams, ): + if trace_config_ctx.span is None: + return + if trace_config_ctx.span.is_recording(): trace_config_ctx.span.set_status( Status( @@ -177,6 +211,9 @@ async def on_request_exception( trace_config_ctx: types.SimpleNamespace, params: aiohttp.TraceRequestExceptionParams, ): + if trace_config_ctx.span is None: + return + if trace_config_ctx.span.is_recording(): if isinstance( params.exception, @@ -193,6 +230,7 @@ async def on_request_exception( status = StatusCanonicalCode.UNAVAILABLE trace_config_ctx.span.set_status(Status(status)) + trace_config_ctx.span.record_exception(params.exception) _end_trace(trace_config_ctx) def _trace_config_ctx_factory(**kwargs): @@ -210,3 +248,84 @@ def _trace_config_ctx_factory(**kwargs): trace_config.on_request_exception.append(on_request_exception) return trace_config + + +def _instrument( + tracer_provider: TracerProvider = None, + url_filter: _UrlFilterT = None, + span_name: _SpanNameT = None, +): + """Enables tracing of all ClientSessions + + When a ClientSession gets created a TraceConfig is automatically added to + the session's trace_configs. + """ + # pylint:disable=unused-argument + def instrumented_init(wrapped, instance, args, kwargs): + if context_api.get_value("suppress_instrumentation"): + return wrapped(*args, **kwargs) + + trace_configs = list(kwargs.get("trace_configs") or ()) + + trace_config = create_trace_config( + url_filter=url_filter, + span_name=span_name, + tracer_provider=tracer_provider, + ) + trace_config.opentelemetry_aiohttp_instrumented = True + trace_configs.append(trace_config) + + kwargs["trace_configs"] = trace_configs + return wrapped(*args, **kwargs) + + wrapt.wrap_function_wrapper( + aiohttp.ClientSession, "__init__", instrumented_init + ) + + +def _uninstrument(): + """Disables instrumenting for all newly created ClientSessions""" + unwrap(aiohttp.ClientSession, "__init__") + + +def _uninstrument_session(client_session: aiohttp.ClientSession): + """Disables instrumentation for the given ClientSession""" + # pylint: disable=protected-access + trace_configs = client_session._trace_configs + client_session._trace_configs = [ + trace_config + for trace_config in trace_configs + if not hasattr(trace_config, "opentelemetry_aiohttp_instrumented") + ] + + +class AioHttpClientInstrumentor(BaseInstrumentor): + """An instrumentor for aiohttp client sessions + + See `BaseInstrumentor` + """ + + def _instrument(self, **kwargs): + """Instruments aiohttp ClientSession + + Args: + **kwargs: Optional arguments + ``tracer_provider``: a TracerProvider, defaults to global + ``url_filter``: A callback to process the requested URL prior to adding + it as a span attribute. This can be useful to remove sensitive data + such as API keys or user personal information. + ``span_name``: Override the default span name. + """ + _instrument( + tracer_provider=kwargs.get("tracer_provider"), + url_filter=kwargs.get("url_filter"), + span_name=kwargs.get("span_name"), + ) + + def _uninstrument(self, **kwargs): + _uninstrument() + + @staticmethod + def uninstrument_session(client_session: aiohttp.ClientSession): + """Disables instrumentation for the given session""" + _uninstrument_session(client_session) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 90af17f9e0..fb5b6aac6a 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -15,6 +15,7 @@ import asyncio import contextlib import typing +import unittest import urllib.parse from http import HTTPStatus from unittest import mock @@ -22,15 +23,39 @@ import aiohttp import aiohttp.test_utils import yarl +from pkg_resources import iter_entry_points -import opentelemetry.instrumentation.aiohttp_client +from opentelemetry import context +from opentelemetry.instrumentation import aiohttp_client +from opentelemetry.instrumentation.aiohttp_client import ( + AioHttpClientInstrumentor, +) from opentelemetry.test.test_base import TestBase from opentelemetry.trace.status import StatusCanonicalCode -class TestAioHttpIntegration(TestBase): - maxDiff = None +def run_with_test_server( + runnable: typing.Callable, url: str, handler: typing.Callable +) -> typing.Tuple[str, int]: + async def do_request(): + app = aiohttp.web.Application() + parsed_url = urllib.parse.urlparse(url) + app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) + app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) + + with contextlib.suppress(aiohttp.ClientError): + async with aiohttp.test_utils.TestServer(app) as server: + netloc = (server.host, server.port) + await server.start_server() + await runnable(server) + return netloc + + loop = asyncio.get_event_loop() + return loop.run_until_complete(do_request()) + +class TestAioHttpIntegration(TestBase): def assert_spans(self, spans): self.assertEqual( [ @@ -54,9 +79,7 @@ def test_url_path_span_name(self): ): with self.subTest(url=url): params = aiohttp.TraceRequestStartParams("METHOD", url, {}) - actual = opentelemetry.instrumentation.aiohttp_client.url_path_span_name( - params - ) + actual = aiohttp_client.url_path_span_name(params) self.assertEqual(actual, expected) self.assertIsInstance(actual, str) @@ -71,33 +94,20 @@ def _http_request( ) -> typing.Tuple[str, int]: """Helper to start an aiohttp test server and send an actual HTTP request to it.""" - async def do_request(): - async def default_handler(request): - assert "traceparent" in request.headers - return aiohttp.web.Response(status=int(status_code)) - - handler = request_handler or default_handler - - app = aiohttp.web.Application() - parsed_url = urllib.parse.urlparse(url) - app.add_routes([aiohttp.web.get(parsed_url.path, handler)]) - app.add_routes([aiohttp.web.post(parsed_url.path, handler)]) - app.add_routes([aiohttp.web.patch(parsed_url.path, handler)]) - - with contextlib.suppress(aiohttp.ClientError): - async with aiohttp.test_utils.TestServer(app) as server: - netloc = (server.host, server.port) - async with aiohttp.test_utils.TestClient( - server, trace_configs=[trace_config] - ) as client: - await client.start_server() - await client.request( - method, url, trace_request_ctx={}, **kwargs - ) - return netloc + async def default_handler(request): + assert "traceparent" in request.headers + return aiohttp.web.Response(status=int(status_code)) + + async def client_request(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient( + server, trace_configs=[trace_config] + ) as client: + await client.request( + method, url, trace_request_ctx={}, **kwargs + ) - loop = asyncio.get_event_loop() - return loop.run_until_complete(do_request()) + handler = request_handler or default_handler + return run_with_test_server(client_request, url, handler) def test_status_codes(self): for status_code, span_status in ( @@ -111,7 +121,7 @@ def test_status_codes(self): ): with self.subTest(status_code=status_code): host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", status_code=status_code, ) @@ -144,7 +154,7 @@ def test_not_recording(self): with mock.patch("opentelemetry.trace.get_tracer"): # pylint: disable=W0612 host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test-path?query=param#foobar", ) self.assertFalse(mock_span.is_recording()) @@ -166,7 +176,7 @@ def test_span_name_option(self): ): with self.subTest(span_name=span_name, method=method, path=path): host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( + trace_config=aiohttp_client.create_trace_config( span_name=span_name ), method=method, @@ -199,7 +209,7 @@ def strip_query_params(url: yarl.URL) -> str: return str(url.with_query(None)) host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config( + trace_config=aiohttp_client.create_trace_config( url_filter=strip_query_params ), url="/some/path?query=param&other=param2", @@ -225,9 +235,7 @@ def strip_query_params(url: yarl.URL) -> str: ) def test_connection_errors(self): - trace_configs = [ - opentelemetry.instrumentation.aiohttp_client.create_trace_config() - ] + trace_configs = [aiohttp_client.create_trace_config()] for url, expected_status in ( ("http://this-is-unknown.local/", StatusCanonicalCode.UNKNOWN), @@ -237,7 +245,7 @@ def test_connection_errors(self): async def do_request(url): async with aiohttp.ClientSession( - trace_configs=trace_configs + trace_configs=trace_configs, ) as session: async with session.get(url): pass @@ -268,7 +276,7 @@ async def request_handler(request): return aiohttp.web.Response() host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test_timeout", request_handler=request_handler, timeout=aiohttp.ClientTimeout(sock_read=0.01), @@ -298,7 +306,7 @@ async def request_handler(request): raise aiohttp.web.HTTPFound(location=location) host, port = self._http_request( - trace_config=opentelemetry.instrumentation.aiohttp_client.create_trace_config(), + trace_config=aiohttp_client.create_trace_config(), url="/test_too_many_redirects", request_handler=request_handler, max_redirects=2, @@ -319,3 +327,177 @@ async def request_handler(request): ) ] ) + + +class TestAioHttpClientInstrumentor(TestBase): + URL = "/test-path" + + def setUp(self): + super().setUp() + AioHttpClientInstrumentor().instrument() + + def tearDown(self): + super().tearDown() + AioHttpClientInstrumentor().uninstrument() + + @staticmethod + # pylint:disable=unused-argument + async def default_handler(request): + return aiohttp.web.Response(status=int(200)) + + @staticmethod + def get_default_request(url: str = URL): + async def default_request(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient(server) as session: + await session.get(url) + + return default_request + + def assert_spans(self, num_spans: int): + finished_spans = self.memory_exporter.get_finished_spans() + self.assertEqual(num_spans, len(finished_spans)) + if num_spans == 0: + return None + if num_spans == 1: + return finished_spans[0] + return finished_spans + + def test_instrument(self): + host, port = run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + span = self.assert_spans(1) + self.assertEqual("http", span.attributes["component"]) + self.assertEqual("GET", span.attributes["http.method"]) + self.assertEqual( + "http://{}:{}/test-path".format(host, port), + span.attributes["http.url"], + ) + self.assertEqual(200, span.attributes["http.status_code"]) + self.assertEqual("OK", span.attributes["http.status_text"]) + + def test_instrument_with_existing_trace_config(self): + trace_config = aiohttp.TraceConfig() + + async def create_session(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient( + server, trace_configs=[trace_config] + ) as client: + # pylint:disable=protected-access + trace_configs = client.session._trace_configs + self.assertEqual(2, len(trace_configs)) + self.assertTrue(trace_config in trace_configs) + async with client as session: + await session.get(TestAioHttpClientInstrumentor.URL) + + run_with_test_server(create_session, self.URL, self.default_handler) + self.assert_spans(1) + + def test_uninstrument(self): + AioHttpClientInstrumentor().uninstrument() + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + + self.assert_spans(0) + + AioHttpClientInstrumentor().instrument() + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + self.assert_spans(1) + + def test_uninstrument_session(self): + async def uninstrument_request(server: aiohttp.test_utils.TestServer): + client = aiohttp.test_utils.TestClient(server) + AioHttpClientInstrumentor().uninstrument_session(client.session) + async with client as session: + await session.get(self.URL) + + run_with_test_server( + uninstrument_request, self.URL, self.default_handler + ) + self.assert_spans(0) + + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + self.assert_spans(1) + + def test_suppress_instrumentation(self): + token = context.attach( + context.set_value("suppress_instrumentation", True) + ) + try: + run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + finally: + context.detach(token) + self.assert_spans(0) + + @staticmethod + async def suppressed_request(server: aiohttp.test_utils.TestServer): + async with aiohttp.test_utils.TestClient(server) as client: + token = context.attach( + context.set_value("suppress_instrumentation", True) + ) + await client.get(TestAioHttpClientInstrumentor.URL) + context.detach(token) + + def test_suppress_instrumentation_after_creation(self): + run_with_test_server( + self.suppressed_request, self.URL, self.default_handler + ) + self.assert_spans(0) + + def test_suppress_instrumentation_with_server_exception(self): + # pylint:disable=unused-argument + async def raising_handler(request): + raise aiohttp.web.HTTPFound(location=self.URL) + + run_with_test_server( + self.suppressed_request, self.URL, raising_handler + ) + self.assert_spans(0) + + def test_url_filter(self): + def strip_query_params(url: yarl.URL) -> str: + return str(url.with_query(None)) + + AioHttpClientInstrumentor().uninstrument() + AioHttpClientInstrumentor().instrument(url_filter=strip_query_params) + + url = "/test-path?query=params" + host, port = run_with_test_server( + self.get_default_request(url), url, self.default_handler + ) + span = self.assert_spans(1) + self.assertEqual( + "http://{}:{}/test-path".format(host, port), + span.attributes["http.url"], + ) + + def test_span_name(self): + def span_name_callback(params: aiohttp.TraceRequestStartParams) -> str: + return "{} - {}".format(params.method, params.url.path) + + AioHttpClientInstrumentor().uninstrument() + AioHttpClientInstrumentor().instrument(span_name=span_name_callback) + + url = "/test-path" + run_with_test_server( + self.get_default_request(url), url, self.default_handler + ) + span = self.assert_spans(1) + self.assertEqual("GET - /test-path", span.name) + + +class TestLoadingAioHttpInstrumentor(unittest.TestCase): + def test_loading_instrumentor(self): + entry_points = iter_entry_points( + "opentelemetry_instrumentor", "aiohttp-client" + ) + + instrumentor = next(entry_points).load()() + self.assertIsInstance(instrumentor, AioHttpClientInstrumentor) From 83cbce451f7ac4db23decd2d6849e85695f7feb1 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 13 Oct 2020 14:38:09 -0400 Subject: [PATCH 9/9] chore: bump dev version (#1235) --- .../opentelemetry-instrumentation-aiohttp-client/setup.cfg | 4 ++-- .../opentelemetry/instrumentation/aiohttp_client/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg index eae097a62a..27e120d660 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api == 0.14.dev0 - opentelemetry-instrumentation == 0.14.dev0 + opentelemetry-api == 0.15.dev0 + opentelemetry-instrumentation == 0.15.dev0 aiohttp ~= 3.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py index 404790dad7..0fdc34158f 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.14.dev0" +__version__ = "0.15.dev0"