prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <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
  
  </issue>
  <code>
  [start of ddtrace/contrib/sanic/patch.py]
  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 of ddtrace/contrib/sanic/patch.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: 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()
  
verification_info: {"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"}]}