diff --git a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md index 2248ea35c8..439b65b17c 100644 --- a/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md +++ b/instrumentation/opentelemetry-instrumentation-django/CHANGELOG.md @@ -10,6 +10,7 @@ Released 2020-10-13 - Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992)) - Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154)) +- Added capture of http.route ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226)) ## Version 0.13b0 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 11991413eb..e3cb78dbd8 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -125,6 +125,26 @@ def process_request(self, request): request.META[self._environ_span_key] = span request.META[self._environ_token] = token + # pylint: disable=unused-argument + def process_view(self, request, view_func, *args, **kwargs): + # Process view is executed before the view function, here we get the + # route template from request.resolver_match. It is not set yet in process_request + if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): + return + + if ( + self._environ_activation_key in request.META.keys() + and self._environ_span_key in request.META.keys() + ): + span = request.META[self._environ_span_key] + + if span.is_recording(): + match = getattr(request, "resolver_match") + if match: + route = getattr(match, "route") + if route: + span.set_attribute("http.route", route) + def process_exception(self, request, exception): # Django can call this method and process_response later. In order # to avoid __exit__ and detach from being called twice then, the diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 4db6c485de..6e3196e3ef 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -36,12 +36,14 @@ excluded_noarg2, route_span_name, traced, + traced_template, ) 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), @@ -68,6 +70,35 @@ def tearDown(self): teardown_test_environment() _django_instrumentor.uninstrument() + def test_templated_route_get(self): + 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/$" + if DJANGO_2_2 + else "tests.views.traced", + ) + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK) + 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") + def test_traced_get(self): Client().get("/traced/") @@ -85,6 +116,7 @@ def test_traced_get(self): 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") @@ -121,6 +153,7 @@ def test_traced_post(self): 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") @@ -145,6 +178,7 @@ def test_error(self): self.assertEqual( span.attributes["http.url"], "http://testserver/error/" ) + self.assertEqual(span.attributes["http.route"], "^error/") self.assertEqual(span.attributes["http.scheme"], "http") @patch( diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index e286841011..872222a842 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -5,6 +5,10 @@ def traced(request): # pylint: disable=unused-argument return HttpResponse() +def traced_template(request, year): # pylint: disable=unused-argument + return HttpResponse() + + def error(request): # pylint: disable=unused-argument raise ValueError("error")