Repository: DataDog/dd-trace-py

Issue:
Sanic resource names gets grouped incorrectly
Hi!

The Endpoints gets grouped incorrectly in the UI when using the Sanic integration because the path parameter values are used in the resource name instead of the parameter names and thus creating one unique Endpoint for every unique method + request path.

Example:
![image](https://user-images.githubusercontent.com/853582/102177233-63ce7c00-3ea3-11eb-8bb4-76cb5470bb06.png)

Is this by design? Other integrations (node express for example) groups them by the paramater names which imo seems to be the proper way to do it.

I have created a PR to solve this: #1879


Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/sanic/patch.py b/ddtrace/contrib/sanic/patch.py
--- a/ddtrace/contrib/sanic/patch.py
+++ b/ddtrace/contrib/sanic/patch.py
@@ -52,6 +52,18 @@ async def wrap_async(wrapped, instance, args, kwargs):
     return wrap_sync(callback)
 
 
+def _get_path(request):
+    """Get path and replace path parameter values with names if route exists."""
+    path = request.path
+    try:
+        match_info = request.match_info
+    except sanic.exceptions.SanicException:
+        return path
+    for key, value in match_info.items():
+        path = path.replace(value, f"<{key}>")
+    return path
+
+
 def patch():
     """Patch the instrumented methods."""
     if getattr(sanic, "__datadog_patch", False):
@@ -77,7 +89,7 @@ async def patch_handle_request(wrapped, instance, args, kwargs):
     if request.scheme not in ("http", "https"):
         return await wrapped(request, write_callback, stream_callback, **kwargs)
 
-    resource = "{} {}".format(request.method, request.path)
+    resource = "{} {}".format(request.method, _get_path(request))
 
     headers = request.headers.copy()
 


Test patch:
diff --git a/tests/contrib/sanic/test_sanic.py b/tests/contrib/sanic/test_sanic.py
--- a/tests/contrib/sanic/test_sanic.py
+++ b/tests/contrib/sanic/test_sanic.py
@@ -49,6 +49,16 @@ async def hello(request):
         await random_sleep()
         return json({"hello": "world"})
 
+    @app.route("/hello/<first_name>")
+    async def hello_single_param(request, first_name):
+        await random_sleep()
+        return json({"hello": first_name})
+
+    @app.route("/hello/<first_name>/<surname>")
+    async def hello_multiple_params(request, first_name, surname):
+        await random_sleep()
+        return json({"hello": f"{first_name} {surname}"})
+
     @app.route("/stream_response")
     async def stream_response(request):
         async def sample_streaming_fn(response):
@@ -142,6 +152,7 @@ async def test_basic_app(tracer, client, integration_config, integration_http_co
     assert request_span.get_tag("http.method") == "GET"
     assert re.search("/hello$", request_span.get_tag("http.url"))
     assert request_span.get_tag("http.status_code") == "200"
+    assert request_span.resource == "GET /hello"
 
     sleep_span = spans[0][1]
     assert sleep_span.name == "tests.contrib.sanic.test_sanic.random_sleep"
@@ -171,6 +182,23 @@ async def test_basic_app(tracer, client, integration_config, integration_http_co
         assert request_span.trace_id is not None and request_span.trace_id != 5678
 
 
+@pytest.mark.parametrize(
+    "url, expected_json, expected_resource",
+    [
+        ("/hello/foo", {"hello": "foo"}, "GET /hello/<first_name>"),
+        ("/hello/foo/bar", {"hello": "foo bar"}, "GET /hello/<first_name>/<surname>"),
+    ],
+)
+async def test_resource_name(tracer, client, url, expected_json, expected_resource):
+    response = await client.get(url)
+    assert _response_status(response) == 200
+    assert await _response_json(response) == expected_json
+
+    spans = tracer.writer.pop_traces()
+    request_span = spans[0][0]
+    assert request_span.resource == expected_resource
+
+
 async def test_streaming_response(tracer, client):
     response = await client.get("/stream_response")
     assert _response_status(response) == 200