Skip to content

Commit 50dce14

Browse files
authored
unwrap pg connection/cursor for new psycopg2 extensions (#621)
psycopg2 2.8 added two new extensions, `quote_ident` and `encrypt_password`. Both extensions do a type check on the connection/cursor object, which fails if they are wrapped with our object proxies. Similar to `register_type`, we need to unwrap the connection/cursor before handing it to these extensions. fixes #620
1 parent a06ba12 commit 50dce14

File tree

4 files changed

+60
-5
lines changed

4 files changed

+60
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
### Bugfixes
77

88
* ensure that metrics with value 0 are not collected if they have the `reset_on_collect` flag set (#615)
9+
* unwrap postgres cursor for newly introduced psycopg2 extensions (#621)
910

1011
## v5.2.2
1112
[Check the diff](https://github.com/elastic/apm-agent-python/compare/v5.2.1...v5.2.2)

elasticapm/instrumentation/packages/psycopg2.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,21 @@ def call(self, module, method, wrapped, instance, args, kwargs):
8787
return PGConnectionProxy(wrapped(*args, **kwargs))
8888

8989

90-
class Psycopg2RegisterTypeInstrumentation(DbApi2Instrumentation):
91-
name = "psycopg2-register-type"
90+
class Psycopg2ExtensionsInstrumentation(DbApi2Instrumentation):
91+
"""
92+
Some extensions do a type check on the Connection/Cursor in C-code, which our
93+
proxy fails. For these extensions, we need to ensure that the unwrapped
94+
Connection/Cursor is passed.
95+
"""
96+
97+
name = "psycopg2"
9298

9399
instrument_list = [
94100
("psycopg2.extensions", "register_type"),
95101
# specifically instrument `register_json` as it bypasses `register_type`
96102
("psycopg2._json", "register_json"),
103+
("psycopg2.extensions", "quote_ident"),
104+
("psycopg2.extensions", "encrypt_password"),
97105
]
98106

99107
def call(self, module, method, wrapped, instance, args, kwargs):
@@ -108,4 +116,11 @@ def call(self, module, method, wrapped, instance, args, kwargs):
108116
if args and hasattr(args[0], "__wrapped__"):
109117
args = (args[0].__wrapped__,) + args[1:]
110118

119+
elif method == "encrypt_password":
120+
# connection/cursor is either 3rd argument, or "scope" keyword argument
121+
if len(args) >= 3 and hasattr(args[2], "__wrapped__"):
122+
args = args[:2] + (args[2].__wrapped__,) + args[3:]
123+
elif "scope" in kwargs and hasattr(kwargs["scope"], "__wrapped__"):
124+
kwargs["scope"] = kwargs["scope"].__wrapped__
125+
111126
return wrapped(*args, **kwargs)

elasticapm/instrumentation/register.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"elasticapm.instrumentation.packages.botocore.BotocoreInstrumentation",
3636
"elasticapm.instrumentation.packages.jinja2.Jinja2Instrumentation",
3737
"elasticapm.instrumentation.packages.psycopg2.Psycopg2Instrumentation",
38-
"elasticapm.instrumentation.packages.psycopg2.Psycopg2RegisterTypeInstrumentation",
38+
"elasticapm.instrumentation.packages.psycopg2.Psycopg2ExtensionsInstrumentation",
3939
"elasticapm.instrumentation.packages.mysql.MySQLInstrumentation",
4040
"elasticapm.instrumentation.packages.pylibmc.PyLibMcInstrumentation",
4141
"elasticapm.instrumentation.packages.pymongo.PyMongoInstrumentation",

tests/instrumentation/psycopg2_tests.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
# as of Jan 2018, psycopg2cffi doesn't have this module
4949
has_sql_module = False
5050

51+
try:
52+
import psycopg2.extensions
53+
except ImportError:
54+
psycopg2.extensions = None
55+
5156

5257
pytestmark = pytest.mark.psycopg2
5358

@@ -259,7 +264,7 @@ def test_fully_qualified_table_name():
259264

260265
@pytest.mark.integrationtest
261266
@pytest.mark.skipif(not has_postgres_configured, reason="PostgresSQL not configured")
262-
def test_psycopg2_register_type(postgres_connection, elasticapm_client):
267+
def test_psycopg2_register_type(instrument, postgres_connection, elasticapm_client):
263268
import psycopg2.extras
264269

265270
elasticapm_client.begin_transaction("web.django")
@@ -271,7 +276,7 @@ def test_psycopg2_register_type(postgres_connection, elasticapm_client):
271276

272277
@pytest.mark.integrationtest
273278
@pytest.mark.skipif(not has_postgres_configured, reason="PostgresSQL not configured")
274-
def test_psycopg2_register_json(postgres_connection, elasticapm_client):
279+
def test_psycopg2_register_json(instrument, postgres_connection, elasticapm_client):
275280
# register_json bypasses register_type, so we have to test unwrapping
276281
# separately
277282
import psycopg2.extras
@@ -286,6 +291,40 @@ def test_psycopg2_register_json(postgres_connection, elasticapm_client):
286291
elasticapm_client.end_transaction(None, "test-transaction")
287292

288293

294+
@pytest.mark.integrationtest
295+
@pytest.mark.skipif(not has_postgres_configured, reason="PostgresSQL not configured")
296+
@pytest.mark.skipif(
297+
not hasattr(psycopg2.extensions, "quote_ident"), reason="psycopg2 driver doesn't have quote_ident extension"
298+
)
299+
def test_psycopg2_quote_ident(instrument, postgres_connection, elasticapm_client):
300+
elasticapm_client.begin_transaction("web.django")
301+
ident = psycopg2.extensions.quote_ident("x'x", postgres_connection)
302+
elasticapm_client.end_transaction(None, "test-transaction")
303+
304+
assert ident == '"x\'x"'
305+
306+
307+
@pytest.mark.integrationtest
308+
@pytest.mark.skipif(not has_postgres_configured, reason="PostgresSQL not configured")
309+
@pytest.mark.skipif(
310+
not hasattr(psycopg2.extensions, "encrypt_password"),
311+
reason="psycopg2 driver doesn't have encrypt_password extension",
312+
)
313+
@pytest.mark.skipif(
314+
hasattr(psycopg2, "__libpq_version__") and psycopg2.__libpq_version__ < 100000,
315+
reason="test code requires libpq >= 10",
316+
)
317+
def test_psycopg2_encrypt_password(instrument, postgres_connection, elasticapm_client):
318+
elasticapm_client.begin_transaction("web.django")
319+
pw1 = psycopg2.extensions.encrypt_password("user", "password", postgres_connection)
320+
pw2 = psycopg2.extensions.encrypt_password("user", "password", postgres_connection, None)
321+
pw3 = psycopg2.extensions.encrypt_password("user", "password", postgres_connection, algorithm=None)
322+
pw4 = psycopg2.extensions.encrypt_password("user", "password", scope=postgres_connection, algorithm=None)
323+
elasticapm_client.end_transaction(None, "test-transaction")
324+
325+
assert pw1.startswith("md5") and (pw1 == pw2 == pw3 == pw4)
326+
327+
289328
@pytest.mark.integrationtest
290329
@pytest.mark.skipif(not has_postgres_configured, reason="PostgresSQL not configured")
291330
def test_psycopg2_tracing_outside_of_elasticapm_transaction(instrument, postgres_connection, elasticapm_client):

0 commit comments

Comments
 (0)