diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a3f35757..841382c865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/706)) - `opentelemetry-instrumentation-requests` added exclude urls functionality ([#714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/714)) +- `opentelemetry-instrumentation-django` Add ASGI support + ([#391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/391)) ### Changed - `opentelemetry-instrumentation-botocore` Make common span attributes compliant with semantic conventions @@ -64,12 +66,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package ([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586)) -- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`, - `opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks +- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`, + `opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks ([#576](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/576)) - `opentelemetry-instrumentation-pika` added RabbitMQ's pika module instrumentation. ([#680](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/680)) - + ### Changed - `opentelemetry-instrumentation-fastapi` Allow instrumentation of newer FastAPI versions. diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index 83bfc42f11..7bbf23e407 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -121,7 +121,7 @@ def get_host_port_url_tuple(scope): """Returns (host, port, full_url) tuple.""" server = scope.get("server") or ["0.0.0.0", 80] port = server[1] - server_host = server[0] + (":" + str(port) if port != 80 else "") + server_host = server[0] + (":" + str(port) if str(port) != "80" else "") full_path = scope.get("root_path", "") + scope.get("path", "") http_url = scope.get("scheme", "http") + "://" + server_host + full_path return server_host, port, http_url diff --git a/instrumentation/opentelemetry-instrumentation-django/setup.cfg b/instrumentation/opentelemetry-instrumentation-django/setup.cfg index 67eb7c52b5..4ce8df1a33 100644 --- a/instrumentation/opentelemetry-instrumentation-django/setup.cfg +++ b/instrumentation/opentelemetry-instrumentation-django/setup.cfg @@ -45,6 +45,8 @@ install_requires = opentelemetry-semantic-conventions == 0.24b0 [options.extras_require] +asgi = + opentelemetry-instrumentation-asgi == 0.24b0 test = opentelemetry-test == 0.24b0 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 f071ba6e56..114f6f4a51 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import types from logging import getLogger from time import time from typing import Callable @@ -24,11 +25,11 @@ get_global_response_propagator, ) from opentelemetry.instrumentation.utils import extract_attributes_from_object +from opentelemetry.instrumentation.wsgi import add_response_attributes from opentelemetry.instrumentation.wsgi import ( - add_response_attributes, - collect_request_attributes, - wsgi_getter, + collect_request_attributes as wsgi_collect_request_attributes, ) +from opentelemetry.instrumentation.wsgi import wsgi_getter from opentelemetry.propagate import extract from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span, SpanKind, use_span @@ -43,6 +44,7 @@ from django.urls import Resolver404, resolve DJANGO_2_0 = django_version >= (2, 0) +DJANGO_3_0 = django_version >= (3, 0) if DJANGO_2_0: # Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style @@ -67,6 +69,26 @@ def __call__(self, request): except ImportError: MiddlewareMixin = object +if DJANGO_3_0: + from django.core.handlers.asgi import ASGIRequest +else: + ASGIRequest = None + +# try/except block exclusive for optional ASGI imports. +try: + from opentelemetry.instrumentation.asgi import asgi_getter + from opentelemetry.instrumentation.asgi import ( + collect_request_attributes as asgi_collect_request_attributes, + ) + from opentelemetry.instrumentation.asgi import set_status_code + + _is_asgi_supported = True +except ImportError: + asgi_getter = None + asgi_collect_request_attributes = None + set_status_code = None + _is_asgi_supported = False + _logger = getLogger(__name__) _attributes_by_preference = [ @@ -91,6 +113,10 @@ def __call__(self, request): ] +def _is_asgi_request(request: HttpRequest) -> bool: + return ASGIRequest is not None and isinstance(request, ASGIRequest) + + class _DjangoMiddleware(MiddlewareMixin): """Django Middleware for OpenTelemetry""" @@ -140,12 +166,25 @@ def process_request(self, request): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return + is_asgi_request = _is_asgi_request(request) + if not _is_asgi_supported and is_asgi_request: + return + # pylint:disable=W0212 request._otel_start_time = time() request_meta = request.META - token = attach(extract(request_meta, getter=wsgi_getter)) + if is_asgi_request: + carrier = request.scope + carrier_getter = asgi_getter + collect_request_attributes = asgi_collect_request_attributes + else: + carrier = request_meta + carrier_getter = wsgi_getter + collect_request_attributes = wsgi_collect_request_attributes + + token = attach(extract(request_meta, getter=carrier_getter)) span = self._tracer.start_span( self._get_span_name(request), @@ -155,12 +194,25 @@ def process_request(self, request): ), ) - attributes = collect_request_attributes(request_meta) + attributes = collect_request_attributes(carrier) if span.is_recording(): attributes = extract_attributes_from_object( request, self._traced_request_attrs, attributes ) + if is_asgi_request: + # ASGI requests include extra attributes in request.scope.headers. + attributes = extract_attributes_from_object( + types.SimpleNamespace( + **{ + name.decode("latin1"): value.decode("latin1") + for name, value in request.scope.get("headers", []) + } + ), + self._traced_request_attrs, + attributes, + ) + for key, value in attributes.items(): span.set_attribute(key, value) @@ -207,15 +259,22 @@ def process_response(self, request, response): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return response + is_asgi_request = _is_asgi_request(request) + if not _is_asgi_supported and is_asgi_request: + return response + activation = request.META.pop(self._environ_activation_key, None) span = request.META.pop(self._environ_span_key, None) if activation and span: - add_response_attributes( - span, - f"{response.status_code} {response.reason_phrase}", - response, - ) + if is_asgi_request: + set_status_code(span, response.status_code) + else: + add_response_attributes( + span, + f"{response.status_code} {response.reason_phrase}", + response, + ) propagator = get_global_response_propagator() if propagator: @@ -238,7 +297,7 @@ def process_response(self, request, response): activation.__exit__(None, None, None) if self._environ_token in request.META.keys(): - detach(request.environ.get(self._environ_token)) + detach(request.META.get(self._environ_token)) request.META.pop(self._environ_token) return response diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index bec805ffd8..c9142472ce 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -15,12 +15,11 @@ 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 import VERSION, conf from django.http import HttpRequest, HttpResponse -from django.test import Client +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, @@ -57,13 +56,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() @@ -71,8 +70,8 @@ class TestMiddleware(TestBase, WsgiTestBase): @classmethod def setUpClass(cls): + conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() - settings.configure(ROOT_URLCONF=modules[__name__]) def setUp(self): super().setUp() @@ -105,6 +104,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/") @@ -357,6 +361,7 @@ def test_trace_response_headers(self): class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase): @classmethod def setUpClass(cls): + conf.settings.configure(ROOT_URLCONF=modules[__name__]) super().setUpClass() def setUp(self): @@ -375,6 +380,11 @@ def tearDown(self): teardown_test_environment() _django_instrumentor.uninstrument() + @classmethod + def tearDownClass(cls): + super().tearDownClass() + conf.settings = conf.LazySettings() + def test_tracer_provider_traced(self): Client().post("/traced/") 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..36a45c5fc1 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -0,0 +1,382 @@ +# 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 + +import pytest +from django import VERSION, conf +from django.http import HttpRequest, HttpResponse +from django.test import SimpleTestCase +from django.test.utils import setup_test_environment, teardown_test_environment +from django.urls import re_path + +from opentelemetry.instrumentation.django import ( + DjangoInstrumentor, + _DjangoMiddleware, +) +from opentelemetry.instrumentation.propagators import ( + TraceResponsePropagator, + set_global_response_propagator, +) +from opentelemetry.sdk import resources +from opentelemetry.sdk.trace import Span +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import ( + SpanKind, + StatusCode, + format_span_id, + format_trace_id, +) +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) + +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): + conf.settings.configure(ROOT_URLCONF=modules[__name__]) + super().setUpClass() + + 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() + + 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[SpanAttributes.HTTP_METHOD], "GET") + self.assertEqual( + span.attributes[SpanAttributes.HTTP_URL], + "http://127.0.0.1/route/2020/template/", + ) + self.assertEqual( + span.attributes[SpanAttributes.HTTP_ROUTE], + "^route/(?P[0-9]{4})/template/$", + ) + self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + + 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[SpanAttributes.HTTP_METHOD], "GET") + self.assertEqual( + span.attributes[SpanAttributes.HTTP_URL], + "http://127.0.0.1/traced/", + ) + self.assertEqual( + span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + + 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) + + 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[SpanAttributes.HTTP_METHOD], "POST") + self.assertEqual( + span.attributes[SpanAttributes.HTTP_URL], + "http://127.0.0.1/traced/", + ) + self.assertEqual( + span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + + 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[SpanAttributes.HTTP_METHOD], "GET") + self.assertEqual( + span.attributes[SpanAttributes.HTTP_URL], + "http://127.0.0.1/error/", + ) + self.assertEqual(span.attributes[SpanAttributes.HTTP_ROUTE], "^error/") + self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 500) + + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + self.assertEqual( + event.attributes[SpanAttributes.EXCEPTION_TYPE], "ValueError" + ) + self.assertEqual( + event.attributes[SpanAttributes.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") + + 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) + + async def test_hooks(self): + request_hook_args = () + response_hook_args = () + + def request_hook(span, request): + nonlocal request_hook_args + request_hook_args = (span, request) + + def response_hook(span, request, response): + nonlocal response_hook_args + response_hook_args = (span, request, response) + response["hook-header"] = "set by hook" + + _DjangoMiddleware._otel_request_hook = request_hook + _DjangoMiddleware._otel_response_hook = response_hook + + response = await self.async_client.get("/span_name/1234/") + _DjangoMiddleware._otel_request_hook = ( + _DjangoMiddleware._otel_response_hook + ) = None + + self.assertEqual(response["hook-header"], "set by hook") + + 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(len(request_hook_args), 2) + self.assertEqual(request_hook_args[0].name, span.name) + self.assertIsInstance(request_hook_args[0], Span) + self.assertIsInstance(request_hook_args[1], HttpRequest) + + self.assertEqual(len(response_hook_args), 3) + self.assertEqual(request_hook_args[0], response_hook_args[0]) + self.assertIsInstance(response_hook_args[1], HttpRequest) + self.assertIsInstance(response_hook_args[2], HttpResponse) + self.assertEqual(response_hook_args[2], response) + + async def test_trace_response_headers(self): + response = await self.async_client.get("/span_name/1234/") + + self.assertNotIn("Server-Timing", response.headers) + self.memory_exporter.clear() + + set_global_response_propagator(TraceResponsePropagator()) + + response = await self.async_client.get("/span_name/1234/") + span = self.memory_exporter.get_finished_spans()[0] + + self.assertIn("traceresponse", response.headers) + self.assertEqual( + response.headers["Access-Control-Expose-Headers"], "traceresponse", + ) + self.assertEqual( + response.headers["traceresponse"], + "00-{0}-{1}-01".format( + format_trace_id(span.get_span_context().trace_id), + format_span_id(span.get_span_context().span_id), + ), + ) + self.memory_exporter.clear() + + +class TestMiddlewareAsgiWithTracerProvider(SimpleTestCase, TestBase): + @classmethod + def setUpClass(cls): + conf.settings.configure(ROOT_URLCONF=modules[__name__]) + super().setUpClass() + + def setUp(self): + super().setUp() + setup_test_environment() + resource = resources.Resource.create( + {"resource-key": "resource-value"} + ) + result = self.create_tracer_provider(resource=resource) + tracer_provider, exporter = result + self.exporter = exporter + _django_instrumentor.instrument(tracer_provider=tracer_provider) + + def tearDown(self): + super().tearDown() + teardown_test_environment() + _django_instrumentor.uninstrument() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + conf.settings = conf.LazySettings() + + async def test_tracer_provider_traced(self): + await self.async_client.post("/traced/") + + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual( + span.resource.attributes["resource-key"], "resource-value" + ) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index 872222a842..0bcc7e95be 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -29,3 +29,35 @@ 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() diff --git a/tox.ini b/tox.ini index f8d09a9b97..20ac03b53e 100644 --- a/tox.ini +++ b/tox.ini @@ -258,7 +258,7 @@ commands_pre = falcon{2,3},flask,django,pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test] wsgi,falcon{2,3},flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test] - asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test] + asgi,django,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test] asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]