From 60222c2282914a484f4c6f41ff1018e09e931f9e Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Tue, 6 Apr 2021 12:53:45 -0300 Subject: [PATCH] Work on unit tests with AsyncClient --- .../setup.cfg | 2 +- .../instrumentation/django/middleware.py | 5 +- .../tests/test_middleware.py | 28 +- .../tests/test_middleware_asgi.py | 274 ++++++++++++++++++ .../tests/views.py | 30 ++ 5 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index 945212ba68..370847b8a5 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -46,7 +46,7 @@ install_requires = [options.extras_require] asgi = - opentelemetry-instrumentation-asgi == 0.19.b0 + opentelemetry-instrumentation-asgi == 0.20.dev0 test = opentelemetry-test == 0.20.dev0 diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 990803cfbf..04d6149eb7 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -142,7 +142,10 @@ def process_request(self, request): ), ) - attributes = collect_request_attributes(request_meta) + if is_asgi_request: + attributes = collect_request_attributes(request.scope) + else: + attributes = collect_request_attributes(request_meta) if span.is_recording(): attributes = extract_attributes_from_object( diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 154e68bc15..db714768ca 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -15,11 +15,10 @@ from sys import modules from unittest.mock import Mock, patch -from django import VERSION -from django.conf import settings -from django.conf.urls import url -from django.test import Client +from django import VERSION, conf +from django.test.client import Client from django.test.utils import setup_test_environment, teardown_test_environment +from django.urls import re_path from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.test.test_base import TestBase @@ -41,13 +40,13 @@ DJANGO_2_2 = VERSION >= (2, 2) urlpatterns = [ - url(r"^traced/", traced), - url(r"^route/(?P[0-9]{4})/template/$", traced_template), - url(r"^error/", error), - url(r"^excluded_arg/", excluded), - url(r"^excluded_noarg/", excluded_noarg), - url(r"^excluded_noarg2/", excluded_noarg2), - url(r"^span_name/([0-9]{4})/$", route_span_name), + re_path(r"^traced/", traced), + re_path(r"^route/(?P[0-9]{4})/template/$", traced_template), + re_path(r"^error/", error), + re_path(r"^excluded_arg/", excluded), + re_path(r"^excluded_noarg/", excluded_noarg), + re_path(r"^excluded_noarg2/", excluded_noarg2), + re_path(r"^span_name/([0-9]{4})/$", route_span_name), ] _django_instrumentor = DjangoInstrumentor() @@ -56,7 +55,7 @@ class TestMiddleware(TestBase, WsgiTestBase): @classmethod def setUpClass(cls): super().setUpClass() - settings.configure(ROOT_URLCONF=modules[__name__]) + conf.settings.configure(ROOT_URLCONF=modules[__name__]) def setUp(self): super().setUp() @@ -89,6 +88,11 @@ def tearDown(self): teardown_test_environment() _django_instrumentor.uninstrument() + @classmethod + def tearDownClass(cls): + super().tearDownClass() + conf.settings = conf.LazySettings() + def test_templated_route_get(self): Client().get("/route/2020/template/") diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py new file mode 100644 index 0000000000..8a8ce8bd9c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -0,0 +1,274 @@ +# Copyright The 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. + +from sys import modules +from unittest.mock import Mock, patch + +from django import VERSION, conf +from django.test import SimpleTestCase +from django.test.utils import setup_test_environment, teardown_test_environment +from django.urls import re_path +import pytest + +from opentelemetry.instrumentation.django import DjangoInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind, StatusCode +from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs + +# pylint: disable=import-error +from .views import ( + async_error, + async_excluded, + async_excluded_noarg, + async_excluded_noarg2, + async_route_span_name, + async_traced, + async_traced_template, +) + +DJANGO_3_1 = VERSION >= (3, 1) + +if DJANGO_3_1: + from django.test.client import AsyncClient +else: + AsyncClient = None + +urlpatterns = [ + re_path(r"^traced/", async_traced), + re_path(r"^route/(?P[0-9]{4})/template/$", async_traced_template), + re_path(r"^error/", async_error), + re_path(r"^excluded_arg/", async_excluded), + re_path(r"^excluded_noarg/", async_excluded_noarg), + re_path(r"^excluded_noarg2/", async_excluded_noarg2), + re_path(r"^span_name/([0-9]{4})/$", async_route_span_name), +] +_django_instrumentor = DjangoInstrumentor() + + +@pytest.mark.skipif(not DJANGO_3_1, reason="AsyncClient implemented since Django 3.1") +class TestMiddlewareAsgi(SimpleTestCase, TestBase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + conf.settings.configure(ROOT_URLCONF=modules[__name__]) + + def setUp(self): + super().setUp() + setup_test_environment() + _django_instrumentor.instrument() + self.env_patch = patch.dict( + "os.environ", + { + "OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "http://testserver/excluded_arg/123,excluded_noarg", + "OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS": "path_info,content_type,non_existing_variable", + }, + ) + self.env_patch.start() + self.exclude_patch = patch( + "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls", + get_excluded_urls("DJANGO"), + ) + self.traced_patch = patch( + "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs", + get_traced_request_attrs("DJANGO"), + ) + self.exclude_patch.start() + self.traced_patch.start() + + def tearDown(self): + super().tearDown() + self.env_patch.stop() + self.exclude_patch.stop() + self.traced_patch.stop() + teardown_test_environment() + _django_instrumentor.uninstrument() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + conf.settings = conf.LazySettings() + + @classmethod + def _add_databases_failures(cls): + # Disable databases. + pass + + @classmethod + def _remove_databases_failures(cls): + # Disable databases. + pass + + @pytest.mark.skip(reason="TODO") + async def test_templated_route_get(self): + await self.async_client.get("/route/2020/template/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "^route/(?P[0-9]{4})/template/$") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual( + span.attributes["http.url"], + "http://testserver/route/2020/template/", + ) + self.assertEqual( + span.attributes["http.route"], + "^route/(?P[0-9]{4})/template/$", + ) + self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 200) + self.assertEqual(span.attributes["http.status_text"], "OK") + + @pytest.mark.skip(reason="TODO") + async def test_traced_get(self): + await self.async_client.get("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "^traced/") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual( + span.attributes["http.url"], "http://testserver/traced/" + ) + self.assertEqual(span.attributes["http.route"], "^traced/") + self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 200) + self.assertEqual(span.attributes["http.status_text"], "OK") + + async def test_not_recording(self): + mock_tracer = Mock() + mock_span = Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + with patch("opentelemetry.trace.get_tracer") as tracer: + tracer.return_value = mock_tracer + await self.async_client.get("/traced/") + 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) + + @pytest.mark.skip(reason="TODO") + async def test_traced_post(self): + await self.async_client.post("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "^traced/") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes["http.method"], "POST") + self.assertEqual( + span.attributes["http.url"], "http://testserver/traced/" + ) + self.assertEqual(span.attributes["http.route"], "^traced/") + self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 200) + self.assertEqual(span.attributes["http.status_text"], "OK") + + @pytest.mark.skip(reason="TODO") + async def test_error(self): + with self.assertRaises(ValueError): + await self.async_client.get("/error/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "^error/") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual(span.attributes["http.method"], "GET") + self.assertEqual( + span.attributes["http.url"], "http://testserver/error/" + ) + self.assertEqual(span.attributes["http.route"], "^error/") + self.assertEqual(span.attributes["http.scheme"], "http") + self.assertEqual(span.attributes["http.status_code"], 500) + + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + self.assertEqual(event.attributes["exception.type"], "ValueError") + self.assertEqual(event.attributes["exception.message"], "error") + + async def test_exclude_lists(self): + await self.async_client.get("/excluded_arg/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + await self.async_client.get("/excluded_arg/125") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + await self.async_client.get("/excluded_noarg/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + await self.async_client.get("/excluded_noarg2/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + async def test_span_name(self): + # test no query_string + await self.async_client.get("/span_name/1234/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual(span.name, "^span_name/([0-9]{4})/$") + + async def test_span_name_for_query_string(self): + """ + request not have query string + """ + await self.async_client.get("/span_name/1234/?query=test") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual(span.name, "^span_name/([0-9]{4})/$") + + async def test_span_name_404(self): + await self.async_client.get("/span_name/1234567890/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual(span.name, "HTTP GET") + + @pytest.mark.skip(reason="TODO") + async def test_traced_request_attrs(self): + await self.async_client.get("/span_name/1234/", CONTENT_TYPE="test/ct") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + span = span_list[0] + self.assertEqual(span.attributes["path_info"], "/span_name/1234/") + self.assertEqual(span.attributes["content_type"], "test/ct") + self.assertNotIn("non_existing_variable", span.attributes) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index 872222a842..82220717ce 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -29,3 +29,33 @@ def route_span_name( request, *args, **kwargs ): # pylint: disable=unused-argument return HttpResponse() + + +async def async_traced(request): # pylint: disable=unused-argument + return HttpResponse() + + +async def async_traced_template(request, year): # pylint: disable=unused-argument + return HttpResponse() + + +async def async_error(request): # pylint: disable=unused-argument + raise ValueError("error") + + +async def async_excluded(request): # pylint: disable=unused-argument + return HttpResponse() + + +async def async_excluded_noarg(request): # pylint: disable=unused-argument + return HttpResponse() + + +async def async_excluded_noarg2(request): # pylint: disable=unused-argument + return HttpResponse() + + +async def async_route_span_name( + request, *args, **kwargs +): # pylint: disable=unused-argument + return HttpResponse()