We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ddtrace/contrib/sanic/patch.py`
Content:
```
1 import asyncio
2 import ddtrace
3 import sanic
4 from ddtrace import config
5 from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
6 from ddtrace.ext import SpanTypes
7 from ddtrace.propagation.http import HTTPPropagator
8 from ddtrace.utils.wrappers import unwrap as _u
9 from ddtrace.vendor import wrapt
10 from ddtrace.vendor.wrapt import wrap_function_wrapper as _w
11 
12 from .. import trace_utils
13 from ...internal.logger import get_logger
14 
15 log = get_logger(__name__)
16 
17 config._add("sanic", dict(_default_service="sanic", distributed_tracing=True))
18 
19 
20 def _wrap_response_callback(span, callback):
21     # wrap response callbacks (either sync or async function) to set span tags
22     # based on response and finish span before returning response
23 
24     def update_span(response):
25         if isinstance(response, sanic.response.BaseHTTPResponse):
26             status_code = response.status
27             response_headers = response.headers
28         else:
29             # invalid response causes ServerError exception which must be handled
30             status_code = 500
31             response_headers = None
32         trace_utils.set_http_meta(span, config.sanic, status_code=status_code, response_headers=response_headers)
33         span.finish()
34 
35     @wrapt.function_wrapper
36     def wrap_sync(wrapped, instance, args, kwargs):
37         r = wrapped(*args, **kwargs)
38         response = args[0]
39         update_span(response)
40         return r
41 
42     @wrapt.function_wrapper
43     async def wrap_async(wrapped, instance, args, kwargs):
44         r = await wrapped(*args, **kwargs)
45         response = args[0]
46         update_span(response)
47         return r
48 
49     if asyncio.iscoroutinefunction(callback):
50         return wrap_async(callback)
51 
52     return wrap_sync(callback)
53 
54 
55 def patch():
56     """Patch the instrumented methods."""
57     if getattr(sanic, "__datadog_patch", False):
58         return
59     setattr(sanic, "__datadog_patch", True)
60     _w("sanic", "Sanic.handle_request", patch_handle_request)
61 
62 
63 def unpatch():
64     """Unpatch the instrumented methods."""
65     _u(sanic.Sanic, "handle_request")
66     if not getattr(sanic, "__datadog_patch", False):
67         return
68     setattr(sanic, "__datadog_patch", False)
69 
70 
71 async def patch_handle_request(wrapped, instance, args, kwargs):
72     """Wrapper for Sanic.handle_request"""
73     request = kwargs.get("request", args[0])
74     write_callback = kwargs.get("write_callback", args[1])
75     stream_callback = kwargs.get("stream_callback", args[2])
76 
77     if request.scheme not in ("http", "https"):
78         return await wrapped(request, write_callback, stream_callback, **kwargs)
79 
80     resource = "{} {}".format(request.method, request.path)
81 
82     headers = request.headers.copy()
83 
84     if config.sanic.distributed_tracing:
85         propagator = HTTPPropagator()
86         context = propagator.extract(headers)
87         if context.trace_id:
88             ddtrace.tracer.context_provider.activate(context)
89 
90     span = ddtrace.tracer.trace(
91         "sanic.request",
92         service=trace_utils.int_service(None, config.sanic),
93         resource=resource,
94         span_type=SpanTypes.WEB,
95     )
96     sample_rate = config.sanic.get_analytics_sample_rate(use_global_config=True)
97     if sample_rate is not None:
98         span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)
99 
100     method = request.method
101     url = "{scheme}://{host}{path}".format(scheme=request.scheme, host=request.host, path=request.path)
102     query_string = request.query_string
103     if isinstance(query_string, bytes):
104         query_string = query_string.decode()
105     trace_utils.set_http_meta(span, config.sanic, method=method, url=url, query=query_string, request_headers=headers)
106 
107     if write_callback is not None:
108         write_callback = _wrap_response_callback(span, write_callback)
109     if stream_callback is not None:
110         stream_callback = _wrap_response_callback(span, stream_callback)
111 
112     return await wrapped(request, write_callback, stream_callback, **kwargs)
113
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
     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 @@
     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()

