Repository: DataDog/dd-trace-py

Issue:
Customizable top-level django resource name
In newer versions of ddtrace (with #1161 or later, so I believe 0.34.0 based on the milestone), there was a change introduced where the method was placed in the resource name, and for Django versions 2.2+, root resource names for Django were changed to be the url regex. You can find that in the [patch file for Django](https://github.com/DataDog/dd-trace-py/blame/master/ddtrace/contrib/django/patch.py#L349).

This change means that each method and each regex (in the case that a view has more than one regex, like detail/list) is showing when rolling up resources under a service. Here's an example:

### Prior to #1161
![asd0](https://user-images.githubusercontent.com/8700540/81112143-33c20200-8eec-11ea-973a-8902edcb917e.png)

### After #1161
![asd1](https://user-images.githubusercontent.com/8700540/81112150-36245c00-8eec-11ea-90a7-73dcf089a40e.png)

I was able to work around this by adding in some configuration options on my own branch, but I think others may also want to keep the previous resource naming scheme, even if they are upgrading ddtrace or Django.

Thoughts on either adding two boolean config options (to turn off regex and/or turn off the method respectively) or possibly adding a string with preexisting format names (like `"{method} {handler} {path}"`)?

#### ddtrace version
`0.37.0`
#### Django version
`3.0.x`



Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/django/__init__.py b/ddtrace/contrib/django/__init__.py
--- a/ddtrace/contrib/django/__init__.py
+++ b/ddtrace/contrib/django/__init__.py
@@ -101,6 +101,12 @@
 
    Default: ``True``
 
+.. py:data:: ddtrace.config.django['use_handler_resource_format']
+
+   Whether or not to use the legacy resource format `"{method} {handler}"`.
+
+   The default resource format for Django >= 2.2.0 is otherwise `"{method} {urlpattern}"`.
+
 
 Example::
 
diff --git a/ddtrace/contrib/django/patch.py b/ddtrace/contrib/django/patch.py
--- a/ddtrace/contrib/django/patch.py
+++ b/ddtrace/contrib/django/patch.py
@@ -44,6 +44,7 @@
         analytics_sample_rate=None,
         trace_query_string=None,  # Default to global config
         include_user_name=True,
+        use_handler_resource_format=get_env("django", "use_handler_resource_format", default=False),
     ),
 )
 
@@ -354,22 +355,28 @@ def traced_get_response(django, pin, func, instance, args, kwargs):
         resolver_match = None
         resource = request.method
         try:
-            # Resolve the requested url
+            # Resolve the requested url and build resource name pieces
             resolver_match = resolver.resolve(request.path_info)
+            handler, _, _ = resolver_match
+            handler = func_name(handler)
+            urlpattern = ""
+            resource_format = None
 
-            # Determine the resource name to use
-            # In Django >= 2.2.0 we can access the original route or regex pattern
-            if django.VERSION >= (2, 2, 0):
-                route = utils.get_django_2_route(resolver, resolver_match)
-                if route:
-                    resource = "{0} {1}".format(request.method, route)
-                else:
-                    resource = request.method
-            # Older versions just use the view/handler name, e.g. `views.MyView.handler`
+            if config.django.use_handler_resource_format:
+                resource_format = "{method} {handler}"
             else:
-                # TODO: Validate if `resolver.pattern.regex.pattern` is available or not
-                callback, callback_args, callback_kwargs = resolver_match
-                resource = "{0} {1}".format(request.method, func_name(callback))
+                # In Django >= 2.2.0 we can access the original route or regex pattern
+                # TODO: Validate if `resolver.pattern.regex.pattern` is available on django<2.2
+                if django.VERSION >= (2, 2, 0):
+                    route = utils.get_django_2_route(resolver, resolver_match)
+                    resource_format = "{method} {urlpattern}"
+                else:
+                    resource_format = "{method} {handler}"
+
+                if route is not None:
+                    urlpattern = route
+
+            resource = resource_format.format(method=request.method, urlpattern=urlpattern, handler=handler)
 
         except error_type_404:
             # Normalize all 404 requests into a single resource name


Test patch:
diff --git a/tests/contrib/django/test_django.py b/tests/contrib/django/test_django.py
--- a/tests/contrib/django/test_django.py
+++ b/tests/contrib/django/test_django.py
@@ -1348,3 +1348,19 @@ def test_user_name_excluded(client, test_spans):
     root = test_spans.get_root_span()
     assert "django.user.name" not in root.meta
     assert root.meta.get("django.user.is_authenticated") == "True"
+
+
+def test_django_use_handler_resource_format(client, test_spans):
+    """
+    Test that the specified format is used over the default.
+    """
+    with BaseTestCase.override_config("django", dict(use_handler_resource_format=True)):
+        resp = client.get("/")
+        assert resp.status_code == 200
+        assert resp.content == b"Hello, test app."
+
+        # Assert the structure of the root `django.request` span
+        root = test_spans.get_root_span()
+        resource = "GET tests.contrib.django.views.index"
+
+        root.assert_matches(resource=resource, parent_id=None, span_type="http")