From f346658bc7ffc09355d7012e398c623d40e8d1a7 Mon Sep 17 00:00:00 2001 From: henry3260 Date: Sat, 18 Jul 2026 22:40:14 +0800 Subject: [PATCH 1/2] opentelemetry-instrumentation-psycopg: add semconv stability opt-in test coverage Cover database,http and database/dup,http/dup opt-in modes for both the sync and async connection paths, verifying stable-only and dual-emission span attributes, mirroring the pattern from #4810. --- .../tests/test_psycopg_integration.py | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 85855a6274..7aaad07794 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -3,6 +3,7 @@ # pylint: disable=no-member # psycopg stubs reference string.templatelib on 3.14+ import asyncio +import contextlib import sys import types from unittest import IsolatedAsyncioTestCase, mock @@ -12,11 +13,49 @@ from psycopg.sql import SQL, Composed import opentelemetry.instrumentation.psycopg +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor from opentelemetry.sdk import resources +from opentelemetry.semconv._incubating.attributes.db_attributes import ( + DB_NAME, + DB_STATEMENT, + DB_SYSTEM, + DB_USER, +) +from opentelemetry.semconv._incubating.attributes.net_attributes import ( + NET_PEER_NAME, + NET_PEER_PORT, +) +from opentelemetry.semconv.attributes.db_attributes import ( + DB_NAMESPACE, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) from opentelemetry.test.test_base import TestBase +@contextlib.contextmanager +def use_semconv_opt_in(sem_conv_mode): + env_patch = mock.patch.dict( + "os.environ", + {OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode}, + ) + _OpenTelemetrySemanticConventionStability._initialized = False + env_patch.start() + try: + yield + finally: + env_patch.stop() + _OpenTelemetrySemanticConventionStability._initialized = False + + class MockCursor: execute = mock.MagicMock(spec=types.MethodType) execute.__name__ = "execute" @@ -128,6 +167,25 @@ async def __aexit__(self, *args): return mock.MagicMock(spec=types.MethodType) +class MockConnectionInfo: + dbname = "test" + host = "localhost" + port = 5432 + user = "testuser" + + +class MockConnectionWithInfo(MockConnection): + info = MockConnectionInfo() + + +class MockAsyncConnectionWithInfo(MockAsyncConnection): + info = MockConnectionInfo() + + @staticmethod + async def connect(*args, **kwargs): + return MockAsyncConnectionWithInfo(**kwargs) + + class PostgresqlIntegrationTestMixin: # pylint: disable=invalid-name def setUp(self): @@ -508,6 +566,68 @@ def test_t_string_commenter(self): span.attributes["db.statement"], "SELECT {value} FROM foo;" ) + def test_semconv_stable(self): + """database,http opt-in emits only stable attributes.""" + with ( + use_semconv_opt_in("database,http"), + mock.patch("psycopg.connect", MockConnectionWithInfo), + ): + PsycopgInstrumentor().instrument() + + cnx = psycopg.connect(database="test") + cursor = cnx.cursor() + cursor.execute("SELECT * FROM test") + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") + self.assertEqual(span.attributes[DB_NAMESPACE], "test") + self.assertEqual( + span.attributes[DB_QUERY_TEXT], "SELECT * FROM test" + ) + self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") + self.assertEqual(span.attributes[SERVER_PORT], 5432) + self.assertNotIn(DB_SYSTEM, span.attributes) + self.assertNotIn(DB_NAME, span.attributes) + self.assertNotIn(DB_STATEMENT, span.attributes) + self.assertNotIn(DB_USER, span.attributes) + self.assertNotIn(NET_PEER_NAME, span.attributes) + self.assertNotIn(NET_PEER_PORT, span.attributes) + + def test_semconv_dup(self): + """database/dup,http/dup opt-in emits both legacy and stable attributes.""" + with ( + use_semconv_opt_in("database/dup,http/dup"), + mock.patch("psycopg.connect", MockConnectionWithInfo), + ): + PsycopgInstrumentor().instrument() + + cnx = psycopg.connect(database="test") + cursor = cnx.cursor() + cursor.execute("SELECT * FROM test") + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") + self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") + self.assertEqual(span.attributes[DB_NAME], "test") + self.assertEqual(span.attributes[DB_NAMESPACE], "test") + self.assertEqual( + span.attributes[DB_STATEMENT], "SELECT * FROM test" + ) + self.assertEqual( + span.attributes[DB_QUERY_TEXT], "SELECT * FROM test" + ) + self.assertEqual(span.attributes[DB_USER], "testuser") + self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") + self.assertEqual(span.attributes[NET_PEER_PORT], 5432) + self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") + self.assertEqual(span.attributes[SERVER_PORT], 5432) + class TestPostgresqlIntegrationAsync( PostgresqlIntegrationTestMixin, TestBase, IsolatedAsyncioTestCase @@ -717,3 +837,73 @@ async def test_t_string_span_attributes_async(self): self.assertEqual( span.attributes["db.statement"], "SELECT {value} FROM foo" ) + + async def test_semconv_stable_async(self): + """database,http opt-in emits only stable attributes.""" + with ( + use_semconv_opt_in("database,http"), + mock.patch( + "psycopg.AsyncConnection.connect", + MockAsyncConnectionWithInfo.connect, + ), + ): + PsycopgInstrumentor().instrument() + + acnx = await psycopg.AsyncConnection.connect("test") + async with acnx as cnx: + async with cnx.cursor() as cursor: + await cursor.execute("SELECT * FROM test") + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") + self.assertEqual(span.attributes[DB_NAMESPACE], "test") + self.assertEqual( + span.attributes[DB_QUERY_TEXT], "SELECT * FROM test" + ) + self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") + self.assertEqual(span.attributes[SERVER_PORT], 5432) + self.assertNotIn(DB_SYSTEM, span.attributes) + self.assertNotIn(DB_NAME, span.attributes) + self.assertNotIn(DB_STATEMENT, span.attributes) + self.assertNotIn(DB_USER, span.attributes) + self.assertNotIn(NET_PEER_NAME, span.attributes) + self.assertNotIn(NET_PEER_PORT, span.attributes) + + async def test_semconv_dup_async(self): + """database/dup,http/dup opt-in emits both legacy and stable attributes.""" + with ( + use_semconv_opt_in("database/dup,http/dup"), + mock.patch( + "psycopg.AsyncConnection.connect", + MockAsyncConnectionWithInfo.connect, + ), + ): + PsycopgInstrumentor().instrument() + + acnx = await psycopg.AsyncConnection.connect("test") + async with acnx as cnx: + async with cnx.cursor() as cursor: + await cursor.execute("SELECT * FROM test") + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + self.assertEqual(span.attributes[DB_SYSTEM], "postgresql") + self.assertEqual(span.attributes[DB_SYSTEM_NAME], "postgresql") + self.assertEqual(span.attributes[DB_NAME], "test") + self.assertEqual(span.attributes[DB_NAMESPACE], "test") + self.assertEqual( + span.attributes[DB_STATEMENT], "SELECT * FROM test" + ) + self.assertEqual( + span.attributes[DB_QUERY_TEXT], "SELECT * FROM test" + ) + self.assertEqual(span.attributes[DB_USER], "testuser") + self.assertEqual(span.attributes[NET_PEER_NAME], "localhost") + self.assertEqual(span.attributes[NET_PEER_PORT], 5432) + self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost") + self.assertEqual(span.attributes[SERVER_PORT], 5432) From f98b5ec455f0d780e14fb479f05340855bc7ec9e Mon Sep 17 00:00:00 2001 From: henry3260 Date: Sat, 18 Jul 2026 23:28:11 +0800 Subject: [PATCH 2/2] Add changelog fragment for semconv opt-in mode tests --- .changelog/4838.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/4838.added diff --git a/.changelog/4838.added b/.changelog/4838.added new file mode 100644 index 0000000000..a112b5ef07 --- /dev/null +++ b/.changelog/4838.added @@ -0,0 +1 @@ +`opentelemetry-instrumentation-psycopg`: add semantic convention stability opt-in mode tests