{"golden_diff": "diff --git a/ddtrace/contrib/sanic/patch.py b/ddtrace/contrib/sanic/patch.py\n--- a/ddtrace/contrib/sanic/patch.py\n+++ b/ddtrace/contrib/sanic/patch.py\n@@ -52,6 +52,18 @@\n     return wrap_sync(callback)\n \n \n+def _get_path(request):\n+    \"\"\"Get path and replace path parameter values with names if route exists.\"\"\"\n+    path = request.path\n+    try:\n+        match_info = request.match_info\n+    except sanic.exceptions.SanicException:\n+        return path\n+    for key, value in match_info.items():\n+        path = path.replace(value, f\"<{key}>\")\n+    return path\n+\n+\n def patch():\n     \"\"\"Patch the instrumented methods.\"\"\"\n     if getattr(sanic, \"__datadog_patch\", False):\n@@ -77,7 +89,7 @@\n     if request.scheme not in (\"http\", \"https\"):\n         return await wrapped(request, write_callback, stream_callback, **kwargs)\n \n-    resource = \"{} {}\".format(request.method, request.path)\n+    resource = \"{} {}\".format(request.method, _get_path(request))\n \n     headers = request.headers.copy()\n", "issue": "Sanic resource names gets grouped incorrectly\nHi!\r\n\r\nThe 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.\r\n\r\nExample:\r\n![image](https://user-images.githubusercontent.com/853582/102177233-63ce7c00-3ea3-11eb-8bb4-76cb5470bb06.png)\r\n\r\nIs 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.\r\n\r\nI have created a PR to solve this: #1879\n", "before_files": [{"content": "import asyncio\nimport ddtrace\nimport sanic\nfrom ddtrace import config\nfrom ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY\nfrom ddtrace.ext import SpanTypes\nfrom ddtrace.propagation.http import HTTPPropagator\nfrom ddtrace.utils.wrappers import unwrap as _u\nfrom ddtrace.vendor import wrapt\nfrom ddtrace.vendor.wrapt import wrap_function_wrapper as _w\n\nfrom .. import trace_utils\nfrom ...internal.logger import get_logger\n\nlog = get_logger(__name__)\n\nconfig._add(\"sanic\", dict(_default_service=\"sanic\", distributed_tracing=True))\n\n\ndef _wrap_response_callback(span, callback):\n    # wrap response callbacks (either sync or async function) to set span tags\n    # based on response and finish span before returning response\n\n    def update_span(response):\n        if isinstance(response, sanic.response.BaseHTTPResponse):\n            status_code = response.status\n            response_headers = response.headers\n        else:\n            # invalid response causes ServerError exception which must be handled\n            status_code = 500\n            response_headers = None\n        trace_utils.set_http_meta(span, config.sanic, status_code=status_code, response_headers=response_headers)\n        span.finish()\n\n    @wrapt.function_wrapper\n    def wrap_sync(wrapped, instance, args, kwargs):\n        r = wrapped(*args, **kwargs)\n        response = args[0]\n        update_span(response)\n        return r\n\n    @wrapt.function_wrapper\n    async def wrap_async(wrapped, instance, args, kwargs):\n        r = await wrapped(*args, **kwargs)\n        response = args[0]\n        update_span(response)\n        return r\n\n    if asyncio.iscoroutinefunction(callback):\n        return wrap_async(callback)\n\n    return wrap_sync(callback)\n\n\ndef patch():\n    \"\"\"Patch the instrumented methods.\"\"\"\n    if getattr(sanic, \"__datadog_patch\", False):\n        return\n    setattr(sanic, \"__datadog_patch\", True)\n    _w(\"sanic\", \"Sanic.handle_request\", patch_handle_request)\n\n\ndef unpatch():\n    \"\"\"Unpatch the instrumented methods.\"\"\"\n    _u(sanic.Sanic, \"handle_request\")\n    if not getattr(sanic, \"__datadog_patch\", False):\n        return\n    setattr(sanic, \"__datadog_patch\", False)\n\n\nasync def patch_handle_request(wrapped, instance, args, kwargs):\n    \"\"\"Wrapper for Sanic.handle_request\"\"\"\n    request = kwargs.get(\"request\", args[0])\n    write_callback = kwargs.get(\"write_callback\", args[1])\n    stream_callback = kwargs.get(\"stream_callback\", args[2])\n\n    if request.scheme not in (\"http\", \"https\"):\n        return await wrapped(request, write_callback, stream_callback, **kwargs)\n\n    resource = \"{} {}\".format(request.method, request.path)\n\n    headers = request.headers.copy()\n\n    if config.sanic.distributed_tracing:\n        propagator = HTTPPropagator()\n        context = propagator.extract(headers)\n        if context.trace_id:\n            ddtrace.tracer.context_provider.activate(context)\n\n    span = ddtrace.tracer.trace(\n        \"sanic.request\",\n        service=trace_utils.int_service(None, config.sanic),\n        resource=resource,\n        span_type=SpanTypes.WEB,\n    )\n    sample_rate = config.sanic.get_analytics_sample_rate(use_global_config=True)\n    if sample_rate is not None:\n        span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)\n\n    method = request.method\n    url = \"{scheme}://{host}{path}\".format(scheme=request.scheme, host=request.host, path=request.path)\n    query_string = request.query_string\n    if isinstance(query_string, bytes):\n        query_string = query_string.decode()\n    trace_utils.set_http_meta(span, config.sanic, method=method, url=url, query=query_string, request_headers=headers)\n\n    if write_callback is not None:\n        write_callback = _wrap_response_callback(span, write_callback)\n    if stream_callback is not None:\n        stream_callback = _wrap_response_callback(span, stream_callback)\n\n    return await wrapped(request, write_callback, stream_callback, **kwargs)\n", "path": "ddtrace/contrib/sanic/patch.py"}], "after_files": [{"content": "import asyncio\nimport ddtrace\nimport sanic\nfrom ddtrace import config\nfrom ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY\nfrom ddtrace.ext import SpanTypes\nfrom ddtrace.propagation.http import HTTPPropagator\nfrom ddtrace.utils.wrappers import unwrap as _u\nfrom ddtrace.vendor import wrapt\nfrom ddtrace.vendor.wrapt import wrap_function_wrapper as _w\n\nfrom .. import trace_utils\nfrom ...internal.logger import get_logger\n\nlog = get_logger(__name__)\n\nconfig._add(\"sanic\", dict(_default_service=\"sanic\", distributed_tracing=True))\n\n\ndef _wrap_response_callback(span, callback):\n    # wrap response callbacks (either sync or async function) to set span tags\n    # based on response and finish span before returning response\n\n    def update_span(response):\n        if isinstance(response, sanic.response.BaseHTTPResponse):\n            status_code = response.status\n            response_headers = response.headers\n        else:\n            # invalid response causes ServerError exception which must be handled\n            status_code = 500\n            response_headers = None\n        trace_utils.set_http_meta(span, config.sanic, status_code=status_code, response_headers=response_headers)\n        span.finish()\n\n    @wrapt.function_wrapper\n    def wrap_sync(wrapped, instance, args, kwargs):\n        r = wrapped(*args, **kwargs)\n        response = args[0]\n        update_span(response)\n        return r\n\n    @wrapt.function_wrapper\n    async def wrap_async(wrapped, instance, args, kwargs):\n        r = await wrapped(*args, **kwargs)\n        response = args[0]\n        update_span(response)\n        return r\n\n    if asyncio.iscoroutinefunction(callback):\n        return wrap_async(callback)\n\n    return wrap_sync(callback)\n\n\ndef _get_path(request):\n    \"\"\"Get path and replace path parameter values with names if route exists.\"\"\"\n    path = request.path\n    try:\n        match_info = request.match_info\n    except sanic.exceptions.SanicException:\n        return path\n    for key, value in match_info.items():\n        path = path.replace(value, f\"<{key}>\")\n    return path\n\n\ndef patch():\n    \"\"\"Patch the instrumented methods.\"\"\"\n    if getattr(sanic, \"__datadog_patch\", False):\n        return\n    setattr(sanic, \"__datadog_patch\", True)\n    _w(\"sanic\", \"Sanic.handle_request\", patch_handle_request)\n\n\ndef unpatch():\n    \"\"\"Unpatch the instrumented methods.\"\"\"\n    _u(sanic.Sanic, \"handle_request\")\n    if not getattr(sanic, \"__datadog_patch\", False):\n        return\n    setattr(sanic, \"__datadog_patch\", False)\n\n\nasync def patch_handle_request(wrapped, instance, args, kwargs):\n    \"\"\"Wrapper for Sanic.handle_request\"\"\"\n    request = kwargs.get(\"request\", args[0])\n    write_callback = kwargs.get(\"write_callback\", args[1])\n    stream_callback = kwargs.get(\"stream_callback\", args[2])\n\n    if request.scheme not in (\"http\", \"https\"):\n        return await wrapped(request, write_callback, stream_callback, **kwargs)\n\n    resource = \"{} {}\".format(request.method, _get_path(request))\n\n    headers = request.headers.copy()\n\n    if config.sanic.distributed_tracing:\n        propagator = HTTPPropagator()\n        context = propagator.extract(headers)\n        if context.trace_id:\n            ddtrace.tracer.context_provider.activate(context)\n\n    span = ddtrace.tracer.trace(\n        \"sanic.request\",\n        service=trace_utils.int_service(None, config.sanic),\n        resource=resource,\n        span_type=SpanTypes.WEB,\n    )\n    sample_rate = config.sanic.get_analytics_sample_rate(use_global_config=True)\n    if sample_rate is not None:\n        span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)\n\n    method = request.method\n    url = \"{scheme}://{host}{path}\".format(scheme=request.scheme, host=request.host, path=request.path)\n    query_string = request.query_string\n    if isinstance(query_string, bytes):\n        query_string = query_string.decode()\n    trace_utils.set_http_meta(span, config.sanic, method=method, url=url, query=query_string, request_headers=headers)\n\n    if write_callback is not None:\n        write_callback = _wrap_response_callback(span, write_callback)\n    if stream_callback is not None:\n        stream_callback = _wrap_response_callback(span, stream_callback)\n\n    return await wrapped(request, write_callback, stream_callback, **kwargs)\n", "path": "ddtrace/contrib/sanic/patch.py"}]}