Skip to content

Commit ae4972c

Browse files
authored
opentelemetry-instrumentation-dbapi: fix libpq version detection (#3796)
* opentelemetry-instrumentation-dbapi: fix libpq version detection This fixes a crash in the detection of libpq version in the psycopg module when we wrap a module that is not the one containing the pq module that has the version. * Fix tests * More fixes * Please pylint * Add changelog * Fix typo * Fix test name * Add docker tests for psycopg * Simplify psycopg handling * Fix non root module tests
1 parent 5128683 commit ae4972c

File tree

4 files changed

+125
-36
lines changed

4 files changed

+125
-36
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Fixed
1515

16+
- `opentelemetry-instrumentation-dbapi`: fix crash retrieving libpq version when enabling commenter with psycopg
17+
([#3796](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3796))
18+
1619
### Added
1720
- `opentelemetry-instrumentation`: botocore: Add support for AWS Secrets Manager semantic convention attribute
1821
([#3765](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3765))
1922
- Add `rstcheck` to pre-commit to stop introducing invalid RST
2023
([#3777](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3777))
21-
2224
- `opentelemetry-exporter-credential-provider-gcp`: create this package which provides support for supplying your machine's Application Default Credentials (https://cloud.google.com/docs/authentication/application-default-credentials) to the OTLP Exporters created automatically by OpenTelemetry Python's auto instrumentation. These credentials authorize OTLP traces to be sent to `telemetry.googleapis.com`.
2325
[#3766](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3766).
2426

instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,24 @@ def calculate_commenter_data(self) -> dict[str, Any]:
350350
}
351351

352352
if self.database_system == "postgresql":
353-
if hasattr(self.connect_module, "__libpq_version__"):
354-
libpq_version = self.connect_module.__libpq_version__
355-
else:
356-
libpq_version = self.connect_module.pq.__build_version__
357-
commenter_data.update({"libpq_version": libpq_version})
353+
libpq_version = None
354+
# psycopg
355+
try:
356+
libpq_version = self.connect_module.pq.version()
357+
except AttributeError:
358+
pass
359+
360+
# psycopg2
361+
if libpq_version is None:
362+
# this the libpq version the client has been built against
363+
libpq_version = getattr(
364+
self.connect_module, "__libpq_version__", None
365+
)
366+
367+
# we instrument psycopg modules that are not the root one, in that case you
368+
# won't get the libpq_version
369+
if libpq_version is not None:
370+
commenter_data.update({"libpq_version": libpq_version})
358371
elif self.database_system == "mysql":
359372
mysqlc_version = ""
360373
if db_driver == "MySQLdb":

instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def test_executemany_comment(self):
279279
connect_module = mock.MagicMock()
280280
connect_module.__name__ = "test"
281281
connect_module.__version__ = mock.MagicMock()
282-
connect_module.__libpq_version__ = 123
282+
connect_module.pq.version.return_value = 123
283283
connect_module.apilevel = 123
284284
connect_module.threadsafety = 123
285285
connect_module.paramstyle = "test"
@@ -312,7 +312,7 @@ def test_executemany_comment_stmt_enabled(self):
312312
connect_module = mock.MagicMock()
313313
connect_module.__name__ = "test"
314314
connect_module.__version__ = mock.MagicMock()
315-
connect_module.__libpq_version__ = 123
315+
connect_module.pq.version.return_value = 123
316316
connect_module.apilevel = 123
317317
connect_module.threadsafety = 123
318318
connect_module.paramstyle = "test"
@@ -420,7 +420,7 @@ def test_executemany_comment_stmt_enabled_matches_db_statement_attribute(
420420
):
421421
connect_module = mock.MagicMock()
422422
connect_module.__version__ = mock.MagicMock()
423-
connect_module.__libpq_version__ = 123
423+
connect_module.pq.version.return_value = 123
424424
connect_module.apilevel = 123
425425
connect_module.threadsafety = 123
426426
connect_module.paramstyle = "test"
@@ -456,12 +456,12 @@ def test_executemany_comment_stmt_enabled_matches_db_statement_attribute(
456456
).group()
457457
self.assertEqual(cursor_span_id, db_statement_span_id)
458458

459-
def test_compatible_build_version_psycopg_psycopg2_libpq(self):
459+
def test_compatible_build_version_psycopg2_libpq(self):
460460
connect_module = mock.MagicMock()
461461
connect_module.__name__ = "test"
462462
connect_module.__version__ = mock.MagicMock()
463-
connect_module.pq = mock.MagicMock()
464-
connect_module.pq.__build_version__ = 123
463+
connect_module.pq.version.side_effect = AttributeError
464+
connect_module.__libpq_version__ = 123
465465
connect_module.apilevel = 123
466466
connect_module.threadsafety = 123
467467
connect_module.paramstyle = "test"
@@ -490,14 +490,47 @@ def test_compatible_build_version_psycopg_psycopg2_libpq(self):
490490
"Select 1;",
491491
)
492492

493-
def test_compatible_build_version_psycopg_psycopg2_libpq_stmt_enabled(
493+
def test_compatible_build_version_psycopg_libpq(self):
494+
connect_module = mock.MagicMock()
495+
connect_module.__name__ = "test"
496+
connect_module.__version__ = mock.MagicMock()
497+
connect_module.pq.version.return_value = 123
498+
connect_module.apilevel = 123
499+
connect_module.threadsafety = 123
500+
connect_module.paramstyle = "test"
501+
502+
db_integration = dbapi.DatabaseApiIntegration(
503+
"instrumenting_module_test_name",
504+
"postgresql",
505+
enable_commenter=True,
506+
commenter_options={"db_driver": False, "dbapi_level": False},
507+
connect_module=connect_module,
508+
)
509+
mock_connection = db_integration.wrapped_connection(
510+
mock_connect, {}, {}
511+
)
512+
cursor = mock_connection.cursor()
513+
cursor.executemany("Select 1;")
514+
self.assertRegex(
515+
cursor.query,
516+
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
517+
)
518+
spans_list = self.memory_exporter.get_finished_spans()
519+
self.assertEqual(len(spans_list), 1)
520+
span = spans_list[0]
521+
self.assertEqual(
522+
span.attributes[SpanAttributes.DB_STATEMENT],
523+
"Select 1;",
524+
)
525+
526+
def test_no_libpq_version_when_we_dont_instrument_the_root_module(
494527
self,
495528
):
496529
connect_module = mock.MagicMock()
497530
connect_module.__name__ = "test"
498531
connect_module.__version__ = mock.MagicMock()
499-
connect_module.pq = mock.MagicMock()
500-
connect_module.pq.__build_version__ = 123
532+
connect_module.pq.version.side_effect = AttributeError
533+
connect_module.__libpq_version__ = None
501534
connect_module.apilevel = 123
502535
connect_module.threadsafety = 123
503536
connect_module.paramstyle = "test"
@@ -517,20 +550,21 @@ def test_compatible_build_version_psycopg_psycopg2_libpq_stmt_enabled(
517550
cursor.executemany("Select 1;")
518551
self.assertRegex(
519552
cursor.query,
520-
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
553+
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
521554
)
522555
spans_list = self.memory_exporter.get_finished_spans()
523556
self.assertEqual(len(spans_list), 1)
524557
span = spans_list[0]
525558
self.assertRegex(
526559
span.attributes[SpanAttributes.DB_STATEMENT],
527-
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
560+
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
528561
)
529562

530563
def test_executemany_psycopg2_integration_comment(self):
531564
connect_module = mock.MagicMock()
532565
connect_module.__name__ = "psycopg2"
533566
connect_module.__version__ = "1.2.3"
567+
connect_module.pq.version.side_effect = AttributeError
534568
connect_module.__libpq_version__ = 123
535569
connect_module.apilevel = 123
536570
connect_module.threadsafety = 123
@@ -564,6 +598,7 @@ def test_executemany_psycopg2_integration_comment_stmt_enabled(self):
564598
connect_module = mock.MagicMock()
565599
connect_module.__name__ = "psycopg2"
566600
connect_module.__version__ = "1.2.3"
601+
connect_module.pq.version.side_effect = AttributeError
567602
connect_module.__libpq_version__ = 123
568603
connect_module.apilevel = 123
569604
connect_module.threadsafety = 123
@@ -598,8 +633,7 @@ def test_executemany_psycopg_integration_comment(self):
598633
connect_module = mock.MagicMock()
599634
connect_module.__name__ = "psycopg"
600635
connect_module.__version__ = "1.2.3"
601-
connect_module.pq = mock.MagicMock()
602-
connect_module.pq.__build_version__ = 123
636+
connect_module.pq.version.return_value = 123
603637
connect_module.apilevel = 123
604638
connect_module.threadsafety = 123
605639
connect_module.paramstyle = "test"
@@ -632,8 +666,7 @@ def test_executemany_psycopg_integration_comment_stmt_enabled(self):
632666
connect_module = mock.MagicMock()
633667
connect_module.__name__ = "psycopg"
634668
connect_module.__version__ = "1.2.3"
635-
connect_module.pq = mock.MagicMock()
636-
connect_module.pq.__build_version__ = 123
669+
connect_module.pq.version.return_value = 123
637670
connect_module.apilevel = 123
638671
connect_module.threadsafety = 123
639672
connect_module.paramstyle = "test"
@@ -887,8 +920,7 @@ def test_executemany_pymysql_integration_comment_stmt_enabled(self):
887920
def test_executemany_flask_integration_comment(self):
888921
connect_module = mock.MagicMock()
889922
connect_module.__name__ = "test"
890-
connect_module.__version__ = mock.MagicMock()
891-
connect_module.__libpq_version__ = 123
923+
connect_module.pq.version.return_value = 123
892924
connect_module.apilevel = 123
893925
connect_module.threadsafety = 123
894926
connect_module.paramstyle = "test"
@@ -904,7 +936,7 @@ def test_executemany_flask_integration_comment(self):
904936
sqlcommenter_context = context.set_value(
905937
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context
906938
)
907-
context.attach(sqlcommenter_context)
939+
token = context.attach(sqlcommenter_context)
908940

909941
mock_connection = db_integration.wrapped_connection(
910942
mock_connect, {}, {}
@@ -923,16 +955,13 @@ def test_executemany_flask_integration_comment(self):
923955
"Select 1;",
924956
)
925957

926-
clear_context = context.set_value(
927-
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", {}, current_context
928-
)
929-
context.attach(clear_context)
958+
context.detach(token)
930959

931960
def test_executemany_flask_integration_comment_stmt_enabled(self):
932961
connect_module = mock.MagicMock()
933962
connect_module.__name__ = "test"
934963
connect_module.__version__ = mock.MagicMock()
935-
connect_module.__libpq_version__ = 123
964+
connect_module.pq.version.return_value = 123
936965
connect_module.apilevel = 123
937966
connect_module.threadsafety = 123
938967
connect_module.paramstyle = "test"
@@ -949,7 +978,7 @@ def test_executemany_flask_integration_comment_stmt_enabled(self):
949978
sqlcommenter_context = context.set_value(
950979
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", {"flask": 1}, current_context
951980
)
952-
context.attach(sqlcommenter_context)
981+
token = context.attach(sqlcommenter_context)
953982

954983
mock_connection = db_integration.wrapped_connection(
955984
mock_connect, {}, {}
@@ -968,10 +997,7 @@ def test_executemany_flask_integration_comment_stmt_enabled(self):
968997
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',flask=1,libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
969998
)
970999

971-
clear_context = context.set_value(
972-
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", {}, current_context
973-
)
974-
context.attach(clear_context)
1000+
context.detach(token)
9751001

9761002
def test_callproc(self):
9771003
db_integration = dbapi.DatabaseApiIntegration(
@@ -1096,7 +1122,7 @@ def test_non_string_sql_conversion(self):
10961122
connect_module = mock.MagicMock()
10971123
connect_module.__name__ = "test"
10981124
connect_module.__version__ = "1.0"
1099-
connect_module.__libpq_version__ = 123
1125+
connect_module.pq.version.return_value = 123
11001126
connect_module.apilevel = "2.0"
11011127
connect_module.threadsafety = 1
11021128
connect_module.paramstyle = "test"

tests/opentelemetry-docker-tests/tests/postgres/test_psycopg_sqlcommenter.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import psycopg
1516
import psycopg2
1617
from test_psycopg_functional import (
1718
POSTGRES_DB_NAME,
@@ -21,11 +22,12 @@
2122
POSTGRES_USER,
2223
)
2324

25+
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
2426
from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
2527
from opentelemetry.test.test_base import TestBase
2628

2729

28-
class TestFunctionalPsycopg(TestBase):
30+
class TestFunctionalPsycopg2(TestBase):
2931
def setUp(self):
3032
super().setUp()
3133
self._tracer = self.tracer_provider.get_tracer(__name__)
@@ -52,3 +54,49 @@ def test_commenter_enabled(self):
5254
self._cursor.query.decode("ascii"),
5355
r"SELECT 1 /\*db_driver='psycopg2(.*)',dbapi_level='\d.\d',dbapi_threadsafety=\d,driver_paramstyle=(.*),libpq_version=\d*,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
5456
)
57+
58+
59+
class TestFunctionalPsycopg(TestBase):
60+
def setUp(self):
61+
super().setUp()
62+
PsycopgInstrumentor().instrument(enable_commenter=True)
63+
64+
def tearDown(self):
65+
PsycopgInstrumentor().uninstrument()
66+
super().tearDown()
67+
68+
def test_commenter_enabled_root_module(self):
69+
connection = psycopg.connect(
70+
dbname=POSTGRES_DB_NAME,
71+
user=POSTGRES_USER,
72+
password=POSTGRES_PASSWORD,
73+
host=POSTGRES_HOST,
74+
port=POSTGRES_PORT,
75+
)
76+
cursor = connection.cursor()
77+
cursor.execute("SELECT 1;")
78+
self.assertRegex(
79+
cursor._query.query.decode("ascii"),
80+
r"SELECT 1 /\*db_driver='psycopg(.*)',dbapi_level='\d.\d',dbapi_threadsafety=\d,driver_paramstyle=(.*),libpq_version=\d*,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
81+
)
82+
83+
cursor.close()
84+
connection.close()
85+
86+
def test_commenter_enabled_not_root_module(self):
87+
connection = psycopg.Connection.connect(
88+
dbname=POSTGRES_DB_NAME,
89+
user=POSTGRES_USER,
90+
password=POSTGRES_PASSWORD,
91+
host=POSTGRES_HOST,
92+
port=POSTGRES_PORT,
93+
)
94+
cursor = connection.cursor()
95+
cursor.execute("SELECT 1;")
96+
self.assertRegex(
97+
cursor._query.query.decode("ascii"),
98+
r"SELECT 1 /\*db_driver='Connection%%3Aunknown',dbapi_level='\d.\d',dbapi_threadsafety='unknown',driver_paramstyle='unknown',traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
99+
)
100+
101+
cursor.close()
102+
connection.close()

0 commit comments

Comments
 (0)