diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e4827020..7a35a867ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Breaking changes + +- `opentelemetry-instrumentation-dbapi`, `opentelemetry-instrumentation-asyncpg`, + `opentelemetry-instrumentation-tortoiseorm`: Replace `db.statement.parameters` + attribute with individual `db.query.parameter.` attributes following + [OTel database semantic conventions](https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/). + This also affects instrumentations that delegate to dbapi (psycopg, psycopg2, aiopg, mysql, etc.). + ## Version 1.40.0/0.61b0 (2026-03-04) ### Added diff --git a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py index ab1fe53843..92b219555e 100644 --- a/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiopg/tests/test_aiopg_integration.py @@ -30,6 +30,7 @@ from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, + DB_QUERY_PARAMETER_TEMPLATE, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -329,8 +330,12 @@ def test_span_succeeded(self): self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertEqual( - span.attributes["db.statement.parameters"], - "('param1Value', False)", + span.attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], + "'param1Value'", + ) + self.assertEqual( + span.attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.1"], + "False", ) self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py index c8aba9bbf3..1c0d9f797c 100644 --- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py @@ -59,9 +59,12 @@ async def main(): from opentelemetry.instrumentation.asyncpg.package import _instruments from opentelemetry.instrumentation.asyncpg.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.instrumentation.utils import ( + unwrap, +) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, + DB_QUERY_PARAMETER_TEMPLATE, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -107,7 +110,10 @@ def _hydrate_span_from_args(connection, query, parameters) -> dict: span_attributes[DB_STATEMENT] = query if parameters is not None and len(parameters) > 0: - span_attributes["db.statement.parameters"] = str(parameters) + for idx, value in enumerate(parameters): + span_attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.{idx}"] = repr( + value + ) return span_attributes diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py index cd154616a6..45bb23f92a 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -186,6 +186,7 @@ ) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, + DB_QUERY_PARAMETER_TEMPLATE, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -231,7 +232,7 @@ def trace_integration( user in Connection object. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. - capture_parameters: Configure if db.statement.parameters should be captured. + capture_parameters: Configure if db.query.parameter. attributes should be captured. enable_commenter: Flag to enable/disable sqlcommenter. db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the default one is used. @@ -280,7 +281,7 @@ def wrap_connect( user in Connection object. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. - capture_parameters: Configure if db.statement.parameters should be captured. + capture_parameters: Configure if db.query.parameter. attributes should be captured. enable_commenter: Flag to enable/disable sqlcommenter. db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the default one is used. @@ -359,7 +360,7 @@ def instrument_connection( user in a connection object. tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to use. If omitted the current configured one is used. - capture_parameters: Configure if db.statement.parameters should be captured. + capture_parameters: Configure if db.query.parameter. attributes should be captured. enable_commenter: Flag to enable/disable sqlcommenter. commenter_options: Configurations for tags to be appended at the sql query. connect_module: Module name where connect method is available. @@ -701,7 +702,17 @@ def _populate_span( span.set_attribute(attribute_key, attribute_value) if self._db_api_integration.capture_parameters and len(args) > 1: - span.set_attribute("db.statement.parameters", str(args[1])) + params = args[1] + if isinstance(params, dict): + for key, value in params.items(): + span.set_attribute( + f"{DB_QUERY_PARAMETER_TEMPLATE}.{key}", repr(value) + ) + elif isinstance(params, (tuple, list)): + for idx, value in enumerate(params): + span.set_attribute( + f"{DB_QUERY_PARAMETER_TEMPLATE}.{idx}", repr(value) + ) def get_operation_name( self, cursor: CursorT, args: tuple[Any, ...] diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index f9fb9c952b..91ba870068 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -26,6 +26,7 @@ from opentelemetry.semconv._incubating.attributes import net_attributes from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, + DB_QUERY_PARAMETER_TEMPLATE, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -75,7 +76,7 @@ def test_span_succeeded(self): self.assertEqual(span.attributes[DB_SYSTEM], "testcomponent") self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") - self.assertFalse("db.statement.parameters" in span.attributes) + self.assertNotIn(f"{DB_QUERY_PARAMETER_TEMPLATE}.0", span.attributes) self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual(span.attributes[NET_PEER_NAME], "testhost") self.assertEqual(span.attributes[NET_PEER_PORT], 123) @@ -142,8 +143,12 @@ def test_span_succeeded_with_capture_of_statement_parameters(self): self.assertEqual(span.attributes[DB_NAME], "testdatabase") self.assertEqual(span.attributes[DB_STATEMENT], "Test query") self.assertEqual( - span.attributes["db.statement.parameters"], - "('param1Value', False)", + span.attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], + "'param1Value'", + ) + self.assertEqual( + span.attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.1"], + "False", ) self.assertEqual(span.attributes[DB_USER], "testuser") self.assertEqual( diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 84d6709bbb..0342e120e6 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -22,6 +22,9 @@ import opentelemetry.instrumentation.psycopg from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor from opentelemetry.sdk import resources +from opentelemetry.semconv._incubating.attributes.db_attributes import ( + DB_QUERY_PARAMETER_TEMPLATE, +) from opentelemetry.test.test_base import TestBase @@ -288,7 +291,11 @@ def test_span_params_attribute(self): assert spans_list[0].attributes is not None self.assertEqual(spans_list[0].attributes["db.statement"], query) self.assertEqual( - spans_list[0].attributes["db.statement.parameters"], str(params) + spans_list[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], + "'test'", + ) + self.assertEqual( + spans_list[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.1"], "42" ) # pylint: disable=unused-argument @@ -568,7 +575,11 @@ async def test_span_params_attribute(self): assert spans_list[0].attributes is not None self.assertEqual(spans_list[0].attributes["db.statement"], query) self.assertEqual( - spans_list[0].attributes["db.statement.parameters"], str(params) + spans_list[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], + "'test'", + ) + self.assertEqual( + spans_list[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.1"], "42" ) # pylint: disable=unused-argument diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py index 4c011c188b..5412d254ac 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py @@ -54,6 +54,7 @@ ) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, + DB_QUERY_PARAMETER_TEMPLATE, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -237,7 +238,9 @@ def _uninstrument(self, **kwargs): tortoise.contrib.pydantic.base.PydanticListModel, "from_queryset" ) - def _hydrate_span_from_args(self, connection, query, parameters) -> dict: + def _hydrate_span_from_args( # pylint: disable=too-many-branches + self, connection, query, parameters + ) -> dict: """Get network and database attributes from connection.""" span_attributes = {} capabilities = getattr(connection, "capabilities", None) @@ -268,7 +271,10 @@ def _hydrate_span_from_args(self, connection, query, parameters) -> dict: if self.capture_parameters: if parameters is not None and len(parameters) > 0: - span_attributes["db.statement.parameters"] = str(parameters) + for idx, value in enumerate(parameters): + span_attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.{idx}"] = ( + repr(value) + ) return span_attributes diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests/test_tortoiseorm_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests/test_tortoiseorm_instrumentation.py index 910c4a8b41..3cbe9f548e 100644 --- a/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests/test_tortoiseorm_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/tests/test_tortoiseorm_instrumentation.py @@ -21,6 +21,7 @@ from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, + DB_QUERY_PARAMETER_TEMPLATE, DB_STATEMENT, DB_SYSTEM, ) @@ -30,6 +31,7 @@ class MockModel(models.Model): id = fields.IntField(pk=True) name = fields.TextField() + description = fields.TextField(default="") def __str__(self): return self.name @@ -87,15 +89,31 @@ def test_capture_parameters(self): async def run(): await self._init_tortoise() - await MockModel.create(name="Test Parameterized") + await MockModel.create( + name="Test Capture Params", description="Multiple Params" + ) self._async_call(run()) spans = self.memory_exporter.get_finished_spans() insert_span = next(s for s in spans if s.name == "INSERT") - self.assertIn("db.statement.parameters", insert_span.attributes) - self.assertIn( - "Test Parameterized", - insert_span.attributes["db.statement.parameters"], + self.assertEqual( + insert_span.attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], + "['Test Capture Params', 'Multiple Params']", + ) + + def test_capture_no_parameters(self): + TortoiseORMInstrumentor().uninstrument() + TortoiseORMInstrumentor().instrument(capture_parameters=True) + + async def run(): + await self._init_tortoise() + await MockModel.all() + + self._async_call(run()) + spans = self.memory_exporter.get_finished_spans() + select_span = next(s for s in spans if s.name == "SELECT") + self.assertNotIn( + f"{DB_QUERY_PARAMETER_TEMPLATE}.0", select_span.attributes ) def test_uninstrument(self): diff --git a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py index e14ba07271..04d4d0ebf3 100644 --- a/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py +++ b/tests/opentelemetry-docker-tests/tests/asyncpg/test_asyncpg_functional.py @@ -12,6 +12,9 @@ from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) +from opentelemetry.semconv._incubating.attributes.db_attributes import ( + DB_QUERY_PARAMETER_TEMPLATE, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import StatusCode @@ -360,7 +363,7 @@ def test_instrumented_execute_method_with_arguments(self, *_, **__): spans[0].attributes[SpanAttributes.DB_STATEMENT], "SELECT $1;" ) self.assertEqual( - spans[0].attributes["db.statement.parameters"], "('1',)" + spans[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], "'1'" ) def test_instrumented_fetch_method_with_arguments(self, *_, **__): @@ -375,7 +378,7 @@ def test_instrumented_fetch_method_with_arguments(self, *_, **__): spans[0].attributes[SpanAttributes.DB_STATEMENT], "SELECT $1;" ) self.assertEqual( - spans[0].attributes["db.statement.parameters"], "('1',)" + spans[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], "'1'" ) def test_instrumented_executemany_method_with_arguments(self, *_, **__): @@ -389,7 +392,8 @@ def test_instrumented_executemany_method_with_arguments(self, *_, **__): spans[0].attributes[SpanAttributes.DB_STATEMENT], "SELECT $1;" ) self.assertEqual( - spans[0].attributes["db.statement.parameters"], "([['1'], ['2']],)" + spans[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], + "[['1'], ['2']]", ) def test_instrumented_execute_interface_error_method(self, *_, **__): @@ -404,7 +408,13 @@ def test_instrumented_execute_interface_error_method(self, *_, **__): spans[0].attributes[SpanAttributes.DB_STATEMENT], "SELECT 42;" ) self.assertEqual( - spans[0].attributes["db.statement.parameters"], "(1, 2, 3)" + spans[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], "'1'" + ) + self.assertEqual( + spans[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.1"], "2" + ) + self.assertEqual( + spans[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.2"], "3" ) def test_instrumented_executemany_method_empty_query(self, *_, **__): @@ -417,7 +427,7 @@ def test_instrumented_executemany_method_empty_query(self, *_, **__): self.assertEqual(spans[0].name, POSTGRES_DB_NAME) self.assertEqual(spans[0].attributes[SpanAttributes.DB_STATEMENT], "") self.assertEqual( - spans[0].attributes["db.statement.parameters"], "([],)" + spans[0].attributes[f"{DB_QUERY_PARAMETER_TEMPLATE}.0"], "[]" ) def test_instrumented_fetch_method_broken_asyncpg(self, *_, **__): @@ -489,7 +499,7 @@ async def _fake_execute(*args, **kwargs): SpanAttributes.NET_PEER_PORT: 5432, SpanAttributes.NET_TRANSPORT: "ip_tcp", SpanAttributes.DB_STATEMENT: "SELECT $1", - "db.statement.parameters": "('42',)", + f"{DB_QUERY_PARAMETER_TEMPLATE}.0": "'42'", }, ) self.assertEqual(span.kind, trace.SpanKind.CLIENT) @@ -523,7 +533,7 @@ async def _fake_cursor_execute(*args, **kwargs): SpanAttributes.NET_PEER_PORT: 5432, SpanAttributes.NET_TRANSPORT: "ip_tcp", SpanAttributes.DB_STATEMENT: "SELECT $1", - "db.statement.parameters": "('99',)", + f"{DB_QUERY_PARAMETER_TEMPLATE}.0": "'99'", }, ) self.assertEqual(span.kind, trace.SpanKind.CLIENT)