Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django span names according to convention #992

Merged
merged 15 commits into from
Sep 21, 2020
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Changed span name extraction from request to comply semantic convention
HiveTraum marked this conversation as resolved.
Show resolved Hide resolved

## Version 0.12b0

Released 2020-08-14
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
from opentelemetry.trace import SpanKind, get_tracer
from opentelemetry.util import ExcludeList

try:
from django.core.urlresolvers import ( # pylint: disable=no-name-in-module
resolve,
Resolver404,
)
except ImportError:
from django.urls import resolve, Resolver404

try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
Expand All @@ -50,17 +58,33 @@ class _DjangoMiddleware(MiddlewareMixin):
else:
_excluded_urls = ExcludeList(_excluded_urls)

def process_view(
self, request, view_func, view_args, view_kwargs
): # pylint: disable=unused-argument
@staticmethod
def _get_span_name(request):
try:
if getattr(request, "resolver_match"):
match = request.resolver_match
else:
match = resolve(request.get_full_path())

if hasattr(match, "route"):
return match.route

# Instead of using `view_name`, better to use `_func_path` as some applications can use similar
# view names in different modules
if hasattr(match, "_func_name"):
HiveTraum marked this conversation as resolved.
Show resolved Hide resolved
return match._func_name # pylint: disable=protected-access

# Fallback for safety as `_func_name` private field
return match.view_name
HiveTraum marked this conversation as resolved.
Show resolved Hide resolved

except Resolver404:
return "HTTP {}".format(request.method)

def process_request(self, request):
# request.META is a dictionary containing all available HTTP headers
# Read more about request.META here:
# https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META

# environ = {
# key.lower().replace('_', '-').replace("http-", "", 1): value
# for key, value in request.META.items()
# }
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
return

Expand All @@ -73,7 +97,7 @@ def process_view(
attributes = collect_request_attributes(environ)

span = tracer.start_span(
view_func.__name__,
self._get_span_name(request),
kind=SpanKind.SERVER,
attributes=attributes,
start_time=environ.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sys import modules
from unittest.mock import patch

from django import VERSION
from django.conf import settings
from django.conf.urls import url
from django.test import Client
Expand All @@ -28,14 +29,24 @@
from opentelemetry.util import ExcludeList

# pylint: disable=import-error
from .views import error, excluded, excluded_noarg, excluded_noarg2, traced
from .views import (
error,
excluded,
excluded_noarg,
excluded_noarg2,
route_span_name,
traced,
)

DJANGO_2_2 = VERSION >= (2, 2)

urlpatterns = [
url(r"^traced/", traced),
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),
]
_django_instrumentor = DjangoInstrumentor()

Expand Down Expand Up @@ -65,7 +76,9 @@ def test_traced_get(self):

span = spans[0]

self.assertEqual(span.name, "traced")
self.assertEqual(
span.name, "^traced/" 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")
Expand All @@ -84,7 +97,9 @@ def test_traced_post(self):

span = spans[0]

self.assertEqual(span.name, "traced")
self.assertEqual(
span.name, "^traced/" 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"], "POST")
Expand All @@ -104,7 +119,9 @@ def test_error(self):

span = spans[0]

self.assertEqual(span.name, "error")
self.assertEqual(
span.name, "^error/" if DJANGO_2_2 else "tests.views.error"
)
self.assertEqual(span.kind, SpanKind.SERVER)
self.assertEqual(
span.status.canonical_code, StatusCanonicalCode.UNKNOWN
Expand Down Expand Up @@ -136,3 +153,24 @@ def test_exclude_lists(self):
client.get("/excluded_noarg2/")
span_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(span_list), 1)

def test_span_name(self):
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})/$"
if DJANGO_2_2
else "tests.views.route_span_name",
)

def test_span_name_404(self):
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")
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ def excluded_noarg(request): # pylint: disable=unused-argument

def excluded_noarg2(request): # pylint: disable=unused-argument
return HttpResponse()


def route_span_name(
request, *args, **kwargs
): # pylint: disable=unused-argument
return HttpResponse()