Repository: DataDog/dd-trace-py

Issue:
Bug with version 1.9.*
<!--
Thanks for taking the time for reporting an issue!

Before reporting an issue on dd-trace-py, please be sure to provide all
necessary information.

If you're hitting a bug, make sure that you're using the latest version of this
library.
-->

### Summary of problem
We have a python lambda with an agent of DataDog, that started to fail 3 days ago.
After investigation we found that the lat deployment of the lambda increased the ddtrace version to the latest.
There is a bug in ddtrace.ext.sql.parse_pg_dsn, it fails on a piece of code:
`
psycopg2.connect(host="<HOST>",
                                 database="<DATABASE>"
                                 user="<USERNAME>",
                                 password="<TOKEN>",
                                 sslmode="verify-ca",
                                 sslrootcert="<CERT_PATH>",
                                 options=f'-c search_path=<SCHEMA> -c statement_timeout=0',
)
`
The bug is that when it parses options, it parses it as follows:
`
dict(_.split("=", 1) for _ in dsn.split())
` 
However the first split returns a `-c` part (without a "=" literal), for which the second split returns a single element that can't be a dict element (<key, value>).

This kind of error is thrown
**ValueError: dictionary update sequence element #0 has length 1; 2 is required**

In version 1.8.0 the parsing was imported from psycopg2 (that is why it worked fine):
`
try:
    from psycopg2.extensions import parse_dsn as parse_pg_dsn
except ImportError:

    def parse_pg_dsn(dsn):
        # type: (str) -> Dict[str, str]
        """
        Return a dictionary of the components of a postgres DSN.
        >>> parse_pg_dsn('user=dog port=1543 dbname=dogdata')
        {'user':'dog', 'port':'1543', 'dbname':'dogdata'}
        """
        return dict(_.split("=", maxsplit=1) for _ in dsn.split())
`


### Which version of dd-trace-py are you using?
1.9.0, and also with 1.9.2 we got the same issue

### Which version of pip are you using?
23.0.1

### Which libraries and their versions are you using?

<details>
  <summary>`pip freeze`</summary>
  <!-- Paste output of pip freeze here -->
aiohttp==3.8.4
aiosignal==1.3.1
arrow==1.2.3
async-timeout==4.0.2
attrs==22.2.0
beautifulsoup4==4.10.0
boto3==1.26.82
botocore==1.29.82
bump2version==1.0.1
bytecode==0.14.0
cattrs==22.2.0
certifi==2022.12.7
charset-normalizer==3.0.1
click==8.1.3
cramjam==2.6.2
Cython==0.29.33
datadog==0.44.0
datadog-lambda==4.68.0
datefinder==0.7.3
ddsketch==2.0.4
ddtrace==1.9.2
elasticsearch==7.15.1
envier==0.4.0
exceptiongroup==1.1.0
fastparquet==2023.2.0
frozenlist==1.3.3
fsspec==2023.1.0
ftfy==6.1.1
geographiclib==1.52
geopy==2.2.0
h3==3.7.6
idna==3.4
importlib-resources==5.12.0
iniconfig==2.0.0
Jinja2==3.0.3
jmespath==1.0.1
joblib==1.2.0
jsonschema==4.17.3
MarkupSafe==2.1.2
mock==5.0.1
more-itertools==9.0.0
multidict==6.0.4
mypy-boto3-s3==1.26.62
navina-dev-utils @ git+https://github.com/Navina-ai/navina_dev_utils.git@f348db2aaf753bda24517ad5554df91fb6707409
navina-utils @ git+https://github.com/Navina-ai/navina_utils.git@2eebe3dc65acc6ef714090a9329d2f5af2a98f61
nltk==3.8.1
numpy==1.24.2
opensearch-py==2.2.0
packaging==23.0
pandas==1.5.3
pgeocode==0.4.0
Pillow==8.3.2
pkgutil_resolve_name==1.3.10
pluggy==1.0.0
protobuf==4.22.0
psycopg2-binary==2.9.5
pyarrow==11.0.0
pydantic==1.10.5
PyPDF2==1.26.0
pyrsistent==0.19.3
pytest==7.2.1
python-dateutil==2.8.2
python-dotenv==1.0.0
python-json-logger==2.0.7
pytz==2021.3
regex==2022.10.31
requests==2.28.2
requests-aws4auth==1.2.2
s3transfer==0.6.0
scikit-learn==1.2.1
scipy==1.10.1
six==1.16.0
sklearn==0.0
soupsieve==2.4
SQLAlchemy==2.0.4
statsd==4.0.1
tenacity==8.2.2
tfidf-matcher==0.2.1
threadpoolctl==3.1.0
timezonefinder==6.0.2
tomli==2.0.1
tqdm==4.64.1
typing_extensions==4.5.0
urllib3==1.26.14
watchtower==1.0.6
wcwidth==0.2.6
wrapt==1.15.0
xmltodict==0.13.0
yarl==1.8.2
zipp==3.15.0
</details>

### How can we reproduce your problem?
just run the function with the bug on the input I gave as example (change all <VALUE> with mocks

### What is the result that you get?
**ValueError: dictionary update sequence element #0 has length 1; 2 is required**

### What is the result that you expected?
I would expect my lambda to work



Discussion:

Resolving patch:
diff --git a/ddtrace/contrib/psycopg/connection.py b/ddtrace/contrib/psycopg/connection.py
--- a/ddtrace/contrib/psycopg/connection.py
+++ b/ddtrace/contrib/psycopg/connection.py
@@ -5,6 +5,7 @@
 
 from psycopg2.extensions import connection
 from psycopg2.extensions import cursor
+from psycopg2.extensions import parse_dsn
 
 from ddtrace import config
 from ddtrace.internal.constants import COMPONENT
@@ -13,7 +14,6 @@
 from ...ext import SpanTypes
 from ...ext import db
 from ...ext import net
-from ...ext import sql
 
 
 class TracedCursor(cursor):
@@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs):
         super(TracedConnection, self).__init__(*args, **kwargs)
 
         # add metadata (from the connection, string, etc)
-        dsn = sql.parse_pg_dsn(self.dsn)
+        dsn = parse_dsn(self.dsn)
         self._datadog_tags = {
             net.TARGET_HOST: dsn.get("host"),
             net.TARGET_PORT: dsn.get("port"),
diff --git a/ddtrace/contrib/psycopg/patch.py b/ddtrace/contrib/psycopg/patch.py
--- a/ddtrace/contrib/psycopg/patch.py
+++ b/ddtrace/contrib/psycopg/patch.py
@@ -1,6 +1,7 @@
 import os
 
 import psycopg2
+from psycopg2.extensions import parse_dsn
 from psycopg2.sql import Composable
 
 from ddtrace import Pin
@@ -11,7 +12,6 @@
 from ddtrace.ext import SpanTypes
 from ddtrace.ext import db
 from ddtrace.ext import net
-from ddtrace.ext import sql
 from ddtrace.internal.constants import COMPONENT
 from ddtrace.vendor import wrapt
 
@@ -105,7 +105,7 @@ def patch_conn(conn, traced_conn_cls=Psycopg2TracedConnection):
     c = traced_conn_cls(conn)
 
     # fetch tags from the dsn
-    dsn = sql.parse_pg_dsn(conn.dsn)
+    dsn = parse_dsn(conn.dsn)
     tags = {
         net.TARGET_HOST: dsn.get("host"),
         net.TARGET_PORT: dsn.get("port"),


Test patch: