From 5a14c7dae9a2908d0d064cf90bed7b3a234026ef Mon Sep 17 00:00:00 2001 From: Daniel Manchon Date: Wed, 2 Mar 2022 23:04:41 +0100 Subject: [PATCH] aiohttp server --- .../setup.cfg | 2 +- .../aiohttp_server/__init__.py | 11 +- .../tests/test_aiohttp_server_integration.py | 133 ++++++++++++++++++ .../instrumentation/bootstrap.py | 12 +- .../instrumentation/bootstrap_gen.py | 5 +- tox.ini | 8 ++ 6 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/setup.cfg b/instrumentation/opentelemetry-instrumentation-aiohttp-server/setup.cfg index 1d32ab88ac..a5def2a8cc 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/setup.cfg @@ -55,4 +55,4 @@ test = [options.entry_points] opentelemetry_instrumentor = - aiohttp-server = opentelemetry.instrumentation.aiohttp_server:AioHttpInstrumentor \ No newline at end of file + aiohttp-server = opentelemetry.instrumentation.aiohttp_server:AioHttpServerInstrumentor \ No newline at end of file diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py index 9b5f83be2f..b9cd548420 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py @@ -1,6 +1,5 @@ import urllib from aiohttp import web -from guillotina.utils import get_dotted_name from multidict import CIMultiDictProxy from opentelemetry import context, trace from opentelemetry.instrumentation.aiohttp_server.package import _instruments @@ -19,7 +18,7 @@ _SUPPRESS_HTTP_INSTRUMENTATION_KEY = "suppress_http_instrumentation" tracer = trace.get_tracer(__name__) -_excluded_urls = get_excluded_urls("FLASK") +_excluded_urls = get_excluded_urls("AIOHTTP_SERVER") def get_default_span_details(request: web.Request) -> Tuple[str, dict]: @@ -34,9 +33,9 @@ def get_default_span_details(request: web.Request) -> Tuple[str, dict]: def _get_view_func(request) -> str: - """TODO: is this only working for guillotina?""" + """TODO: is this useful??""" try: - return get_dotted_name(request.found_view) + return request.match_info.handler.__name__ except AttributeError: return "unknown" @@ -139,7 +138,7 @@ async def middleware(request, handler): if ( context.get_value("suppress_instrumentation") or context.get_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY) - or not _excluded_urls.url_disabled(request.url) + or _excluded_urls.url_disabled(request.url) ): return await handler(request) @@ -173,7 +172,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -class AioHttpInstrumentor(BaseInstrumentor): +class AioHttpServerInstrumentor(BaseInstrumentor): # pylint: disable=protected-access,attribute-defined-outside-init """An instrumentor for aiohttp.web.Application diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py new file mode 100644 index 0000000000..f029f0e954 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py @@ -0,0 +1,133 @@ +# 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 unittest +import urllib.parse +from functools import partial +from http import HTTPStatus +from unittest import mock + +import aiohttp +import aiohttp.test_utils +import yarl +from pkg_resources import iter_entry_points + +from opentelemetry import context +from opentelemetry.instrumentation import aiohttp_server +from opentelemetry.instrumentation.aiohttp_server import ( + AioHttpServerInstrumentor, +) +from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import Span, StatusCode + + +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 TestAioHttpServerIntegration(TestBase): + URL = "/test-path" + + def setUp(self): + super().setUp() + AioHttpServerInstrumentor().instrument() + + def tearDown(self): + super().tearDown() + AioHttpServerInstrumentor().uninstrument() + + @staticmethod + # pylint:disable=unused-argument + async def default_handler(request, status=200): + return aiohttp.web.Response(status=status) + + 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 + + @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 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("GET", span.attributes[SpanAttributes.HTTP_METHOD]) + self.assertEqual( + f"http://{host}:{port}/test-path", + span.attributes[SpanAttributes.HTTP_URL], + ) + self.assertEqual(200, span.attributes[SpanAttributes.HTTP_STATUS_CODE]) + + def test_status_codes(self): + error_handler = partial(self.default_handler, status=400) + host, port = run_with_test_server( + self.get_default_request(), self.URL, error_handler + ) + span = self.assert_spans(1) + self.assertEqual("GET", span.attributes[SpanAttributes.HTTP_METHOD]) + self.assertEqual( + f"http://{host}:{port}/test-path", + span.attributes[SpanAttributes.HTTP_URL], + ) + self.assertEqual(400, span.attributes[SpanAttributes.HTTP_STATUS_CODE]) + + 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 = run_with_test_server( + self.get_default_request(), self.URL, self.default_handler + ) + + 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) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 6fa36f0463..bb0044850a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -103,13 +103,11 @@ def _is_installed(req): def _find_installed_libraries(): libs = default_instrumentations[:] - libs.extend( - [ - v["instrumentation"] - for _, v in libraries.items() - if _is_installed(v["library"]) - ] - ) + + for _, v in libraries.items(): + if _is_installed(v["library"]): + libs.extend(v["instrumentation"]) + return libs diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index 05c77b9fea..674c012e84 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -22,7 +22,10 @@ }, "aiohttp": { "library": "aiohttp ~= 3.0", - "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.39b0.dev", + "instrumentation": [ + "opentelemetry-instrumentation-aiohttp-client==0.39b0.dev", + "opentelemetry-instrumentation-aiohttp-server==0.39b0.dev", + ], }, "aiopg": { "library": "aiopg >= 0.13.0, < 2.0.0", diff --git a/tox.ini b/tox.ini index 1603dfb745..b3a542ab30 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,10 @@ envlist = py3{7,8,9,10,11}-test-instrumentation-aiohttp-client pypy3-test-instrumentation-aiohttp-client + ; opentelemetry-instrumentation-aiohttp-server + py3{6,7,8,9,10}-test-instrumentation-aiohttp-server + pypy3-test-instrumentation-aiohttp-server + ; opentelemetry-instrumentation-aiopg py3{7,8,9,10,11}-test-instrumentation-aiopg ; instrumentation-aiopg intentionally excluded from pypy3 @@ -287,6 +291,7 @@ changedir = test-opentelemetry-instrumentation: opentelemetry-instrumentation/tests test-instrumentation-aio-pika: instrumentation/opentelemetry-instrumentation-aio-pika/tests test-instrumentation-aiohttp-client: instrumentation/opentelemetry-instrumentation-aiohttp-client/tests + test-instrumentation-aiohttp-server: instrumentation/opentelemetry-instrumentation-aiohttp-server/tests test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests test-instrumentation-asyncpg: instrumentation/opentelemetry-instrumentation-asyncpg/tests @@ -425,6 +430,8 @@ commands_pre = aiohttp-client: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client[test] + aiohttp-server: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server[test] + aiopg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-dbapi pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] richconsole: pip install flaky {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] @@ -527,6 +534,7 @@ commands_pre = python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymemcache[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-psycopg2[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-client[test] + python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiohttp-server[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlite3[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-pyramid[test]