prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Psycopg patching doesn't properly handle execute_values
  The `execute_values` extension in psycopg2 composes and executes the query with b-string, even if you passed the query as a string. Below is the full function from psycopg2.extras
  
  ```python
  def execute_values(cur, sql, argslist, template=None, page_size=100, fetch=False):
      from psycopg2.sql import Composable
      if isinstance(sql, Composable):
          sql = sql.as_string(cur)
  
      # we can't just use sql % vals because vals is bytes: if sql is bytes
      # there will be some decoding error because of stupid codec used, and Py3
      # doesn't implement % on bytes.
      if not isinstance(sql, bytes):
          sql = sql.encode(_ext.encodings[cur.connection.encoding])
      pre, post = _split_sql(sql)
  
      result = [] if fetch else None
      for page in _paginate(argslist, page_size=page_size):
          if template is None:
              template = b'(' + b','.join([b'%s'] * len(page[0])) + b')'
          parts = pre[:]
          for args in page:
              parts.append(cur.mogrify(template, args))
              parts.append(b',')
          parts[-1:] = post
          cur.execute(b''.join(parts))
          if fetch:
              result.extend(cur.fetchall())
  
      return result
  ```
  
  The problem is that ddtrace assumes that the "resource" added to a span is a string. The result is that when `span.finish()` is called in the datadog lambda handler and it tries to serialize the span to json, it blows up with "TypeError: Object of type bytes is not JSON serializable".  Upon investigation, I discovered that the ddtrace.internal.encoder.py's JSONEncoder just does a simple json.dumps() on all the spans and the `resource` attribute on the span from the using `execute_values` is bytes, not a string.
  
  I think the solution here is simply to update the Psycopg2TracedCursor class to decode the resource from bytes if it is bytes, like this:
  
  ```python
  class Psycopg2TracedCursor(dbapi.TracedCursor):
      """TracedCursor for psycopg2"""
  
      def _trace_method(self, method, name, resource, extra_tags, *args, **kwargs):
          # treat psycopg2.sql.Composable resource objects as strings
          if isinstance(resource, Composable):
              resource = resource.as_string(self.__wrapped__)
          # THIS IS THE NEW PART BELOW (next 2 lines)
          if isinstance(resource, bytes):
              resource = resource.decode('utf-8')
          return super(Psycopg2TracedCursor, self)._trace_method(method, name, resource, extra_tags, *args, **kwargs)
  ```
  
  ### Which version of dd-trace-py are you using?
  Lambda layer, v50.
  ### Which version of pip are you using?
  n/a
  
  ### How can we reproduce your problem?
  Use `execute_values` while inside a tracing context. It should have a 100% failure rate.
  
  ### What is the result that you get?
  A type error when span.finish() is called and the metrics are furnished to DD.
  
  ### What is the result that you expected?
  It should work as normal, with the resource decoded as a string.
  
  
  --- 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/internal/encoding.py`
  Content:
  ```
  1 import json
  2 from typing import Any
  3 from typing import Dict
  4 from typing import List
  5 from typing import Optional
  6 from typing import TYPE_CHECKING
  7 
  8 from ._encoding import ListStringTable
  9 from ._encoding import MsgpackEncoderV03
  10 from ._encoding import MsgpackEncoderV05
  11 from .logger import get_logger
  12 
  13 
  14 __all__ = ["MsgpackEncoderV03", "MsgpackEncoderV05", "ListStringTable", "MSGPACK_ENCODERS"]
  15 
  16 
  17 if TYPE_CHECKING:
  18     from ..span import Span
  19 
  20 
  21 log = get_logger(__name__)
  22 
  23 
  24 class _EncoderBase(object):
  25     """
  26     Encoder interface that provides the logic to encode traces and service.
  27     """
  28 
  29     def encode_traces(self, traces):
  30         # type: (List[List[Span]]) -> str
  31         """
  32         Encodes a list of traces, expecting a list of items where each items
  33         is a list of spans. Before dumping the string in a serialized format all
  34         traces are normalized according to the encoding format. The trace
  35         nesting is not changed.
  36 
  37         :param traces: A list of traces that should be serialized
  38         """
  39         raise NotImplementedError()
  40 
  41     def encode(self, obj):
  42         # type: (List[List[Any]]) -> str
  43         """
  44         Defines the underlying format used during traces or services encoding.
  45         This method must be implemented and should only be used by the internal
  46         functions.
  47         """
  48         raise NotImplementedError()
  49 
  50 
  51 class JSONEncoder(_EncoderBase):
  52     content_type = "application/json"
  53 
  54     def encode_traces(self, traces):
  55         normalized_traces = [[span.to_dict() for span in trace] for trace in traces]
  56         return self.encode(normalized_traces)
  57 
  58     @staticmethod
  59     def encode(obj):
  60         # type: (Any) -> str
  61         return json.dumps(obj)
  62 
  63 
  64 class JSONEncoderV2(JSONEncoder):
  65     """
  66     JSONEncoderV2 encodes traces to the new intake API format.
  67     """
  68 
  69     content_type = "application/json"
  70 
  71     def encode_traces(self, traces):
  72         # type: (List[List[Span]]) -> str
  73         normalized_traces = [[JSONEncoderV2._convert_span(span) for span in trace] for trace in traces]
  74         return self.encode({"traces": normalized_traces})
  75 
  76     @staticmethod
  77     def _convert_span(span):
  78         # type: (Span) -> Dict[str, Any]
  79         sp = span.to_dict()
  80         sp["trace_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("trace_id"))
  81         sp["parent_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("parent_id"))
  82         sp["span_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("span_id"))
  83         return sp
  84 
  85     @staticmethod
  86     def _encode_id_to_hex(dd_id):
  87         # type: (Optional[int]) -> str
  88         if not dd_id:
  89             return "0000000000000000"
  90         return "%0.16X" % int(dd_id)
  91 
  92     @staticmethod
  93     def _decode_id_to_hex(hex_id):
  94         # type: (Optional[str]) -> int
  95         if not hex_id:
  96             return 0
  97         return int(hex_id, 16)
  98 
  99 
  100 MSGPACK_ENCODERS = {
  101     "v0.3": MsgpackEncoderV03,
  102     "v0.4": MsgpackEncoderV03,
  103     "v0.5": MsgpackEncoderV05,
  104 }
  105
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/ddtrace/internal/encoding.py b/ddtrace/internal/encoding.py
  --- a/ddtrace/internal/encoding.py
  +++ b/ddtrace/internal/encoding.py
  @@ -8,6 +8,9 @@
   from ._encoding import ListStringTable
   from ._encoding import MsgpackEncoderV03
   from ._encoding import MsgpackEncoderV05
  +from .compat import PY3
  +from .compat import binary_type
  +from .compat import ensure_text
   from .logger import get_logger
   
   
  @@ -48,17 +51,33 @@
           raise NotImplementedError()
   
   
  -class JSONEncoder(_EncoderBase):
  +class JSONEncoder(json.JSONEncoder, _EncoderBase):
       content_type = "application/json"
   
       def encode_traces(self, traces):
  -        normalized_traces = [[span.to_dict() for span in trace] for trace in traces]
  +        normalized_traces = [[JSONEncoder._normalize_span(span.to_dict()) for span in trace] for trace in traces]
           return self.encode(normalized_traces)
   
       @staticmethod
  -    def encode(obj):
  -        # type: (Any) -> str
  -        return json.dumps(obj)
  +    def _normalize_span(span):
  +        # Ensure all string attributes are actually strings and not bytes
  +        # DEV: We are deferring meta/metrics to reduce any performance issues.
  +        #      Meta/metrics may still contain `bytes` and have encoding issues.
  +        span["resource"] = JSONEncoder._normalize_str(span["resource"])
  +        span["name"] = JSONEncoder._normalize_str(span["name"])
  +        span["service"] = JSONEncoder._normalize_str(span["service"])
  +        return span
  +
  +    @staticmethod
  +    def _normalize_str(obj):
  +        if obj is None:
  +            return obj
  +
  +        if PY3:
  +            return ensure_text(obj, errors="backslashreplace")
  +        elif isinstance(obj, binary_type):
  +            return obj.decode("utf-8", errors="replace")
  +        return obj
   
   
   class JSONEncoderV2(JSONEncoder):
  @@ -77,6 +96,7 @@
       def _convert_span(span):
           # type: (Span) -> Dict[str, Any]
           sp = span.to_dict()
  +        sp = JSONEncoderV2._normalize_span(sp)
           sp["trace_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("trace_id"))
           sp["parent_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("parent_id"))
           sp["span_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("span_id"))
  
verification_info: {"golden_diff": "diff --git a/ddtrace/internal/encoding.py b/ddtrace/internal/encoding.py\n--- a/ddtrace/internal/encoding.py\n+++ b/ddtrace/internal/encoding.py\n@@ -8,6 +8,9 @@\n from ._encoding import ListStringTable\n from ._encoding import MsgpackEncoderV03\n from ._encoding import MsgpackEncoderV05\n+from .compat import PY3\n+from .compat import binary_type\n+from .compat import ensure_text\n from .logger import get_logger\n \n \n@@ -48,17 +51,33 @@\n         raise NotImplementedError()\n \n \n-class JSONEncoder(_EncoderBase):\n+class JSONEncoder(json.JSONEncoder, _EncoderBase):\n     content_type = \"application/json\"\n \n     def encode_traces(self, traces):\n-        normalized_traces = [[span.to_dict() for span in trace] for trace in traces]\n+        normalized_traces = [[JSONEncoder._normalize_span(span.to_dict()) for span in trace] for trace in traces]\n         return self.encode(normalized_traces)\n \n     @staticmethod\n-    def encode(obj):\n-        # type: (Any) -> str\n-        return json.dumps(obj)\n+    def _normalize_span(span):\n+        # Ensure all string attributes are actually strings and not bytes\n+        # DEV: We are deferring meta/metrics to reduce any performance issues.\n+        #      Meta/metrics may still contain `bytes` and have encoding issues.\n+        span[\"resource\"] = JSONEncoder._normalize_str(span[\"resource\"])\n+        span[\"name\"] = JSONEncoder._normalize_str(span[\"name\"])\n+        span[\"service\"] = JSONEncoder._normalize_str(span[\"service\"])\n+        return span\n+\n+    @staticmethod\n+    def _normalize_str(obj):\n+        if obj is None:\n+            return obj\n+\n+        if PY3:\n+            return ensure_text(obj, errors=\"backslashreplace\")\n+        elif isinstance(obj, binary_type):\n+            return obj.decode(\"utf-8\", errors=\"replace\")\n+        return obj\n \n \n class JSONEncoderV2(JSONEncoder):\n@@ -77,6 +96,7 @@\n     def _convert_span(span):\n         # type: (Span) -> Dict[str, Any]\n         sp = span.to_dict()\n+        sp = JSONEncoderV2._normalize_span(sp)\n         sp[\"trace_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"trace_id\"))\n         sp[\"parent_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"parent_id\"))\n         sp[\"span_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"span_id\"))\n", "issue": "Psycopg patching doesn't properly handle execute_values\nThe `execute_values` extension in psycopg2 composes and executes the query with b-string, even if you passed the query as a string. Below is the full function from psycopg2.extras\r\n\r\n```python\r\ndef execute_values(cur, sql, argslist, template=None, page_size=100, fetch=False):\r\n    from psycopg2.sql import Composable\r\n    if isinstance(sql, Composable):\r\n        sql = sql.as_string(cur)\r\n\r\n    # we can't just use sql % vals because vals is bytes: if sql is bytes\r\n    # there will be some decoding error because of stupid codec used, and Py3\r\n    # doesn't implement % on bytes.\r\n    if not isinstance(sql, bytes):\r\n        sql = sql.encode(_ext.encodings[cur.connection.encoding])\r\n    pre, post = _split_sql(sql)\r\n\r\n    result = [] if fetch else None\r\n    for page in _paginate(argslist, page_size=page_size):\r\n        if template is None:\r\n            template = b'(' + b','.join([b'%s'] * len(page[0])) + b')'\r\n        parts = pre[:]\r\n        for args in page:\r\n            parts.append(cur.mogrify(template, args))\r\n            parts.append(b',')\r\n        parts[-1:] = post\r\n        cur.execute(b''.join(parts))\r\n        if fetch:\r\n            result.extend(cur.fetchall())\r\n\r\n    return result\r\n```\r\n\r\nThe problem is that ddtrace assumes that the \"resource\" added to a span is a string. The result is that when `span.finish()` is called in the datadog lambda handler and it tries to serialize the span to json, it blows up with \"TypeError: Object of type bytes is not JSON serializable\".  Upon investigation, I discovered that the ddtrace.internal.encoder.py's JSONEncoder just does a simple json.dumps() on all the spans and the `resource` attribute on the span from the using `execute_values` is bytes, not a string.\r\n\r\nI think the solution here is simply to update the Psycopg2TracedCursor class to decode the resource from bytes if it is bytes, like this:\r\n\r\n```python\r\nclass Psycopg2TracedCursor(dbapi.TracedCursor):\r\n    \"\"\"TracedCursor for psycopg2\"\"\"\r\n\r\n    def _trace_method(self, method, name, resource, extra_tags, *args, **kwargs):\r\n        # treat psycopg2.sql.Composable resource objects as strings\r\n        if isinstance(resource, Composable):\r\n            resource = resource.as_string(self.__wrapped__)\r\n        # THIS IS THE NEW PART BELOW (next 2 lines)\r\n        if isinstance(resource, bytes):\r\n            resource = resource.decode('utf-8')\r\n        return super(Psycopg2TracedCursor, self)._trace_method(method, name, resource, extra_tags, *args, **kwargs)\r\n```\r\n\r\n### Which version of dd-trace-py are you using?\r\nLambda layer, v50.\r\n### Which version of pip are you using?\r\nn/a\r\n\r\n### How can we reproduce your problem?\r\nUse `execute_values` while inside a tracing context. It should have a 100% failure rate.\r\n\r\n### What is the result that you get?\r\nA type error when span.finish() is called and the metrics are furnished to DD.\r\n\r\n### What is the result that you expected?\r\nIt should work as normal, with the resource decoded as a string.\r\n\n", "before_files": [{"content": "import json\nfrom typing import Any\nfrom typing import Dict\nfrom typing import List\nfrom typing import Optional\nfrom typing import TYPE_CHECKING\n\nfrom ._encoding import ListStringTable\nfrom ._encoding import MsgpackEncoderV03\nfrom ._encoding import MsgpackEncoderV05\nfrom .logger import get_logger\n\n\n__all__ = [\"MsgpackEncoderV03\", \"MsgpackEncoderV05\", \"ListStringTable\", \"MSGPACK_ENCODERS\"]\n\n\nif TYPE_CHECKING:\n    from ..span import Span\n\n\nlog = get_logger(__name__)\n\n\nclass _EncoderBase(object):\n    \"\"\"\n    Encoder interface that provides the logic to encode traces and service.\n    \"\"\"\n\n    def encode_traces(self, traces):\n        # type: (List[List[Span]]) -> str\n        \"\"\"\n        Encodes a list of traces, expecting a list of items where each items\n        is a list of spans. Before dumping the string in a serialized format all\n        traces are normalized according to the encoding format. The trace\n        nesting is not changed.\n\n        :param traces: A list of traces that should be serialized\n        \"\"\"\n        raise NotImplementedError()\n\n    def encode(self, obj):\n        # type: (List[List[Any]]) -> str\n        \"\"\"\n        Defines the underlying format used during traces or services encoding.\n        This method must be implemented and should only be used by the internal\n        functions.\n        \"\"\"\n        raise NotImplementedError()\n\n\nclass JSONEncoder(_EncoderBase):\n    content_type = \"application/json\"\n\n    def encode_traces(self, traces):\n        normalized_traces = [[span.to_dict() for span in trace] for trace in traces]\n        return self.encode(normalized_traces)\n\n    @staticmethod\n    def encode(obj):\n        # type: (Any) -> str\n        return json.dumps(obj)\n\n\nclass JSONEncoderV2(JSONEncoder):\n    \"\"\"\n    JSONEncoderV2 encodes traces to the new intake API format.\n    \"\"\"\n\n    content_type = \"application/json\"\n\n    def encode_traces(self, traces):\n        # type: (List[List[Span]]) -> str\n        normalized_traces = [[JSONEncoderV2._convert_span(span) for span in trace] for trace in traces]\n        return self.encode({\"traces\": normalized_traces})\n\n    @staticmethod\n    def _convert_span(span):\n        # type: (Span) -> Dict[str, Any]\n        sp = span.to_dict()\n        sp[\"trace_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"trace_id\"))\n        sp[\"parent_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"parent_id\"))\n        sp[\"span_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"span_id\"))\n        return sp\n\n    @staticmethod\n    def _encode_id_to_hex(dd_id):\n        # type: (Optional[int]) -> str\n        if not dd_id:\n            return \"0000000000000000\"\n        return \"%0.16X\" % int(dd_id)\n\n    @staticmethod\n    def _decode_id_to_hex(hex_id):\n        # type: (Optional[str]) -> int\n        if not hex_id:\n            return 0\n        return int(hex_id, 16)\n\n\nMSGPACK_ENCODERS = {\n    \"v0.3\": MsgpackEncoderV03,\n    \"v0.4\": MsgpackEncoderV03,\n    \"v0.5\": MsgpackEncoderV05,\n}\n", "path": "ddtrace/internal/encoding.py"}], "after_files": [{"content": "import json\nfrom typing import Any\nfrom typing import Dict\nfrom typing import List\nfrom typing import Optional\nfrom typing import TYPE_CHECKING\n\nfrom ._encoding import ListStringTable\nfrom ._encoding import MsgpackEncoderV03\nfrom ._encoding import MsgpackEncoderV05\nfrom .compat import PY3\nfrom .compat import binary_type\nfrom .compat import ensure_text\nfrom .logger import get_logger\n\n\n__all__ = [\"MsgpackEncoderV03\", \"MsgpackEncoderV05\", \"ListStringTable\", \"MSGPACK_ENCODERS\"]\n\n\nif TYPE_CHECKING:\n    from ..span import Span\n\n\nlog = get_logger(__name__)\n\n\nclass _EncoderBase(object):\n    \"\"\"\n    Encoder interface that provides the logic to encode traces and service.\n    \"\"\"\n\n    def encode_traces(self, traces):\n        # type: (List[List[Span]]) -> str\n        \"\"\"\n        Encodes a list of traces, expecting a list of items where each items\n        is a list of spans. Before dumping the string in a serialized format all\n        traces are normalized according to the encoding format. The trace\n        nesting is not changed.\n\n        :param traces: A list of traces that should be serialized\n        \"\"\"\n        raise NotImplementedError()\n\n    def encode(self, obj):\n        # type: (List[List[Any]]) -> str\n        \"\"\"\n        Defines the underlying format used during traces or services encoding.\n        This method must be implemented and should only be used by the internal\n        functions.\n        \"\"\"\n        raise NotImplementedError()\n\n\nclass JSONEncoder(json.JSONEncoder, _EncoderBase):\n    content_type = \"application/json\"\n\n    def encode_traces(self, traces):\n        normalized_traces = [[JSONEncoder._normalize_span(span.to_dict()) for span in trace] for trace in traces]\n        return self.encode(normalized_traces)\n\n    @staticmethod\n    def _normalize_span(span):\n        # Ensure all string attributes are actually strings and not bytes\n        # DEV: We are deferring meta/metrics to reduce any performance issues.\n        #      Meta/metrics may still contain `bytes` and have encoding issues.\n        span[\"resource\"] = JSONEncoder._normalize_str(span[\"resource\"])\n        span[\"name\"] = JSONEncoder._normalize_str(span[\"name\"])\n        span[\"service\"] = JSONEncoder._normalize_str(span[\"service\"])\n        return span\n\n    @staticmethod\n    def _normalize_str(obj):\n        if obj is None:\n            return obj\n\n        if PY3:\n            return ensure_text(obj, errors=\"backslashreplace\")\n        elif isinstance(obj, binary_type):\n            return obj.decode(\"utf-8\", errors=\"replace\")\n        return obj\n\n\nclass JSONEncoderV2(JSONEncoder):\n    \"\"\"\n    JSONEncoderV2 encodes traces to the new intake API format.\n    \"\"\"\n\n    content_type = \"application/json\"\n\n    def encode_traces(self, traces):\n        # type: (List[List[Span]]) -> str\n        normalized_traces = [[JSONEncoderV2._convert_span(span) for span in trace] for trace in traces]\n        return self.encode({\"traces\": normalized_traces})\n\n    @staticmethod\n    def _convert_span(span):\n        # type: (Span) -> Dict[str, Any]\n        sp = span.to_dict()\n        sp = JSONEncoderV2._normalize_span(sp)\n        sp[\"trace_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"trace_id\"))\n        sp[\"parent_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"parent_id\"))\n        sp[\"span_id\"] = JSONEncoderV2._encode_id_to_hex(sp.get(\"span_id\"))\n        return sp\n\n    @staticmethod\n    def _encode_id_to_hex(dd_id):\n        # type: (Optional[int]) -> str\n        if not dd_id:\n            return \"0000000000000000\"\n        return \"%0.16X\" % int(dd_id)\n\n    @staticmethod\n    def _decode_id_to_hex(hex_id):\n        # type: (Optional[str]) -> int\n        if not hex_id:\n            return 0\n        return int(hex_id, 16)\n\n\nMSGPACK_ENCODERS = {\n    \"v0.3\": MsgpackEncoderV03,\n    \"v0.4\": MsgpackEncoderV03,\n    \"v0.5\": MsgpackEncoderV05,\n}\n", "path": "ddtrace/internal/encoding.py"}]}