Skip to content

Commit 90c89a5

Browse files
emmettbutlermabdinurZStriker19brettlangdon
authored
fix(dbm): support byte string queries (#5188) [backport to 1.9] (#5198)
Database monitoring and APM Linking only supports unicode sql queries. This PR extends support to sql queries with the type: `bytes`. When `DD_DBM_PROPAGATION_MODE=full` the following code snippet raises an excpetion: ``` import psycopg2 import ddtrace ddtrace.patch(psycopg=True) conn = psycopg2.connect(**POSTGRES_CONFIG) # raises -> `TypeError: can only concatenate str (not "bytes") to str` conn.cursor().execute(b"select 'blah'") ``` ## Checklist - [x] Change(s) are motivated and described in the PR description. Co-authored-by: Munir Abdinur <[email protected]> Co-authored-by: Zachary Groves <[email protected]> Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: ZStriker19 <[email protected]>
1 parent 7356682 commit 90c89a5

File tree

6 files changed

+134
-24
lines changed

6 files changed

+134
-24
lines changed

ddtrace/contrib/psycopg/patch.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
from ...internal.utils.formats import asbool
1919
from ...internal.utils.version import parse_version
2020
from ...propagation._database_monitoring import _DBM_Propagator
21+
from ...propagation._database_monitoring import default_sql_injector as _default_sql_injector
2122

2223

2324
def _psycopg2_sql_injector(dbm_comment, sql_statement):
2425
# type: (str, Composable) -> Composable
2526
if isinstance(sql_statement, Composable):
2627
return psycopg2.sql.SQL(dbm_comment) + sql_statement
27-
return dbm_comment + sql_statement
28+
return _default_sql_injector(dbm_comment, sql_statement)
2829

2930

3031
config._add(

ddtrace/propagation/_database_monitoring.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import TYPE_CHECKING
2+
from typing import Union # noqa
23

34
from ddtrace.internal.logger import get_logger
45
from ddtrace.vendor.sqlcommenter import generate_sql_comment as _generate_sql_comment
@@ -25,23 +26,24 @@
2526
log = get_logger(__name__)
2627

2728

28-
def _default_sql_injector(dbm_comment, sql_statement):
29-
# type: (str, str) -> str
29+
def default_sql_injector(dbm_comment, sql_statement):
30+
# type: (str, Union[str, bytes]) -> Union[str, bytes]
3031
try:
32+
if isinstance(sql_statement, bytes):
33+
return dbm_comment.encode("utf-8", errors="strict") + sql_statement
3134
return dbm_comment + sql_statement
32-
except TypeError:
35+
except (TypeError, ValueError):
3336
log.warning(
3437
"Linking Database Monitoring profiles to spans is not supported for the following query type: %s. "
3538
"To disable this feature please set the following environment variable: "
3639
"DD_DBM_PROPAGATION_MODE=disabled",
3740
type(sql_statement),
38-
exc_info=True,
3941
)
4042
return sql_statement
4143

4244

4345
class _DBM_Propagator(object):
44-
def __init__(self, sql_pos, sql_kw, sql_injector=_default_sql_injector):
46+
def __init__(self, sql_pos, sql_kw, sql_injector=default_sql_injector):
4547
self.sql_pos = sql_pos
4648
self.sql_kw = sql_kw
4749
self.sql_injector = sql_injector
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
dbm: Support sql queries with the type ``byte``.

tests/contrib/psycopg/test_psycopg.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ def test_postgres_dbm_propagation_tag(self):
397397
# test string queries
398398
cursor.execute("select 'str blah'")
399399
cursor.executemany("select %s", (("str_foo",), ("str_bar",)))
400+
# test byte string queries
401+
cursor.execute(b"select 'byte str blah'")
402+
cursor.executemany(b"select %s", ((b"bstr_foo",), (b"bstr_bar",)))
400403
# test composed queries
401404
cursor.execute(SQL("select 'composed_blah'"))
402405
cursor.executemany(SQL("select %s"), (("composed_foo",), ("composed_bar",)))
@@ -418,9 +421,16 @@ def test_postgres_dbm_propagation_comment(self):
418421
cursor.execute("select 'blah'")
419422
cursor.executemany("select %s", (("foo",), ("bar",)))
420423
dbm_comment = "/*dddbs='postgres',dde='staging',ddps='orders-app',ddpv='v7343437-d7ac743'*/ "
421-
# test string queries
422424
cursor.__wrapped__.execute.assert_called_once_with(dbm_comment + "select 'blah'")
423425
cursor.__wrapped__.executemany.assert_called_once_with(dbm_comment + "select %s", (("foo",), ("bar",)))
426+
# test byte string queries
427+
cursor.__wrapped__.reset_mock()
428+
cursor.execute(b"select 'blah'")
429+
cursor.executemany(b"select %s", ((b"foo",), (b"bar",)))
430+
cursor.__wrapped__.execute.assert_called_once_with(dbm_comment.encode() + b"select 'blah'")
431+
cursor.__wrapped__.executemany.assert_called_once_with(
432+
dbm_comment.encode() + b"select %s", ((b"foo",), (b"bar",))
433+
)
424434
# test composed queries
425435
cursor.__wrapped__.reset_mock()
426436
cursor.execute(SQL("select 'blah'"))

tests/internal/test_database_monitoring.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import logging
2+
13
import pytest
24

5+
from ddtrace.propagation._database_monitoring import default_sql_injector
36
from ddtrace.settings import _database_monitoring
47
from tests.utils import override_env
58

@@ -114,3 +117,24 @@ def test_dbm_propagation_full_mode():
114117

115118
# ensure that dbm tag is set (required for full mode)
116119
assert dbspan.get_tag(_database_monitoring.DBM_TRACE_INJECTED_TAG) == "true"
120+
121+
122+
def test_default_sql_injector(caplog):
123+
# test sql injection with unicode str
124+
dbm_comment = "/*dddbs='orders-db'*/ "
125+
str_query = "select * from table;"
126+
assert default_sql_injector(dbm_comment, str_query) == "/*dddbs='orders-db'*/ select * from table;"
127+
128+
# test sql injection with uft-8 byte str query
129+
dbm_comment = "/*dddbs='orders-db'*/ "
130+
str_query = "select * from table;".encode("utf-8")
131+
assert default_sql_injector(dbm_comment, str_query) == b"/*dddbs='orders-db'*/ select * from table;"
132+
133+
# test sql injection with a non supported type
134+
with caplog.at_level(logging.INFO):
135+
dbm_comment = "/*dddbs='orders-db'*/ "
136+
non_string_object = object()
137+
result = default_sql_injector(dbm_comment, non_string_object)
138+
assert result == non_string_object
139+
140+
assert "Linking Database Monitoring profiles to spans is not supported for the following query type:" in caplog.text

tests/snapshots/tests.contrib.psycopg.test_psycopg.test_postgres_dbm_propagation_tag.json

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"db.user": "postgres",
1818
"language": "python",
1919
"out.host": "127.0.0.1",
20-
"runtime-id": "6759aca0ce8d4ff6a328ff125e50c51d"
20+
"runtime-id": "976be4b4fbee409ba62cdc546761d15a"
2121
},
2222
"metrics": {
2323
"_dd.agent_psr": 1.0,
@@ -27,10 +27,10 @@
2727
"_sampling_priority_v1": 1,
2828
"db.row_count": 1,
2929
"network.destination.port": 5432,
30-
"process_id": 56478
30+
"process_id": 58527
3131
},
32-
"duration": 2652000,
33-
"start": 1669148045047826000
32+
"duration": 8128000,
33+
"start": 1677184993764288000
3434
}],
3535
[
3636
{
@@ -51,7 +51,7 @@
5151
"db.user": "postgres",
5252
"language": "python",
5353
"out.host": "127.0.0.1",
54-
"runtime-id": "6759aca0ce8d4ff6a328ff125e50c51d",
54+
"runtime-id": "976be4b4fbee409ba62cdc546761d15a",
5555
"sql.executemany": "true"
5656
},
5757
"metrics": {
@@ -62,16 +62,16 @@
6262
"_sampling_priority_v1": 1,
6363
"db.row_count": 2,
6464
"network.destination.port": 5432,
65-
"process_id": 56478
65+
"process_id": 58527
6666
},
67-
"duration": 2410000,
68-
"start": 1669148045078020000
67+
"duration": 5081000,
68+
"start": 1677184993788584000
6969
}],
7070
[
7171
{
7272
"name": "postgres.query",
7373
"service": "postgres",
74-
"resource": "select 'composed_blah'",
74+
"resource": "select 'byte str blah'",
7575
"trace_id": 2,
7676
"span_id": 1,
7777
"parent_id": 0,
@@ -86,7 +86,7 @@
8686
"db.user": "postgres",
8787
"language": "python",
8888
"out.host": "127.0.0.1",
89-
"runtime-id": "6759aca0ce8d4ff6a328ff125e50c51d"
89+
"runtime-id": "976be4b4fbee409ba62cdc546761d15a"
9090
},
9191
"metrics": {
9292
"_dd.agent_psr": 1.0,
@@ -96,10 +96,10 @@
9696
"_sampling_priority_v1": 1,
9797
"db.row_count": 1,
9898
"network.destination.port": 5432,
99-
"process_id": 56478
99+
"process_id": 58527
100100
},
101-
"duration": 1301000,
102-
"start": 1669148045080500000
101+
"duration": 2678000,
102+
"start": 1677184993793709000
103103
}],
104104
[
105105
{
@@ -120,7 +120,76 @@
120120
"db.user": "postgres",
121121
"language": "python",
122122
"out.host": "127.0.0.1",
123-
"runtime-id": "6759aca0ce8d4ff6a328ff125e50c51d",
123+
"runtime-id": "976be4b4fbee409ba62cdc546761d15a",
124+
"sql.executemany": "true"
125+
},
126+
"metrics": {
127+
"_dd.agent_psr": 1.0,
128+
"_dd.measured": 1,
129+
"_dd.top_level": 1,
130+
"_dd.tracer_kr": 1.0,
131+
"_sampling_priority_v1": 1,
132+
"db.row_count": 2,
133+
"network.destination.port": 5432,
134+
"process_id": 58527
135+
},
136+
"duration": 4226000,
137+
"start": 1677184993796426000
138+
}],
139+
[
140+
{
141+
"name": "postgres.query",
142+
"service": "postgres",
143+
"resource": "select 'composed_blah'",
144+
"trace_id": 4,
145+
"span_id": 1,
146+
"parent_id": 0,
147+
"type": "sql",
148+
"error": 0,
149+
"meta": {
150+
"_dd.dbm_trace_injected": "true",
151+
"_dd.p.dm": "-0",
152+
"component": "psycopg",
153+
"db.application": "None",
154+
"db.name": "postgres",
155+
"db.user": "postgres",
156+
"language": "python",
157+
"out.host": "127.0.0.1",
158+
"runtime-id": "976be4b4fbee409ba62cdc546761d15a"
159+
},
160+
"metrics": {
161+
"_dd.agent_psr": 1.0,
162+
"_dd.measured": 1,
163+
"_dd.top_level": 1,
164+
"_dd.tracer_kr": 1.0,
165+
"_sampling_priority_v1": 1,
166+
"db.row_count": 1,
167+
"network.destination.port": 5432,
168+
"process_id": 58527
169+
},
170+
"duration": 1378000,
171+
"start": 1677184993800707000
172+
}],
173+
[
174+
{
175+
"name": "postgres.query",
176+
"service": "postgres",
177+
"resource": "select %s",
178+
"trace_id": 5,
179+
"span_id": 1,
180+
"parent_id": 0,
181+
"type": "sql",
182+
"error": 0,
183+
"meta": {
184+
"_dd.dbm_trace_injected": "true",
185+
"_dd.p.dm": "-0",
186+
"component": "psycopg",
187+
"db.application": "None",
188+
"db.name": "postgres",
189+
"db.user": "postgres",
190+
"language": "python",
191+
"out.host": "127.0.0.1",
192+
"runtime-id": "976be4b4fbee409ba62cdc546761d15a",
124193
"sql.executemany": "true"
125194
},
126195
"metrics": {
@@ -131,8 +200,8 @@
131200
"_sampling_priority_v1": 1,
132201
"db.row_count": 2,
133202
"network.destination.port": 5432,
134-
"process_id": 56478
203+
"process_id": 58527
135204
},
136-
"duration": 1988000,
137-
"start": 1669148045081837000
205+
"duration": 2705000,
206+
"start": 1677184993802144000
138207
}]]

0 commit comments

Comments
 (0)