Skip to content

Commit fe520a0

Browse files
mabdinurP403n1x87
andauthored
fix(sql): use psycopg's parse_dsn when available (backport #5927 to 1.14) (#5965)
Backports #5927 ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines) are followed. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. Co-authored-by: Gabriele N. Tornetta <[email protected]>
1 parent 3adcf00 commit fe520a0

File tree

5 files changed

+70
-2
lines changed

5 files changed

+70
-2
lines changed

ddtrace/ext/sql.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from typing import Dict
22

33
from ddtrace.internal.compat import ensure_pep562
4+
from ddtrace.internal.logger import get_logger
5+
from ddtrace.internal.module import ModuleWatchdog
46
from ddtrace.vendor.debtcollector import deprecate
57

68

9+
log = get_logger(__name__)
10+
711
# tags
812
QUERY = "sql.query" # the query text
913
ROWS = "sql.rows" # number of rows returned by a query
@@ -23,14 +27,56 @@ def normalize_vendor(vendor):
2327
return vendor
2428

2529

26-
def parse_pg_dsn(dsn):
30+
def _dd_parse_pg_dsn(dsn):
2731
# type: (str) -> Dict[str, str]
2832
"""
2933
Return a dictionary of the components of a postgres DSN.
3034
>>> parse_pg_dsn('user=dog port=1543 dbname=dogdata')
3135
{'user':'dog', 'port':'1543', 'dbname':'dogdata'}
3236
"""
33-
return dict(_.split("=", 1) for _ in dsn.split())
37+
dsn_dict = dict()
38+
try:
39+
# Provides a default implementation for parsing DSN strings.
40+
# The following is an example of a valid DSN string that fails to be parsed:
41+
# "db=moon user=ears options='-c statement_timeout=1000 -c lock_timeout=250'"
42+
dsn_dict = dict(_.split("=", 1) for _ in dsn.split())
43+
except Exception:
44+
log.debug("Failed to parse postgres dsn connection", exc_info=True)
45+
return dsn_dict
46+
47+
48+
# Do not import from psycopg directly! This reference will be updated at runtime to use
49+
# a better implementation that is provided by the psycopg library.
50+
# This is done to avoid circular imports.
51+
parse_pg_dsn = _dd_parse_pg_dsn
52+
53+
54+
@ModuleWatchdog.after_module_imported("psycopg2")
55+
def use_psycopg2_parse_dsn(psycopg_module):
56+
"""Replaces parse_pg_dsn with the helper function defined in psycopg2"""
57+
global parse_pg_dsn
58+
59+
try:
60+
from psycopg2.extensions import parse_dsn
61+
62+
parse_pg_dsn = parse_dsn
63+
except ImportError:
64+
# Best effort, we'll use our own parser: _dd_parse_pg_dsn
65+
pass
66+
67+
68+
@ModuleWatchdog.after_module_imported("psycopg")
69+
def use_psycopg3_parse_dsn(psycopg_module):
70+
"""Replaces parse_pg_dsn with the helper function defined in psycopg3"""
71+
global parse_pg_dsn
72+
73+
try:
74+
from psycopg.conninfo import conninfo_to_dict
75+
76+
parse_pg_dsn = conninfo_to_dict
77+
except ImportError:
78+
# Best effort, we'll use our own parser: _dd_parse_pg_dsn
79+
pass
3480

3581

3682
def __getattr__(name):

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ doctest
7272
dogpile
7373
dogpile.cache
7474
dogstatsd
75+
dsn
7576
elasticsearch
7677
elasticsearch1
7778
elasticsearch7
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fixes:
2+
- |
3+
psycopg: Fixes ``ValueError`` raised when dsn connection strings are parsed. This was fixed in ddtrace v1.9.0 and was re-introduced in v1.13.0.

tests/contrib/psycopg/test_psycopg.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ def assert_conn_is_traced(self, db, service):
137137
self.assertIsNone(root.get_tag("sql.query"))
138138
self.reset()
139139

140+
def test_psycopg3_connection_with_string(self):
141+
# Regression test for DataDog/dd-trace-py/issues/5926
142+
configs_arr = ["{}={}".format(k, v) for k, v in POSTGRES_CONFIG.items()]
143+
configs_arr.append("options='-c statement_timeout=1000 -c lock_timeout=250'")
144+
conn = psycopg.connect(" ".join(configs_arr))
145+
146+
Pin.get_from(conn).clone(service="postgres", tracer=self.tracer).onto(conn)
147+
self.assert_conn_is_traced(conn, "postgres")
148+
140149
def test_opentracing_propagation(self):
141150
# ensure OpenTracing plays well with our integration
142151
query = """SELECT 'tracing'"""

tests/contrib/psycopg2/test_psycopg.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ def assert_conn_is_traced(self, db, service):
143143
self.assertIsNone(root.get_tag("sql.query"))
144144
self.reset()
145145

146+
def test_psycopg2_connection_with_string(self):
147+
# Regression test for DataDog/dd-trace-py/issues/5926
148+
configs_arr = ["{}={}".format(k, v) for k, v in POSTGRES_CONFIG.items()]
149+
configs_arr.append("options='-c statement_timeout=1000 -c lock_timeout=250'")
150+
conn = psycopg2.connect(" ".join(configs_arr))
151+
152+
Pin.get_from(conn).clone(service="postgres", tracer=self.tracer).onto(conn)
153+
self.assert_conn_is_traced(conn, "postgres")
154+
146155
def test_opentracing_propagation(self):
147156
# ensure OpenTracing plays well with our integration
148157
query = """SELECT 'tracing'"""

0 commit comments

Comments
 (0)