Skip to content

Commit a013b35

Browse files
mabdinurP403n1x87
andauthored
fix(sql): use psycopg's parse_dsn when available (backport #5927 to 1.13) (#5964)
Backports #5927 + backports a [refactor](082e1e1) required by this fix ## 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 820fffd commit a013b35

File tree

8 files changed

+88
-4
lines changed

8 files changed

+88
-4
lines changed

ddtrace/bootstrap/sitecustomize.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"""
55
from ddtrace import LOADED_MODULES # isort:skip
66

7-
from functools import partial # noqa
87
import logging # noqa
98
import os # noqa
109
import sys
@@ -148,7 +147,7 @@ def drop(module_name):
148147
# to the newly imported threading module to allow it to retrieve the correct
149148
# thread object information, like the thread name. We register a post-import
150149
# hook on the threading module to perform this update.
151-
@partial(ModuleWatchdog.register_module_hook, "threading")
150+
@ModuleWatchdog.after_module_imported("threading")
152151
def _(threading):
153152
logging.threading = threading
154153

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):

ddtrace/internal/module.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,15 @@ def unregister_module_hook(cls, module, hook):
525525
except ValueError:
526526
raise ValueError("Hook %r not registered for module %r" % (hook, module))
527527

528+
@classmethod
529+
def after_module_imported(cls, module):
530+
# type: (str) -> Callable[[ModuleHookType], None]
531+
def _(hook):
532+
# type: (ModuleHookType) -> None
533+
cls.register_module_hook(module, hook)
534+
535+
return _
536+
528537
@classmethod
529538
def register_pre_exec_module_hook(cls, cond, hook):
530539
# type: (Type[ModuleWatchdog], PreExecHookCond, PreExecHookType) -> None

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
@@ -136,6 +136,15 @@ def assert_conn_is_traced(self, db, service):
136136
self.assertIsNone(root.get_tag("sql.query"))
137137
self.reset()
138138

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

tests/contrib/psycopg2/test_psycopg.py

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

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

tests/internal/test_module.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ def test_import_module_hook_for_imported_module(module_watchdog):
7373
hook.assert_called_once_with(module)
7474

7575

76+
def test_after_module_imported_decorator(module_watchdog):
77+
hook = mock.Mock()
78+
module = sys.modules[__name__]
79+
module_watchdog.after_module_imported(module.__name__)(hook)
80+
81+
hook.assert_called_once_with(module)
82+
83+
7684
def test_register_hook_without_install():
7785
with pytest.raises(RuntimeError):
7886
ModuleWatchdog.register_origin_hook(__file__, mock.Mock())

0 commit comments

Comments
 (0)