From 37574b97de7927c549db79213084abfc36cd6f0f Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Thu, 20 Nov 2025 23:41:54 -0800 Subject: [PATCH 1/9] opentelemetry-instrumentation-dbapi: instrument commit and rollback --- CHANGELOG.md | 7 + .../instrumentation/dbapi/__init__.py | 149 +++++++++++++++--- .../tests/test_dbapi_integration.py | 64 ++++++++ .../instrumentation/mysql/__init__.py | 6 + .../instrumentation/mysqlclient/__init__.py | 6 + .../instrumentation/psycopg/__init__.py | 10 +- .../tests/test_psycopg_integration.py | 60 ++++++- .../instrumentation/psycopg2/__init__.py | 5 +- .../tests/test_psycopg2_instrumentation.py | 1 + .../instrumentation/pymssql/__init__.py | 7 +- .../instrumentation/pymysql/__init__.py | 6 + .../tests/test_pymysql_integration.py | 54 +++++++ .../instrumentation/sqlite3/__init__.py | 6 + .../tests/pymysql/test_pymysql_functional.py | 51 ++++-- 14 files changed, 389 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f0cab4fc..e28874d932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-sqlalchemy`: implement new semantic convention opt-in migration ([#4110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4110)) +- `opentelemetry-instrumentation-dbapi`: Add instrumentation for `commit()` and `rollback()` transaction operations + ([#3964](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3964)) +- `opentelemetry-instrumentation-dbapi`: Add `enable_transaction_spans` configuration flag to control transaction span creation (default: `True`) + ([#3964](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3964)) +- `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-sqlite3`, `opentelemetry-instrumentation-pymssql`: Add support for transaction span instrumentation via `enable_transaction_spans` parameter + ([#3964](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3964)) + ### Fixed - `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.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 8be92c13fe..46d241b6ad 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -227,6 +227,7 @@ def trace_integration( db_api_integration_factory: type[DatabaseApiIntegration] | None = None, enable_attribute_commenter: bool = False, commenter_options: dict[str, Any] | None = None, + enable_transaction_spans: bool = True, ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ @@ -246,6 +247,7 @@ def trace_integration( default one is used. enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True. commenter_options: Configurations for tags to be appended at the sql query. + enable_transaction_spans: Flag to enable/disable transaction spans (commit/rollback). Defaults to True. """ wrap_connect( __name__, @@ -260,6 +262,7 @@ def trace_integration( db_api_integration_factory=db_api_integration_factory, enable_attribute_commenter=enable_attribute_commenter, commenter_options=commenter_options, + enable_transaction_spans=enable_transaction_spans, ) @@ -276,6 +279,7 @@ def wrap_connect( db_api_integration_factory: type[DatabaseApiIntegration] | None = None, commenter_options: dict[str, Any] | None = None, enable_attribute_commenter: bool = False, + enable_transaction_spans: bool = True, ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ @@ -295,6 +299,7 @@ def wrap_connect( default one is used. commenter_options: Configurations for tags to be appended at the sql query. enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True. + enable_transaction_spans: Flag to enable/disable transaction spans (commit/rollback). Defaults to True. """ db_api_integration_factory = ( @@ -319,6 +324,7 @@ def wrap_connect_( commenter_options=commenter_options, connect_module=connect_module, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) return db_integration.wrapped_connection(wrapped, args, kwargs) @@ -356,6 +362,7 @@ def instrument_connection( connect_module: Callable[..., Any] | None = None, enable_attribute_commenter: bool = False, db_api_integration_factory: type[DatabaseApiIntegration] | None = None, + enable_transaction_spans: bool = True, ) -> TracedConnectionProxy[ConnectionT]: """Enable instrumentation in a database connection. @@ -377,6 +384,7 @@ def instrument_connection( replacement for :class:`DatabaseApiIntegration`. Can be used to obtain connection attributes from the connect method instead of from the connection itself (as done by the pymssql intrumentor). + enable_transaction_spans: Flag to enable/disable transaction spans (commit/rollback). Defaults to True. Returns: An instrumented connection. @@ -400,6 +408,7 @@ def instrument_connection( commenter_options=commenter_options, connect_module=connect_module, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) db_integration.get_connection_attributes(connection) return get_traced_connection_proxy(connection, db_integration) @@ -436,6 +445,7 @@ def __init__( commenter_options: dict[str, Any] | None = None, connect_module: Callable[..., Any] | None = None, enable_attribute_commenter: bool = False, + enable_transaction_spans: bool = True, ): if connection_attributes is None: self.connection_attributes = { @@ -458,6 +468,7 @@ def __init__( self.enable_commenter = enable_commenter self.commenter_options = commenter_options self.enable_attribute_commenter = enable_attribute_commenter + self.enable_transaction_spans = enable_transaction_spans self.database_system = database_system self.connection_props: dict[str, Any] = {} self.span_attributes: dict[str, Any] = {} @@ -573,6 +584,17 @@ def get_connection_attributes(self, connection: object) -> None: if port is not None: self.span_attributes[NET_PEER_PORT] = port + def populate_common_span_attributes(self, span: trace_api.Span) -> None: + """Populate span with common database connection attributes.""" + if not span.is_recording(): + return + + span.set_attribute(DB_SYSTEM, self.database_system) + span.set_attribute(DB_NAME, self.database) + + for attribute_key, attribute_value in self.span_attributes.items(): + span.set_attribute(attribute_key, attribute_value) + # pylint: disable=abstract-method,no-member class TracedConnectionProxy(BaseObjectProxy, Generic[ConnectionT]): @@ -581,23 +603,54 @@ def __init__( self, connection: ConnectionT, db_api_integration: DatabaseApiIntegration | None = None, + wrap_cursors: bool = True, ): BaseObjectProxy.__init__(self, connection) self._self_db_api_integration = db_api_integration + self._self_wrap_cursors = wrap_cursors def __getattribute__(self, name: str): - if object.__getattribute__(self, name): + # Try to get the attribute from the proxy first + try: return object.__getattribute__(self, name) - - return object.__getattribute__( - object.__getattribute__(self, "_connection"), name - ) + except AttributeError: + # If not found on proxy, try the wrapped connection + return object.__getattribute__( + object.__getattribute__(self, "__wrapped__"), name + ) def cursor(self, *args: Any, **kwargs: Any): - return get_traced_cursor_proxy( - self.__wrapped__.cursor(*args, **kwargs), - self._self_db_api_integration, - ) + cursor = self.__wrapped__.cursor(*args, **kwargs) + + # For databases like psycopg/psycopg2 that use cursor_factory, + # cursor tracing is already handled by the factory, so skip wrapping + if not self._self_wrap_cursors: + return cursor + + # For standard dbapi connections, wrap the cursor + return get_traced_cursor_proxy(cursor, self._self_db_api_integration) + + def _traced_tx_operation( + self, operation_name: str, operation_method: Callable[[], None] + ) -> None: + """Execute a traced transaction operation (commit, rollback).""" + if not is_instrumentation_enabled(): + return operation_method() + + if not self._self_db_api_integration.enable_transaction_spans: + return operation_method() + + with self._self_db_api_integration._tracer.start_as_current_span( + operation_name, kind=trace_api.SpanKind.CLIENT + ) as span: + self._self_db_api_integration.populate_common_span_attributes(span) + return operation_method() + + def commit(self): + return self._traced_tx_operation("COMMIT", self.__wrapped__.commit) + + def rollback(self): + return self._traced_tx_operation("ROLLBACK", self.__wrapped__.rollback) def __enter__(self): self.__wrapped__.__enter__() @@ -607,13 +660,78 @@ def __exit__(self, *args: Any, **kwargs: Any): self.__wrapped__.__exit__(*args, **kwargs) +class AsyncTracedConnectionProxy(TracedConnectionProxy[ConnectionT]): + async def _traced_tx_operation_async( + self, operation_name: str, operation_method: Callable[[], Awaitable[None]] + ) -> None: + """Execute a traced async transaction operation (commit, rollback).""" + if not is_instrumentation_enabled(): + return await operation_method() + + if not self._self_db_api_integration.enable_transaction_spans: + return await operation_method() + + with self._self_db_api_integration._tracer.start_as_current_span( + operation_name, kind=trace_api.SpanKind.CLIENT + ) as span: + self._self_db_api_integration.populate_common_span_attributes(span) + return await operation_method() + + async def commit(self): + """Async commit for async connections (e.g., psycopg.AsyncConnection).""" + return await self._traced_tx_operation_async("COMMIT", self.__wrapped__.commit) + + async def rollback(self): + """Async rollback for async connections (e.g., psycopg.AsyncConnection).""" + return await self._traced_tx_operation_async("ROLLBACK", self.__wrapped__.rollback) + + # Async context manager support + async def __aenter__(self): + if hasattr(self.__wrapped__, "__aenter__"): + await self.__wrapped__.__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any): + if hasattr(self.__wrapped__, "__aexit__"): + return await self.__wrapped__.__aexit__(*args, **kwargs) + + def get_traced_connection_proxy( connection: ConnectionT, db_api_integration: DatabaseApiIntegration | None, + wrap_cursors: bool = True, *args: Any, **kwargs: Any, ) -> TracedConnectionProxy[ConnectionT]: - return TracedConnectionProxy(connection, db_api_integration) + """Get a traced connection proxy for sync connections. + + Args: + connection: The database connection to wrap. + db_api_integration: The database API integration instance. + wrap_cursors: Whether to wrap cursors returned by connection.cursor(). + Set to False for databases like psycopg/psycopg2 that handle cursor + tracing via cursor_factory. Defaults to True. + """ + return TracedConnectionProxy(connection, db_api_integration, wrap_cursors) + + +def get_traced_async_connection_proxy( + connection: ConnectionT, + db_api_integration: DatabaseApiIntegration | None, + wrap_cursors: bool = True, + *args: Any, + **kwargs: Any, +) -> AsyncTracedConnectionProxy[ConnectionT]: + """Get a traced connection proxy for async connections. + + Args: + connection: The async database connection to wrap. + db_api_integration: The database API integration instance. + wrap_cursors: Whether to wrap cursors returned by connection.cursor(). + Set to False for databases like psycopg/psycopg2 that handle cursor + tracing via cursor_factory. Defaults to True. + """ + return AsyncTracedConnectionProxy(connection, db_api_integration, wrap_cursors) class CursorTracer(Generic[CursorT]): @@ -698,17 +816,12 @@ def _populate_span( ): if not span.is_recording(): return + + self._db_api_integration.populate_common_span_attributes(span) + statement = self.get_statement(cursor, args) - span.set_attribute(DB_SYSTEM, self._db_api_integration.database_system) - span.set_attribute(DB_NAME, self._db_api_integration.database) span.set_attribute(DB_STATEMENT, statement) - for ( - attribute_key, - attribute_value, - ) in self._db_api_integration.span_attributes.items(): - 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])) diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index 27b43f05ad..cdd3ca7c20 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -1061,6 +1061,62 @@ def test_callproc(self): "Test stored procedure", ) + def test_commit(self): + db_integration = dbapi.DatabaseApiIntegration( + "instrumenting_module_test_name", "testcomponent" + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + mock_connection.commit() + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "COMMIT") + + def test_rollback(self): + db_integration = dbapi.DatabaseApiIntegration( + "instrumenting_module_test_name", "testcomponent" + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + mock_connection.rollback() + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "ROLLBACK") + + def test_commit_with_suppress_instrumentation(self): + """Test that commit doesn't create a span when instrumentation is suppressed""" + db_integration = dbapi.DatabaseApiIntegration( + "instrumenting_module_test_name", + "testcomponent", + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + with suppress_instrumentation(): + mock_connection.commit() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + + def test_rollback_with_suppress_instrumentation(self): + """Test that rollback doesn't create a span when instrumentation is suppressed""" + db_integration = dbapi.DatabaseApiIntegration( + "instrumenting_module_test_name", + "testcomponent", + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + with suppress_instrumentation(): + mock_connection.rollback() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 0) + @mock.patch("opentelemetry.instrumentation.dbapi") def test_wrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") @@ -1298,6 +1354,14 @@ def __init__(self, database, server_port, server_host, user): def cursor(self): return MockCursor() + # pylint: disable=no-self-use + def commit(self): + pass + + # pylint: disable=no-self-use + def rollback(self): + pass + class MockCursor: def __init__(self) -> None: diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py index b6c278f50b..929e15ca32 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py @@ -182,6 +182,7 @@ def _instrument(self, **kwargs): enable_attribute_commenter = kwargs.get( "enable_attribute_commenter", False ) + enable_transaction_spans = kwargs.get("enable_transaction_spans", True) dbapi.wrap_connect( __name__, @@ -194,6 +195,7 @@ def _instrument(self, **kwargs): enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) def _uninstrument(self, **kwargs): @@ -208,6 +210,7 @@ def instrument_connection( enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, + enable_transaction_spans=True, ): """Enable instrumentation in a MySQL connection. @@ -225,6 +228,8 @@ def instrument_connection( Optional configurations for tags to be appended at the sql query. enable_attribute_commenter: Optional flag to enable/disable addition of sqlcomment to span attribute (default False). Requires enable_commenter=True. + enable_transaction_spans: + Flag to enable/disable transaction spans (commit/rollback). Defaults to True. Returns: An instrumented MySQL connection with OpenTelemetry tracing enabled. @@ -240,6 +245,7 @@ def instrument_connection( commenter_options=commenter_options, connect_module=mysql.connector, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) def uninstrument_connection(self, connection): diff --git a/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py b/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py index b45e0f97af..5f9fe73a85 100644 --- a/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py @@ -166,6 +166,7 @@ def _instrument(self, **kwargs): # pylint: disable=no-self-use enable_attribute_commenter = kwargs.get( "enable_attribute_commenter", False ) + enable_transaction_spans = kwargs.get("enable_transaction_spans", True) dbapi.wrap_connect( __name__, @@ -178,6 +179,7 @@ def _instrument(self, **kwargs): # pylint: disable=no-self-use enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) def _uninstrument(self, **kwargs): # pylint: disable=no-self-use @@ -191,6 +193,7 @@ def instrument_connection( enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, + enable_transaction_spans=True, ): """Enable instrumentation in a mysqlclient connection. @@ -215,6 +218,8 @@ def instrument_connection( - `mysql_client_version`: Adds the MySQL client version. - `driver_paramstyle`: Adds the parameter style. - `opentelemetry_values`: Includes traceparent values. + enable_transaction_spans: + Flag to enable/disable transaction spans (commit/rollback). Defaults to True. Returns: An instrumented MySQL connection with OpenTelemetry support enabled. """ @@ -230,6 +235,7 @@ def instrument_connection( commenter_options=commenter_options, connect_module=MySQLdb, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) @staticmethod diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py index 28896be138..b938c9f7dd 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py @@ -188,6 +188,7 @@ def _instrument(self, **kwargs: Any): "enable_attribute_commenter", False ) capture_parameters = kwargs.get("capture_parameters", False) + enable_transaction_spans = kwargs.get("enable_transaction_spans", True) dbapi.wrap_connect( __name__, psycopg, @@ -201,6 +202,7 @@ def _instrument(self, **kwargs: Any): commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, + enable_transaction_spans=enable_transaction_spans, ) dbapi.wrap_connect( @@ -216,6 +218,7 @@ def _instrument(self, **kwargs: Any): commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, + enable_transaction_spans=enable_transaction_spans, ) dbapi.wrap_connect( __name__, @@ -230,6 +233,7 @@ def _instrument(self, **kwargs: Any): commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, + enable_transaction_spans=enable_transaction_spans, ) def _uninstrument(self, **kwargs: Any): @@ -309,7 +313,8 @@ def wrapped_connection( kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs) connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) - return connection + # psycopg uses cursor_factory for cursor tracing, so disable cursor wrapping + return dbapi.get_traced_connection_proxy(connection, self, wrap_cursors=False) class DatabaseApiAsyncIntegration(dbapi.DatabaseApiIntegration): @@ -329,7 +334,8 @@ async def wrapped_connection( ) connection = await connect_method(*args, **kwargs) self.get_connection_attributes(connection) - return connection + # psycopg uses cursor_factory for cursor tracing, so disable cursor wrapping + return dbapi.get_traced_async_connection_proxy(connection, self, wrap_cursors=False) class CursorTracer(dbapi.CursorTracer): diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 84d6709bbb..2f96a45d80 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -103,12 +103,6 @@ def get_dsn_parameters(self): # pylint: disable=no-self-use class MockAsyncConnection(psycopg.AsyncConnection): - commit = mock.MagicMock(spec=types.MethodType) - commit.__name__ = "commit" - - rollback = mock.MagicMock(spec=types.MethodType) - rollback.__name__ = "rollback" - def __init__(self, *args, **kwargs): self.cursor_factory = kwargs.pop("cursor_factory", None) @@ -116,6 +110,12 @@ def __init__(self, *args, **kwargs): async def connect(*args, **kwargs): return MockAsyncConnection(**kwargs) + async def commit(self): + pass + + async def rollback(self): + pass + def cursor(self, *args, **kwargs): if self.cursor_factory: cur = self.cursor_factory(self) @@ -441,6 +441,28 @@ def test_uninstrument_connection_with_instrument_connection(self): spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) + def test_commit(self): + PsycopgInstrumentor().instrument() + + cnx = psycopg.connect(database="test") + cnx.commit() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "COMMIT") + + def test_rollback(self): + PsycopgInstrumentor().instrument() + + cnx = psycopg.connect(database="test") + cnx.rollback() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "ROLLBACK") + @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_enabled(self, event_mocked): cnx = psycopg.connect(database="test") @@ -591,6 +613,32 @@ async def test_not_recording_async(self): PsycopgInstrumentor().uninstrument() + async def test_async_commit(self): + PsycopgInstrumentor().instrument() + + cnx = await psycopg.AsyncConnection.connect("test") + await cnx.commit() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "COMMIT") + + PsycopgInstrumentor().uninstrument() + + async def test_async_rollback(self): + PsycopgInstrumentor().instrument() + + cnx = await psycopg.AsyncConnection.connect("test") + await cnx.rollback() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "ROLLBACK") + + PsycopgInstrumentor().uninstrument() + async def test_tracing_is_async(self): PsycopgInstrumentor().instrument() diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py index 4c8b1b6e02..1577c2f45b 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py @@ -227,6 +227,7 @@ def _instrument(self, **kwargs): "enable_attribute_commenter", False ) capture_parameters = kwargs.get("capture_parameters", False) + enable_transaction_spans = kwargs.get("enable_transaction_spans", True) dbapi.wrap_connect( __name__, psycopg2, @@ -240,6 +241,7 @@ def _instrument(self, **kwargs): commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, capture_parameters=capture_parameters, + enable_transaction_spans=enable_transaction_spans, ) def _uninstrument(self, **kwargs): @@ -320,7 +322,8 @@ def wrapped_connection( kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs) connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) - return connection + # psycopg2 uses cursor_factory for cursor tracing, so disable cursor wrapping + return dbapi.get_traced_connection_proxy(connection, self, wrap_cursors=False) class CursorTracer(dbapi.CursorTracer): diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py index 27b8618852..fbad8f5d99 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py @@ -197,6 +197,7 @@ def test_instrument_defaults(self, mock_dbapi): # pylint: disable=no-self-use commenter_options={}, enable_attribute_commenter=False, capture_parameters=False, + enable_transaction_spans=True, ) def test_instrument_capture_parameters(self, mock_dbapi): diff --git a/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py b/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py index 68d29a3c83..9bc401c649 100644 --- a/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py @@ -157,6 +157,7 @@ def _instrument(self, **kwargs): https://github.com/pymssql/pymssql/ """ tracer_provider = kwargs.get("tracer_provider") + enable_transaction_spans = kwargs.get("enable_transaction_spans", True) dbapi.wrap_connect( __name__, @@ -169,6 +170,7 @@ def _instrument(self, **kwargs): # instead, we get the attributes from the connect method (which is done # via PyMSSQLDatabaseApiIntegration.wrapped_connection) db_api_integration_factory=_PyMSSQLDatabaseApiIntegration, + enable_transaction_spans=enable_transaction_spans, ) def _uninstrument(self, **kwargs): @@ -176,13 +178,15 @@ def _uninstrument(self, **kwargs): dbapi.unwrap_connect(pymssql, "connect") @staticmethod - def instrument_connection(connection, tracer_provider=None): + def instrument_connection(connection, tracer_provider=None, enable_transaction_spans=True): """Enable instrumentation in a pymssql connection. Args: connection: The connection to instrument. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. + enable_transaction_spans: Flag to enable/disable transaction spans + (commit/rollback). Defaults to True. Returns: An instrumented connection. @@ -195,6 +199,7 @@ def instrument_connection(connection, tracer_provider=None): version=__version__, tracer_provider=tracer_provider, db_api_integration_factory=_PyMSSQLDatabaseApiIntegration, + enable_transaction_spans=enable_transaction_spans, ) @staticmethod diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py index 2dae113746..f3ef131a81 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py @@ -189,6 +189,7 @@ def _instrument(self, **kwargs): # pylint: disable=no-self-use enable_attribute_commenter = kwargs.get( "enable_attribute_commenter", False ) + enable_transaction_spans = kwargs.get("enable_transaction_spans", True) dbapi.wrap_connect( __name__, @@ -201,6 +202,7 @@ def _instrument(self, **kwargs): # pylint: disable=no-self-use enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) def _uninstrument(self, **kwargs): # pylint: disable=no-self-use @@ -214,6 +216,7 @@ def instrument_connection( enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, + enable_transaction_spans=True, ): """Enable instrumentation in a PyMySQL connection. @@ -232,6 +235,8 @@ def instrument_connection( You can specify various options, such as enabling driver information, database version logging, traceparent propagation, and other customizable metadata enhancements. See *SQLCommenter Configurations* above for more information. + enable_transaction_spans: + A flag to enable/disable transaction spans (commit/rollback). Defaults to True. Returns: An instrumented connection. """ @@ -247,6 +252,7 @@ def instrument_connection( commenter_options=commenter_options, connect_module=pymysql, enable_attribute_commenter=enable_attribute_commenter, + enable_transaction_spans=enable_transaction_spans, ) @staticmethod diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py index 81654746de..627f959834 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py @@ -22,6 +22,7 @@ from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_STATEMENT, + DB_SYSTEM, ) from opentelemetry.test.test_base import TestBase @@ -482,3 +483,56 @@ def test_uninstrument_connection(self, mock_connect): spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 1) + + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_commit(self, mock_connect): + """Test that commit creates a span""" + PyMySQLInstrumentor().instrument() + cnx = pymysql.connect(database="test") + cnx.commit() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "COMMIT") + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(span.attributes[DB_SYSTEM], "mysql") + + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_rollback(self, mock_connect): + """Test that rollback creates a span""" + PyMySQLInstrumentor().instrument() + cnx = pymysql.connect(database="test") + cnx.rollback() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "ROLLBACK") + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(span.attributes[DB_SYSTEM], "mysql") + + @mock.patch("pymysql.connect") + # pylint: disable=unused-argument + def test_commit_and_query(self, mock_connect): + """Test that both execute and commit create spans""" + PyMySQLInstrumentor().instrument() + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + cursor.execute("SELECT * FROM test") + cnx.commit() + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 2) + + # First span should be the SELECT + select_span = spans_list[0] + self.assertEqual(select_span.name, "SELECT") + self.assertIs(select_span.kind, trace_api.SpanKind.CLIENT) + + # Second span should be the COMMIT + commit_span = spans_list[1] + self.assertEqual(commit_span.name, "COMMIT") + self.assertIs(commit_span.kind, trace_api.SpanKind.CLIENT) diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py index 086d47f3f5..97fe67dd1d 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py @@ -89,6 +89,7 @@ def _instrument(self, **kwargs: Any) -> None: https://docs.python.org/3/library/sqlite3.html """ tracer_provider = kwargs.get("tracer_provider") + enable_transaction_spans = kwargs.get("enable_transaction_spans", True) for module in self._TO_WRAP: dbapi.wrap_connect( @@ -99,6 +100,7 @@ def _instrument(self, **kwargs: Any) -> None: _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, + enable_transaction_spans=enable_transaction_spans, ) def _uninstrument(self, **kwargs: Any) -> None: @@ -110,6 +112,7 @@ def _uninstrument(self, **kwargs: Any) -> None: def instrument_connection( connection: SQLite3Connection, tracer_provider: TracerProvider | None = None, + enable_transaction_spans: bool = True, ) -> SQLite3Connection: """Enable instrumentation in a SQLite connection. @@ -117,6 +120,8 @@ def instrument_connection( connection: The connection to instrument. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. + enable_transaction_spans: Flag to enable/disable transaction spans + (commit/rollback). Defaults to True. Returns: An instrumented SQLite connection that supports @@ -130,6 +135,7 @@ def instrument_connection( _CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, + enable_transaction_spans=enable_transaction_spans, ) @staticmethod diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py index 8ff457f195..5d6bf99c5e 100644 --- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -56,28 +56,35 @@ def tearDown(self): PyMySQLInstrumentor().uninstrument() super().tearDown() - def validate_spans(self, span_name): + def validate_spans(self, *span_names): spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 2) + self.assertEqual(len(spans), len(span_names) + 1) # +1 for rootSpan + + root_span = None + db_spans = [] + for span in spans: if span.name == "rootSpan": root_span = span else: - db_span = span + db_spans.append(span) self.assertIsInstance(span.start_time, int) self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) - self.assertIsNotNone(db_span) self.assertEqual(root_span.name, "rootSpan") - self.assertEqual(db_span.name, span_name) - self.assertIsNotNone(db_span.parent) - self.assertIs(db_span.parent, root_span.get_span_context()) - self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(db_span.attributes[DB_SYSTEM], "mysql") - self.assertEqual(db_span.attributes[DB_NAME], MYSQL_DB_NAME) - self.assertEqual(db_span.attributes[DB_USER], MYSQL_USER) - self.assertEqual(db_span.attributes[NET_PEER_NAME], MYSQL_HOST) - self.assertEqual(db_span.attributes[NET_PEER_PORT], MYSQL_PORT) + self.assertEqual(len(db_spans), len(span_names)) + + for db_span, expected_name in zip(db_spans, span_names): + self.assertEqual(db_span.name, expected_name) + self.assertIsNotNone(db_span.parent) + self.assertIs(db_span.parent, root_span.get_span_context()) + self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(db_span.attributes[DB_SYSTEM], "mysql") + self.assertEqual(db_span.attributes[DB_NAME], MYSQL_DB_NAME) + self.assertEqual(db_span.attributes[DB_USER], MYSQL_USER) + self.assertEqual(db_span.attributes[NET_PEER_NAME], MYSQL_HOST) + self.assertEqual(db_span.attributes[NET_PEER_PORT], MYSQL_PORT) def test_execute(self): """Should create a child span for execute""" @@ -122,17 +129,31 @@ def test_callproc(self): self.validate_spans("test") def test_commit(self): + """Should create spans for both INSERT and COMMIT""" stmt = "INSERT INTO test (id) VALUES (%s)" with self._tracer.start_as_current_span("rootSpan"): data = (("4",), ("5",), ("6",)) self._cursor.executemany(stmt, data) self._connection.commit() - self.validate_spans("INSERT") + self.validate_spans("INSERT", "COMMIT") def test_rollback(self): + """Should create spans for both INSERT and ROLLBACK""" stmt = "INSERT INTO test (id) VALUES (%s)" with self._tracer.start_as_current_span("rootSpan"): data = (("7",), ("8",), ("9",)) self._cursor.executemany(stmt, data) self._connection.rollback() - self.validate_spans("INSERT") + self.validate_spans("INSERT", "ROLLBACK") + + def test_commit_only(self): + """Should create a span for standalone COMMIT""" + with self._tracer.start_as_current_span("rootSpan"): + self._connection.commit() + self.validate_spans("COMMIT") + + def test_rollback_only(self): + """Should create a span for standalone ROLLBACK""" + with self._tracer.start_as_current_span("rootSpan"): + self._connection.rollback() + self.validate_spans("ROLLBACK") From a3f82f6fd048072100978e837d72ab0065b67573 Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Thu, 30 Apr 2026 21:23:21 -0400 Subject: [PATCH 2/9] fix CHANGELOG entries: move to Unreleased, update PR ref --- CHANGELOG.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e28874d932..e37289d756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4335](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4335)) - Expand `AGENTS.md` with instrumentation/GenAI guidance and add PR review instructions. ([#4457](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4457)) +- `opentelemetry-instrumentation-dbapi`: Add instrumentation for `commit()` and `rollback()` transaction operations + ([#4519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4519)) +- `opentelemetry-instrumentation-dbapi`: Add `enable_transaction_spans` configuration flag to control transaction span creation (default: `True`) + ([#4519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4519)) +- `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-sqlite3`, `opentelemetry-instrumentation-pymssql`: Add support for transaction span instrumentation via `enable_transaction_spans` parameter + ([#4519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4519)) ### Fixed @@ -59,13 +65,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-sqlalchemy`: implement new semantic convention opt-in migration ([#4110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4110)) -- `opentelemetry-instrumentation-dbapi`: Add instrumentation for `commit()` and `rollback()` transaction operations - ([#3964](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3964)) -- `opentelemetry-instrumentation-dbapi`: Add `enable_transaction_spans` configuration flag to control transaction span creation (default: `True`) - ([#3964](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3964)) -- `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-sqlite3`, `opentelemetry-instrumentation-pymssql`: Add support for transaction span instrumentation via `enable_transaction_spans` parameter - ([#3964](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3964)) - ### Fixed - `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.attributes` From e7c9473bb22673895c7394e52abffbfec54bf590 Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Fri, 1 May 2026 13:33:09 -0400 Subject: [PATCH 3/9] dbapi: set db.operation.name on commit and rollback spans Aligns transaction spans with the OTel semconv db attribute that names the operation. The span name (COMMIT/ROLLBACK) was previously the only way for a backend to know what operation produced the span; the attribute makes it queryable. Assisted-by: Claude Opus 4.7 --- .../src/opentelemetry/instrumentation/dbapi/__init__.py | 9 +++++++-- .../tests/test_dbapi_integration.py | 3 +++ .../tests/test_psycopg_integration.py | 7 +++++++ .../tests/test_pymysql_integration.py | 3 +++ 4 files changed, 20 insertions(+), 2 deletions(-) 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 46d241b6ad..7c549ee191 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -195,6 +195,7 @@ ) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, + DB_OPERATION_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -643,7 +644,9 @@ def _traced_tx_operation( with self._self_db_api_integration._tracer.start_as_current_span( operation_name, kind=trace_api.SpanKind.CLIENT ) as span: - self._self_db_api_integration.populate_common_span_attributes(span) + if span.is_recording(): + self._self_db_api_integration.populate_common_span_attributes(span) + span.set_attribute(DB_OPERATION_NAME, operation_name) return operation_method() def commit(self): @@ -674,7 +677,9 @@ async def _traced_tx_operation_async( with self._self_db_api_integration._tracer.start_as_current_span( operation_name, kind=trace_api.SpanKind.CLIENT ) as span: - self._self_db_api_integration.populate_common_span_attributes(span) + if span.is_recording(): + self._self_db_api_integration.populate_common_span_attributes(span) + span.set_attribute(DB_OPERATION_NAME, operation_name) return await operation_method() async def commit(self): diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index cdd3ca7c20..a97d2a12b6 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_OPERATION_NAME, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -1073,6 +1074,7 @@ def test_commit(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "COMMIT") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") def test_rollback(self): db_integration = dbapi.DatabaseApiIntegration( @@ -1086,6 +1088,7 @@ def test_rollback(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "ROLLBACK") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") def test_commit_with_suppress_instrumentation(self): """Test that commit doesn't create a span when instrumentation is suppressed""" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 2f96a45d80..06928201cf 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_OPERATION_NAME, +) from opentelemetry.test.test_base import TestBase @@ -451,6 +454,7 @@ def test_commit(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "COMMIT") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") def test_rollback(self): PsycopgInstrumentor().instrument() @@ -462,6 +466,7 @@ def test_rollback(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "ROLLBACK") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_enabled(self, event_mocked): @@ -623,6 +628,7 @@ async def test_async_commit(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "COMMIT") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") PsycopgInstrumentor().uninstrument() @@ -636,6 +642,7 @@ async def test_async_rollback(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "ROLLBACK") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") PsycopgInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py index 627f959834..6b163894aa 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py @@ -21,6 +21,7 @@ from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( + DB_OPERATION_NAME, DB_STATEMENT, DB_SYSTEM, ) @@ -498,6 +499,7 @@ def test_commit(self, mock_connect): self.assertEqual(span.name, "COMMIT") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[DB_SYSTEM], "mysql") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") @mock.patch("pymysql.connect") # pylint: disable=unused-argument @@ -513,6 +515,7 @@ def test_rollback(self, mock_connect): self.assertEqual(span.name, "ROLLBACK") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[DB_SYSTEM], "mysql") + self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") @mock.patch("pymysql.connect") # pylint: disable=unused-argument From 2350a9a862a3432b9ba65a38d010179dc8c23726 Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Fri, 1 May 2026 13:46:32 -0400 Subject: [PATCH 4/9] dbapi: default enable_transaction_spans to False (experimental) The OTel DB semconv spec is silent on transaction lifecycle operations (commit/rollback). Java's JDBC instrumentation gates the same behavior behind an experimental opt-in flag; matching that posture here while the spec is unsettled. Existing tests opt in explicitly; CHANGELOG entry consolidated and marked experimental. Assisted-by: Claude Opus 4.7 --- CHANGELOG.md | 6 +----- .../instrumentation/dbapi/__init__.py | 14 +++++++------- .../tests/test_dbapi_integration.py | 10 ++++++++-- .../instrumentation/mysql/__init__.py | 8 +++++--- .../instrumentation/mysqlclient/__init__.py | 8 +++++--- .../instrumentation/psycopg/__init__.py | 4 +++- .../tests/test_psycopg_integration.py | 8 ++++---- .../instrumentation/psycopg2/__init__.py | 4 +++- .../tests/test_psycopg2_instrumentation.py | 2 +- .../instrumentation/pymssql/__init__.py | 12 ++++++++---- .../instrumentation/pymysql/__init__.py | 8 +++++--- .../tests/test_pymysql_integration.py | 6 +++--- .../instrumentation/sqlite3/__init__.py | 10 ++++++---- .../tests/pymysql/test_pymysql_functional.py | 2 +- 14 files changed, 60 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e37289d756..ab3a9e6aef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4335](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4335)) - Expand `AGENTS.md` with instrumentation/GenAI guidance and add PR review instructions. ([#4457](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4457)) -- `opentelemetry-instrumentation-dbapi`: Add instrumentation for `commit()` and `rollback()` transaction operations - ([#4519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4519)) -- `opentelemetry-instrumentation-dbapi`: Add `enable_transaction_spans` configuration flag to control transaction span creation (default: `True`) - ([#4519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4519)) -- `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-sqlite3`, `opentelemetry-instrumentation-pymssql`: Add support for transaction span instrumentation via `enable_transaction_spans` parameter +- `opentelemetry-instrumentation-dbapi`, `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-sqlite3`, `opentelemetry-instrumentation-pymssql`: Add experimental instrumentation for `commit()` and `rollback()` transaction operations behind the `enable_transaction_spans` flag (default: `False`). ([#4519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4519)) ### Fixed 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 7c549ee191..61ca3927f3 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -228,7 +228,7 @@ def trace_integration( db_api_integration_factory: type[DatabaseApiIntegration] | None = None, enable_attribute_commenter: bool = False, commenter_options: dict[str, Any] | None = None, - enable_transaction_spans: bool = True, + enable_transaction_spans: bool = False, ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ @@ -248,7 +248,7 @@ def trace_integration( default one is used. enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True. commenter_options: Configurations for tags to be appended at the sql query. - enable_transaction_spans: Flag to enable/disable transaction spans (commit/rollback). Defaults to True. + enable_transaction_spans: Experimental flag to enable transaction (commit/rollback) spans. Defaults to False. """ wrap_connect( __name__, @@ -280,7 +280,7 @@ def wrap_connect( db_api_integration_factory: type[DatabaseApiIntegration] | None = None, commenter_options: dict[str, Any] | None = None, enable_attribute_commenter: bool = False, - enable_transaction_spans: bool = True, + enable_transaction_spans: bool = False, ): """Integrate with DB API library. https://www.python.org/dev/peps/pep-0249/ @@ -300,7 +300,7 @@ def wrap_connect( default one is used. commenter_options: Configurations for tags to be appended at the sql query. enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True. - enable_transaction_spans: Flag to enable/disable transaction spans (commit/rollback). Defaults to True. + enable_transaction_spans: Experimental flag to enable transaction (commit/rollback) spans. Defaults to False. """ db_api_integration_factory = ( @@ -363,7 +363,7 @@ def instrument_connection( connect_module: Callable[..., Any] | None = None, enable_attribute_commenter: bool = False, db_api_integration_factory: type[DatabaseApiIntegration] | None = None, - enable_transaction_spans: bool = True, + enable_transaction_spans: bool = False, ) -> TracedConnectionProxy[ConnectionT]: """Enable instrumentation in a database connection. @@ -385,7 +385,7 @@ def instrument_connection( replacement for :class:`DatabaseApiIntegration`. Can be used to obtain connection attributes from the connect method instead of from the connection itself (as done by the pymssql intrumentor). - enable_transaction_spans: Flag to enable/disable transaction spans (commit/rollback). Defaults to True. + enable_transaction_spans: Experimental flag to enable transaction (commit/rollback) spans. Defaults to False. Returns: An instrumented connection. @@ -446,7 +446,7 @@ def __init__( commenter_options: dict[str, Any] | None = None, connect_module: Callable[..., Any] | None = None, enable_attribute_commenter: bool = False, - enable_transaction_spans: bool = True, + enable_transaction_spans: bool = False, ): if connection_attributes is None: self.connection_attributes = { diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index a97d2a12b6..e871b3d199 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -1064,7 +1064,9 @@ def test_callproc(self): def test_commit(self): db_integration = dbapi.DatabaseApiIntegration( - "instrumenting_module_test_name", "testcomponent" + "instrumenting_module_test_name", + "testcomponent", + enable_transaction_spans=True, ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, {} @@ -1078,7 +1080,9 @@ def test_commit(self): def test_rollback(self): db_integration = dbapi.DatabaseApiIntegration( - "instrumenting_module_test_name", "testcomponent" + "instrumenting_module_test_name", + "testcomponent", + enable_transaction_spans=True, ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, {} @@ -1095,6 +1099,7 @@ def test_commit_with_suppress_instrumentation(self): db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", + enable_transaction_spans=True, ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, {} @@ -1110,6 +1115,7 @@ def test_rollback_with_suppress_instrumentation(self): db_integration = dbapi.DatabaseApiIntegration( "instrumenting_module_test_name", "testcomponent", + enable_transaction_spans=True, ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, {} diff --git a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py index 929e15ca32..5ea5a5d9b7 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py @@ -182,7 +182,9 @@ def _instrument(self, **kwargs): enable_attribute_commenter = kwargs.get( "enable_attribute_commenter", False ) - enable_transaction_spans = kwargs.get("enable_transaction_spans", True) + enable_transaction_spans = kwargs.get( + "enable_transaction_spans", False + ) dbapi.wrap_connect( __name__, @@ -210,7 +212,7 @@ def instrument_connection( enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, - enable_transaction_spans=True, + enable_transaction_spans=False, ): """Enable instrumentation in a MySQL connection. @@ -229,7 +231,7 @@ def instrument_connection( enable_attribute_commenter: Optional flag to enable/disable addition of sqlcomment to span attribute (default False). Requires enable_commenter=True. enable_transaction_spans: - Flag to enable/disable transaction spans (commit/rollback). Defaults to True. + Experimental flag to enable transaction (commit/rollback) spans. Defaults to False. Returns: An instrumented MySQL connection with OpenTelemetry tracing enabled. diff --git a/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py b/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py index 5f9fe73a85..aebb7ba1ad 100644 --- a/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py @@ -166,7 +166,9 @@ def _instrument(self, **kwargs): # pylint: disable=no-self-use enable_attribute_commenter = kwargs.get( "enable_attribute_commenter", False ) - enable_transaction_spans = kwargs.get("enable_transaction_spans", True) + enable_transaction_spans = kwargs.get( + "enable_transaction_spans", False + ) dbapi.wrap_connect( __name__, @@ -193,7 +195,7 @@ def instrument_connection( enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, - enable_transaction_spans=True, + enable_transaction_spans=False, ): """Enable instrumentation in a mysqlclient connection. @@ -219,7 +221,7 @@ def instrument_connection( - `driver_paramstyle`: Adds the parameter style. - `opentelemetry_values`: Includes traceparent values. enable_transaction_spans: - Flag to enable/disable transaction spans (commit/rollback). Defaults to True. + Experimental flag to enable transaction (commit/rollback) spans. Defaults to False. Returns: An instrumented MySQL connection with OpenTelemetry support enabled. """ diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py index b938c9f7dd..2d4de5e682 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py @@ -188,7 +188,9 @@ def _instrument(self, **kwargs: Any): "enable_attribute_commenter", False ) capture_parameters = kwargs.get("capture_parameters", False) - enable_transaction_spans = kwargs.get("enable_transaction_spans", True) + enable_transaction_spans = kwargs.get( + "enable_transaction_spans", False + ) dbapi.wrap_connect( __name__, psycopg, diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 06928201cf..07be94ce69 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -445,7 +445,7 @@ def test_uninstrument_connection_with_instrument_connection(self): self.assertEqual(len(spans_list), 1) def test_commit(self): - PsycopgInstrumentor().instrument() + PsycopgInstrumentor().instrument(enable_transaction_spans=True) cnx = psycopg.connect(database="test") cnx.commit() @@ -457,7 +457,7 @@ def test_commit(self): self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") def test_rollback(self): - PsycopgInstrumentor().instrument() + PsycopgInstrumentor().instrument(enable_transaction_spans=True) cnx = psycopg.connect(database="test") cnx.rollback() @@ -619,7 +619,7 @@ async def test_not_recording_async(self): PsycopgInstrumentor().uninstrument() async def test_async_commit(self): - PsycopgInstrumentor().instrument() + PsycopgInstrumentor().instrument(enable_transaction_spans=True) cnx = await psycopg.AsyncConnection.connect("test") await cnx.commit() @@ -633,7 +633,7 @@ async def test_async_commit(self): PsycopgInstrumentor().uninstrument() async def test_async_rollback(self): - PsycopgInstrumentor().instrument() + PsycopgInstrumentor().instrument(enable_transaction_spans=True) cnx = await psycopg.AsyncConnection.connect("test") await cnx.rollback() diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py index 1577c2f45b..017fd7ac6d 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py @@ -227,7 +227,9 @@ def _instrument(self, **kwargs): "enable_attribute_commenter", False ) capture_parameters = kwargs.get("capture_parameters", False) - enable_transaction_spans = kwargs.get("enable_transaction_spans", True) + enable_transaction_spans = kwargs.get( + "enable_transaction_spans", False + ) dbapi.wrap_connect( __name__, psycopg2, diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py index fbad8f5d99..c472251dd5 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/tests/test_psycopg2_instrumentation.py @@ -197,7 +197,7 @@ def test_instrument_defaults(self, mock_dbapi): # pylint: disable=no-self-use commenter_options={}, enable_attribute_commenter=False, capture_parameters=False, - enable_transaction_spans=True, + enable_transaction_spans=False, ) def test_instrument_capture_parameters(self, mock_dbapi): diff --git a/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py b/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py index 9bc401c649..6c1ec41dce 100644 --- a/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymssql/src/opentelemetry/instrumentation/pymssql/__init__.py @@ -157,7 +157,9 @@ def _instrument(self, **kwargs): https://github.com/pymssql/pymssql/ """ tracer_provider = kwargs.get("tracer_provider") - enable_transaction_spans = kwargs.get("enable_transaction_spans", True) + enable_transaction_spans = kwargs.get( + "enable_transaction_spans", False + ) dbapi.wrap_connect( __name__, @@ -178,15 +180,17 @@ def _uninstrument(self, **kwargs): dbapi.unwrap_connect(pymssql, "connect") @staticmethod - def instrument_connection(connection, tracer_provider=None, enable_transaction_spans=True): + def instrument_connection( + connection, tracer_provider=None, enable_transaction_spans=False + ): """Enable instrumentation in a pymssql connection. Args: connection: The connection to instrument. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. - enable_transaction_spans: Flag to enable/disable transaction spans - (commit/rollback). Defaults to True. + enable_transaction_spans: Experimental flag to enable transaction + (commit/rollback) spans. Defaults to False. Returns: An instrumented connection. diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py index f3ef131a81..436957f246 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/src/opentelemetry/instrumentation/pymysql/__init__.py @@ -189,7 +189,9 @@ def _instrument(self, **kwargs): # pylint: disable=no-self-use enable_attribute_commenter = kwargs.get( "enable_attribute_commenter", False ) - enable_transaction_spans = kwargs.get("enable_transaction_spans", True) + enable_transaction_spans = kwargs.get( + "enable_transaction_spans", False + ) dbapi.wrap_connect( __name__, @@ -216,7 +218,7 @@ def instrument_connection( enable_commenter=None, commenter_options=None, enable_attribute_commenter=None, - enable_transaction_spans=True, + enable_transaction_spans=False, ): """Enable instrumentation in a PyMySQL connection. @@ -236,7 +238,7 @@ def instrument_connection( traceparent propagation, and other customizable metadata enhancements. See *SQLCommenter Configurations* above for more information. enable_transaction_spans: - A flag to enable/disable transaction spans (commit/rollback). Defaults to True. + Experimental flag to enable transaction (commit/rollback) spans. Defaults to False. Returns: An instrumented connection. """ diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py index 6b163894aa..74a3a815b9 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py @@ -489,7 +489,7 @@ def test_uninstrument_connection(self, mock_connect): # pylint: disable=unused-argument def test_commit(self, mock_connect): """Test that commit creates a span""" - PyMySQLInstrumentor().instrument() + PyMySQLInstrumentor().instrument(enable_transaction_spans=True) cnx = pymysql.connect(database="test") cnx.commit() @@ -505,7 +505,7 @@ def test_commit(self, mock_connect): # pylint: disable=unused-argument def test_rollback(self, mock_connect): """Test that rollback creates a span""" - PyMySQLInstrumentor().instrument() + PyMySQLInstrumentor().instrument(enable_transaction_spans=True) cnx = pymysql.connect(database="test") cnx.rollback() @@ -521,7 +521,7 @@ def test_rollback(self, mock_connect): # pylint: disable=unused-argument def test_commit_and_query(self, mock_connect): """Test that both execute and commit create spans""" - PyMySQLInstrumentor().instrument() + PyMySQLInstrumentor().instrument(enable_transaction_spans=True) cnx = pymysql.connect(database="test") cursor = cnx.cursor() cursor.execute("SELECT * FROM test") diff --git a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py index 97fe67dd1d..85c7d48f38 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-sqlite3/src/opentelemetry/instrumentation/sqlite3/__init__.py @@ -89,7 +89,9 @@ def _instrument(self, **kwargs: Any) -> None: https://docs.python.org/3/library/sqlite3.html """ tracer_provider = kwargs.get("tracer_provider") - enable_transaction_spans = kwargs.get("enable_transaction_spans", True) + enable_transaction_spans = kwargs.get( + "enable_transaction_spans", False + ) for module in self._TO_WRAP: dbapi.wrap_connect( @@ -112,7 +114,7 @@ def _uninstrument(self, **kwargs: Any) -> None: def instrument_connection( connection: SQLite3Connection, tracer_provider: TracerProvider | None = None, - enable_transaction_spans: bool = True, + enable_transaction_spans: bool = False, ) -> SQLite3Connection: """Enable instrumentation in a SQLite connection. @@ -120,8 +122,8 @@ def instrument_connection( connection: The connection to instrument. tracer_provider: The optional tracer provider to use. If omitted the current globally configured one is used. - enable_transaction_spans: Flag to enable/disable transaction spans - (commit/rollback). Defaults to True. + enable_transaction_spans: Experimental flag to enable transaction + (commit/rollback) spans. Defaults to False. Returns: An instrumented SQLite connection that supports diff --git a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py index 5d6bf99c5e..97828d2903 100644 --- a/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py +++ b/tests/opentelemetry-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -40,7 +40,7 @@ class TestFunctionalPyMysql(TestBase): def setUp(self): super().setUp() self._tracer = self.tracer_provider.get_tracer(__name__) - PyMySQLInstrumentor().instrument() + PyMySQLInstrumentor().instrument(enable_transaction_spans=True) self._connection = pymy.connect( user=MYSQL_USER, password=MYSQL_PASSWORD, From 9633e2a752f8867a9ef2972671fa4660a9ddb947 Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Fri, 8 May 2026 18:29:51 -0400 Subject: [PATCH 5/9] dbapi: use db.operation on transaction spans The rest of the dbapi instrumentation (DB_NAME, DB_SYSTEM, DB_STATEMENT) uses the pre-migration semconv attributes. Use the matching pre-migration DB_OPERATION (db.operation) on the new commit/rollback spans instead of the post-migration DB_OPERATION_NAME (db.operation.name) for consistency, as suggested by lmolkova. Migration to the new database semantic conventions is out of scope for this PR. Assisted-by: Claude Opus 4.7 --- .../opentelemetry/instrumentation/dbapi/__init__.py | 6 +++--- .../tests/test_dbapi_integration.py | 6 +++--- .../tests/test_psycopg_integration.py | 10 +++++----- .../tests/test_pymysql_integration.py | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) 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 61ca3927f3..a435a5a852 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -195,7 +195,7 @@ ) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, - DB_OPERATION_NAME, + DB_OPERATION, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -646,7 +646,7 @@ def _traced_tx_operation( ) as span: if span.is_recording(): self._self_db_api_integration.populate_common_span_attributes(span) - span.set_attribute(DB_OPERATION_NAME, operation_name) + span.set_attribute(DB_OPERATION, operation_name) return operation_method() def commit(self): @@ -679,7 +679,7 @@ async def _traced_tx_operation_async( ) as span: if span.is_recording(): self._self_db_api_integration.populate_common_span_attributes(span) - span.set_attribute(DB_OPERATION_NAME, operation_name) + span.set_attribute(DB_OPERATION, operation_name) return await operation_method() async def commit(self): diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index e871b3d199..4e729933f2 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -26,7 +26,7 @@ from opentelemetry.semconv._incubating.attributes import net_attributes from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, - DB_OPERATION_NAME, + DB_OPERATION, DB_STATEMENT, DB_SYSTEM, DB_USER, @@ -1076,7 +1076,7 @@ def test_commit(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "COMMIT") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") + self.assertEqual(span.attributes[DB_OPERATION], "COMMIT") def test_rollback(self): db_integration = dbapi.DatabaseApiIntegration( @@ -1092,7 +1092,7 @@ def test_rollback(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "ROLLBACK") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") + self.assertEqual(span.attributes[DB_OPERATION], "ROLLBACK") def test_commit_with_suppress_instrumentation(self): """Test that commit doesn't create a span when instrumentation is suppressed""" diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 07be94ce69..6d2ecefdde 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -23,7 +23,7 @@ from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( - DB_OPERATION_NAME, + DB_OPERATION, ) from opentelemetry.test.test_base import TestBase @@ -454,7 +454,7 @@ def test_commit(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "COMMIT") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") + self.assertEqual(span.attributes[DB_OPERATION], "COMMIT") def test_rollback(self): PsycopgInstrumentor().instrument(enable_transaction_spans=True) @@ -466,7 +466,7 @@ def test_rollback(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "ROLLBACK") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") + self.assertEqual(span.attributes[DB_OPERATION], "ROLLBACK") @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_enabled(self, event_mocked): @@ -628,7 +628,7 @@ async def test_async_commit(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "COMMIT") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") + self.assertEqual(span.attributes[DB_OPERATION], "COMMIT") PsycopgInstrumentor().uninstrument() @@ -642,7 +642,7 @@ async def test_async_rollback(self): self.assertEqual(len(spans_list), 1) span = spans_list[0] self.assertEqual(span.name, "ROLLBACK") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") + self.assertEqual(span.attributes[DB_OPERATION], "ROLLBACK") PsycopgInstrumentor().uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py index 74a3a815b9..e6d3f04255 100644 --- a/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py @@ -21,7 +21,7 @@ from opentelemetry.instrumentation.pymysql import PyMySQLInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( - DB_OPERATION_NAME, + DB_OPERATION, DB_STATEMENT, DB_SYSTEM, ) @@ -499,7 +499,7 @@ def test_commit(self, mock_connect): self.assertEqual(span.name, "COMMIT") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[DB_SYSTEM], "mysql") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "COMMIT") + self.assertEqual(span.attributes[DB_OPERATION], "COMMIT") @mock.patch("pymysql.connect") # pylint: disable=unused-argument @@ -515,7 +515,7 @@ def test_rollback(self, mock_connect): self.assertEqual(span.name, "ROLLBACK") self.assertIs(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes[DB_SYSTEM], "mysql") - self.assertEqual(span.attributes[DB_OPERATION_NAME], "ROLLBACK") + self.assertEqual(span.attributes[DB_OPERATION], "ROLLBACK") @mock.patch("pymysql.connect") # pylint: disable=unused-argument From 7f34168c5f4cf344a91a9556d0414ad1a95dbcaa Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Fri, 8 May 2026 18:46:09 -0400 Subject: [PATCH 6/9] dbapi: record error.type when commit/rollback raises Catch exceptions raised by the underlying connection during commit() or rollback(), set the error.type span attribute (per the new database semantic conventions) to the exception's qualified class name, and re-raise. Span status is set to ERROR and the exception is recorded as an event by the start_as_current_span context manager. This matches the pattern used in the click and asyncclick instrumentations. Add failure tests in test_dbapi_integration.py covering the sync path and in test_psycopg_integration.py covering both the sync and async paths. The tests use a method-level throw_exception kwarg on the mock connection methods, mirroring the existing convention used by mock cursor methods. The TracedConnectionProxy commit/rollback methods now forward *args/**kwargs to the underlying connection so the test kwarg reaches the mock; this matches how the proxy already forwards args for cursor(). Suggested by lmolkova. Assisted-by: Claude Opus 4.7 --- .../instrumentation/dbapi/__init__.py | 61 ++++++++---- .../tests/test_dbapi_integration.py | 53 ++++++++++- .../tests/test_psycopg_integration.py | 94 +++++++++++++++++-- 3 files changed, 177 insertions(+), 31 deletions(-) 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 a435a5a852..409f37e6b0 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -204,6 +204,7 @@ NET_PEER_NAME, NET_PEER_PORT, ) +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.trace import SpanKind, TracerProvider, get_tracer from opentelemetry.util._importlib_metadata import version as util_version @@ -632,14 +633,18 @@ def cursor(self, *args: Any, **kwargs: Any): return get_traced_cursor_proxy(cursor, self._self_db_api_integration) def _traced_tx_operation( - self, operation_name: str, operation_method: Callable[[], None] + self, + operation_name: str, + operation_method: Callable[..., None], + *args: Any, + **kwargs: Any, ) -> None: """Execute a traced transaction operation (commit, rollback).""" if not is_instrumentation_enabled(): - return operation_method() + return operation_method(*args, **kwargs) if not self._self_db_api_integration.enable_transaction_spans: - return operation_method() + return operation_method(*args, **kwargs) with self._self_db_api_integration._tracer.start_as_current_span( operation_name, kind=trace_api.SpanKind.CLIENT @@ -647,13 +652,22 @@ def _traced_tx_operation( if span.is_recording(): self._self_db_api_integration.populate_common_span_attributes(span) span.set_attribute(DB_OPERATION, operation_name) - return operation_method() - - def commit(self): - return self._traced_tx_operation("COMMIT", self.__wrapped__.commit) + try: + return operation_method(*args, **kwargs) + except Exception as exc: + if span.is_recording(): + span.set_attribute(ERROR_TYPE, type(exc).__qualname__) + raise + + def commit(self, *args: Any, **kwargs: Any): + return self._traced_tx_operation( + "COMMIT", self.__wrapped__.commit, *args, **kwargs + ) - def rollback(self): - return self._traced_tx_operation("ROLLBACK", self.__wrapped__.rollback) + def rollback(self, *args: Any, **kwargs: Any): + return self._traced_tx_operation( + "ROLLBACK", self.__wrapped__.rollback, *args, **kwargs + ) def __enter__(self): self.__wrapped__.__enter__() @@ -665,14 +679,18 @@ def __exit__(self, *args: Any, **kwargs: Any): class AsyncTracedConnectionProxy(TracedConnectionProxy[ConnectionT]): async def _traced_tx_operation_async( - self, operation_name: str, operation_method: Callable[[], Awaitable[None]] + self, + operation_name: str, + operation_method: Callable[..., Awaitable[None]], + *args: Any, + **kwargs: Any, ) -> None: """Execute a traced async transaction operation (commit, rollback).""" if not is_instrumentation_enabled(): - return await operation_method() + return await operation_method(*args, **kwargs) if not self._self_db_api_integration.enable_transaction_spans: - return await operation_method() + return await operation_method(*args, **kwargs) with self._self_db_api_integration._tracer.start_as_current_span( operation_name, kind=trace_api.SpanKind.CLIENT @@ -680,15 +698,24 @@ async def _traced_tx_operation_async( if span.is_recording(): self._self_db_api_integration.populate_common_span_attributes(span) span.set_attribute(DB_OPERATION, operation_name) - return await operation_method() + try: + return await operation_method(*args, **kwargs) + except Exception as exc: + if span.is_recording(): + span.set_attribute(ERROR_TYPE, type(exc).__qualname__) + raise - async def commit(self): + async def commit(self, *args: Any, **kwargs: Any): """Async commit for async connections (e.g., psycopg.AsyncConnection).""" - return await self._traced_tx_operation_async("COMMIT", self.__wrapped__.commit) + return await self._traced_tx_operation_async( + "COMMIT", self.__wrapped__.commit, *args, **kwargs + ) - async def rollback(self): + async def rollback(self, *args: Any, **kwargs: Any): """Async rollback for async connections (e.g., psycopg.AsyncConnection).""" - return await self._traced_tx_operation_async("ROLLBACK", self.__wrapped__.rollback) + return await self._traced_tx_operation_async( + "ROLLBACK", self.__wrapped__.rollback, *args, **kwargs + ) # Async context manager support async def __aenter__(self): diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index 4e729933f2..30b663cff0 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -35,6 +35,7 @@ NET_PEER_NAME, NET_PEER_PORT, ) +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.test.test_base import TestBase @@ -1126,6 +1127,48 @@ def test_rollback_with_suppress_instrumentation(self): spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) + def test_commit_failed(self): + db_integration = dbapi.DatabaseApiIntegration( + "instrumenting_module_test_name", + "testcomponent", + enable_transaction_spans=True, + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + with self.assertRaises(Exception): + mock_connection.commit(throw_exception=True) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "COMMIT") + self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) + self.assertEqual(span.attributes[ERROR_TYPE], "Exception") + self.assertEqual(len(span.events), 1) + self.assertEqual(span.events[0].name, "exception") + + def test_rollback_failed(self): + db_integration = dbapi.DatabaseApiIntegration( + "instrumenting_module_test_name", + "testcomponent", + enable_transaction_spans=True, + ) + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} + ) + with self.assertRaises(Exception): + mock_connection.rollback(throw_exception=True) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "ROLLBACK") + self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) + self.assertEqual(span.attributes[ERROR_TYPE], "Exception") + self.assertEqual(len(span.events), 1) + self.assertEqual(span.events[0].name, "exception") + @mock.patch("opentelemetry.instrumentation.dbapi") def test_wrap_connect(self, mock_dbapi): dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-") @@ -1364,12 +1407,14 @@ def cursor(self): return MockCursor() # pylint: disable=no-self-use - def commit(self): - pass + def commit(self, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") # pylint: disable=no-self-use - def rollback(self): - pass + def rollback(self, throw_exception=False): + if throw_exception: + raise Exception("Test Exception") class MockCursor: diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py index 6d2ecefdde..2d4acf3db7 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/tests/test_psycopg_integration.py @@ -20,11 +20,13 @@ from psycopg.sql import SQL, Composed import opentelemetry.instrumentation.psycopg +from opentelemetry import trace as trace_api from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor from opentelemetry.sdk import resources from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_OPERATION, ) +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.test.test_base import TestBase @@ -87,12 +89,6 @@ def close(self): class MockConnection: - commit = mock.MagicMock(spec=types.MethodType) - commit.__name__ = "commit" - - rollback = mock.MagicMock(spec=types.MethodType) - rollback.__name__ = "rollback" - def __init__(self, *args, **kwargs): self.cursor_factory = kwargs.pop("cursor_factory", None) @@ -101,6 +97,14 @@ def cursor(self): return self.cursor_factory(self) return MockCursor() + def commit(self, throw_exception=False): # pylint: disable=no-self-use + if throw_exception: + raise psycopg.Error("Test Exception") + + def rollback(self, throw_exception=False): # pylint: disable=no-self-use + if throw_exception: + raise psycopg.Error("Test Exception") + def get_dsn_parameters(self): # pylint: disable=no-self-use return {"dbname": "test"} @@ -113,11 +117,13 @@ def __init__(self, *args, **kwargs): async def connect(*args, **kwargs): return MockAsyncConnection(**kwargs) - async def commit(self): - pass + async def commit(self, throw_exception=False): + if throw_exception: + raise psycopg.Error("Test Exception") - async def rollback(self): - pass + async def rollback(self, throw_exception=False): + if throw_exception: + raise psycopg.Error("Test Exception") def cursor(self, *args, **kwargs): if self.cursor_factory: @@ -468,6 +474,38 @@ def test_rollback(self): self.assertEqual(span.name, "ROLLBACK") self.assertEqual(span.attributes[DB_OPERATION], "ROLLBACK") + def test_commit_failed(self): + PsycopgInstrumentor().instrument(enable_transaction_spans=True) + + cnx = psycopg.connect(database="test") + with self.assertRaises(psycopg.Error): + cnx.commit(throw_exception=True) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "COMMIT") + self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) + self.assertEqual(span.attributes[ERROR_TYPE], "Error") + self.assertEqual(len(span.events), 1) + self.assertEqual(span.events[0].name, "exception") + + def test_rollback_failed(self): + PsycopgInstrumentor().instrument(enable_transaction_spans=True) + + cnx = psycopg.connect(database="test") + with self.assertRaises(psycopg.Error): + cnx.rollback(throw_exception=True) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "ROLLBACK") + self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) + self.assertEqual(span.attributes[ERROR_TYPE], "Error") + self.assertEqual(len(span.events), 1) + self.assertEqual(span.events[0].name, "exception") + @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") def test_sqlcommenter_enabled(self, event_mocked): cnx = psycopg.connect(database="test") @@ -646,6 +684,42 @@ async def test_async_rollback(self): PsycopgInstrumentor().uninstrument() + async def test_async_commit_failed(self): + PsycopgInstrumentor().instrument(enable_transaction_spans=True) + + cnx = await psycopg.AsyncConnection.connect("test") + with self.assertRaises(psycopg.Error): + await cnx.commit(throw_exception=True) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "COMMIT") + self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) + self.assertEqual(span.attributes[ERROR_TYPE], "Error") + self.assertEqual(len(span.events), 1) + self.assertEqual(span.events[0].name, "exception") + + PsycopgInstrumentor().uninstrument() + + async def test_async_rollback_failed(self): + PsycopgInstrumentor().instrument(enable_transaction_spans=True) + + cnx = await psycopg.AsyncConnection.connect("test") + with self.assertRaises(psycopg.Error): + await cnx.rollback(throw_exception=True) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + self.assertEqual(span.name, "ROLLBACK") + self.assertIs(span.status.status_code, trace_api.StatusCode.ERROR) + self.assertEqual(span.attributes[ERROR_TYPE], "Error") + self.assertEqual(len(span.events), 1) + self.assertEqual(span.events[0].name, "exception") + + PsycopgInstrumentor().uninstrument() + async def test_tracing_is_async(self): PsycopgInstrumentor().instrument() From a69e0e154baf5dd2b471389ec33a9c33a4ed8205 Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Tue, 12 May 2026 22:01:16 -0400 Subject: [PATCH 7/9] use towncrier changelog fragment instead of CHANGELOG.md edit Per maintainer feedback on #4519, the project moved to towncrier for changelog management. Add the entry as a fragment under .changelog/ and revert the direct CHANGELOG.md modification. Assisted-by: Claude Opus 4.7 --- .changelog/4519.added | 1 + CHANGELOG.md | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 .changelog/4519.added diff --git a/.changelog/4519.added b/.changelog/4519.added new file mode 100644 index 0000000000..7ca83b58cb --- /dev/null +++ b/.changelog/4519.added @@ -0,0 +1 @@ +`opentelemetry-instrumentation-dbapi`, `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-sqlite3`, `opentelemetry-instrumentation-pymssql`: add experimental instrumentation for `commit()` and `rollback()` transaction operations behind the `enable_transaction_spans` flag (default: `False`). diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3a9e6aef..93f0cab4fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4335](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4335)) - Expand `AGENTS.md` with instrumentation/GenAI guidance and add PR review instructions. ([#4457](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4457)) -- `opentelemetry-instrumentation-dbapi`, `opentelemetry-instrumentation-pymysql`, `opentelemetry-instrumentation-mysql`, `opentelemetry-instrumentation-mysqlclient`, `opentelemetry-instrumentation-psycopg`, `opentelemetry-instrumentation-psycopg2`, `opentelemetry-instrumentation-sqlite3`, `opentelemetry-instrumentation-pymssql`: Add experimental instrumentation for `commit()` and `rollback()` transaction operations behind the `enable_transaction_spans` flag (default: `False`). - ([#4519](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4519)) ### Fixed From 7257082bb82542bc23dc94bd3c5b3db0d4c3cbb9 Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Tue, 9 Jun 2026 14:07:10 -0400 Subject: [PATCH 8/9] fix lint and formatting findings from CI - make enable_transaction_spans keyword-only on wrap_connect and instrument_connection to stay under max-positional-arguments - make wrap_cursors keyword-only on the connection proxy helpers to avoid keyword-arg-before-vararg - suppress broad-exception-raised on the mock connection commit/rollback, matching the existing mock cursor pattern - add too-many-lines module pragma - apply ruff-format Assisted-by: Claude Opus 4.8 --- .../opentelemetry/instrumentation/dbapi/__init__.py | 11 ++++++++--- .../tests/test_dbapi_integration.py | 2 ++ .../opentelemetry/instrumentation/psycopg/__init__.py | 8 ++++++-- .../instrumentation/psycopg2/__init__.py | 4 +++- 4 files changed, 19 insertions(+), 6 deletions(-) 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 2a3e8369d0..2e7fccffc5 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py @@ -1,5 +1,6 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +# pylint: disable=too-many-lines """ The trace integration with Database API supports libraries that follow the @@ -272,6 +273,7 @@ def wrap_connect( db_api_integration_factory: type[DatabaseApiIntegration] | None = None, commenter_options: dict[str, Any] | None = None, enable_attribute_commenter: bool = False, + *, enable_transaction_spans: bool = False, ): """Integrate with DB API library. @@ -355,6 +357,7 @@ def instrument_connection( connect_module: Callable[..., Any] | None = None, enable_attribute_commenter: bool = False, db_api_integration_factory: type[DatabaseApiIntegration] | None = None, + *, enable_transaction_spans: bool = False, ) -> TracedConnectionProxy[ConnectionT]: """Enable instrumentation in a database connection. @@ -748,8 +751,8 @@ async def __aexit__(self, *args: Any, **kwargs: Any): def get_traced_connection_proxy( connection: ConnectionT, db_api_integration: DatabaseApiIntegration | None, - wrap_cursors: bool = True, *args: Any, + wrap_cursors: bool = True, **kwargs: Any, ) -> TracedConnectionProxy[ConnectionT]: """Get a traced connection proxy for sync connections. @@ -767,8 +770,8 @@ def get_traced_connection_proxy( def get_traced_async_connection_proxy( connection: ConnectionT, db_api_integration: DatabaseApiIntegration | None, - wrap_cursors: bool = True, *args: Any, + wrap_cursors: bool = True, **kwargs: Any, ) -> AsyncTracedConnectionProxy[ConnectionT]: """Get a traced connection proxy for async connections. @@ -780,7 +783,9 @@ def get_traced_async_connection_proxy( Set to False for databases like psycopg/psycopg2 that handle cursor tracing via cursor_factory. Defaults to True. """ - return AsyncTracedConnectionProxy(connection, db_api_integration, wrap_cursors) + return AsyncTracedConnectionProxy( + connection, db_api_integration, wrap_cursors + ) class CursorTracer(Generic[CursorT]): diff --git a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py index 2515128634..6f027af0b6 100644 --- a/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py +++ b/instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py @@ -1747,11 +1747,13 @@ def cursor(self): # pylint: disable=no-self-use def commit(self, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") # pylint: disable=no-self-use def rollback(self, throw_exception=False): if throw_exception: + # pylint: disable=broad-exception-raised raise Exception("Test Exception") diff --git a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py index c1cfd7cecf..ae4a3aa2d8 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg/src/opentelemetry/instrumentation/psycopg/__init__.py @@ -305,7 +305,9 @@ def wrapped_connection( connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) # psycopg uses cursor_factory for cursor tracing, so disable cursor wrapping - return dbapi.get_traced_connection_proxy(connection, self, wrap_cursors=False) + return dbapi.get_traced_connection_proxy( + connection, self, wrap_cursors=False + ) class DatabaseApiAsyncIntegration(dbapi.DatabaseApiIntegration): @@ -326,7 +328,9 @@ async def wrapped_connection( connection = await connect_method(*args, **kwargs) self.get_connection_attributes(connection) # psycopg uses cursor_factory for cursor tracing, so disable cursor wrapping - return dbapi.get_traced_async_connection_proxy(connection, self, wrap_cursors=False) + return dbapi.get_traced_async_connection_proxy( + connection, self, wrap_cursors=False + ) class CursorTracer(dbapi.CursorTracer): diff --git a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py index e27f6d6c67..f646f3d770 100644 --- a/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-psycopg2/src/opentelemetry/instrumentation/psycopg2/__init__.py @@ -314,7 +314,9 @@ def wrapped_connection( connection = connect_method(*args, **kwargs) self.get_connection_attributes(connection) # psycopg2 uses cursor_factory for cursor tracing, so disable cursor wrapping - return dbapi.get_traced_connection_proxy(connection, self, wrap_cursors=False) + return dbapi.get_traced_connection_proxy( + connection, self, wrap_cursors=False + ) class CursorTracer(dbapi.CursorTracer): From 6f58a60183770104116ac7676dc98ddce9da83f4 Mon Sep 17 00:00:00 2001 From: Bradley Walters Date: Sat, 1 Aug 2026 12:15:08 -0700 Subject: [PATCH 9/9] rename changelog fragment to match new PR number Assisted-by: Claude Opus 5 --- .changelog/{4519.added => 4906.added} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{4519.added => 4906.added} (100%) diff --git a/.changelog/4519.added b/.changelog/4906.added similarity index 100% rename from .changelog/4519.added rename to .changelog/4906.added