From 2efc46c1cc019e84f12f3eed3f28b460be5b94f2 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 2 Jan 2025 12:40:57 -0800 Subject: [PATCH 1/4] Add mysql-connector sqlcomment support --- .../instrumentation/mysql/__init__.py | 87 ++++++++++++++++++- .../tests/test_mysql_integration.py | 36 ++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) 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 0116dab1c3..13a43ccf89 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py @@ -50,6 +50,75 @@ cursor.close() instrumented_cnx.close() +SQLCOMMENTER +***************************************** +You can optionally configure mysql-connector instrumentation to enable sqlcommenter which enriches the query with contextual information. + +Usage +----- + +.. code:: python + + import mysql.connector + from opentelemetry.instrumentation.mysql import MySQLInstrumentor + + MySQLInstrumentor().instrument(enable_commenter=True, commenter_options={}) + + cnx = mysql.connector.connect(database="MySQL_Database") + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)") + cursor.close() + cnx.close() + + +For example, +:: + + Invoking cursor.execute("INSERT INTO test (testField) VALUES (123)") will lead to sql query "INSERT INTO test (testField) VALUES (123)" but when SQLCommenter is enabled + the query will get appended with some configurable tags like "INSERT INTO test (testField) VALUES (123) /*tag=value*/;" + +**WARNING:** sqlcommenter for mysql-connector instrumentation should NOT be used if your application initializes cursors with ``prepared=True``, which will natively prepare and execute MySQL statements. Adding sqlcommenting will introduce a severe performance penalty by repeating ``Prepare`` of statements by mysql-connector that are made unique by traceparent in sqlcomment. The penalty does not happen if cursor ``prepared=False`` (default) and instrumentor ``enable_commenter=True``. + +SQLCommenter Configurations +*************************** +We can configure the tags to be appended to the sqlquery log by adding configuration inside commenter_options(default:{}) keyword + +db_driver = True(Default) or False + +For example, +:: +Enabling this flag will add mysql.connector and its version, e.g. /*mysql.connector%%3A1.2.3*/ + +dbapi_threadsafety = True(Default) or False + +For example, +:: +Enabling this flag will add threadsafety /*dbapi_threadsafety=2*/ + +dbapi_level = True(Default) or False + +For example, +:: +Enabling this flag will add dbapi_level /*dbapi_level='2.0'*/ + +mysql_client_version = True(Default) or False + +For example, +:: +Enabling this flag will add mysql_client_version /*mysql_client_version='123'*/ + +driver_paramstyle = True(Default) or False + +For example, +:: +Enabling this flag will add driver_paramstyle /*driver_paramstyle='pyformat'*/ + +opentelemetry_values = True(Default) or False + +For example, +:: +Enabling this flag will add traceparent values /*traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'*/ + API --- """ @@ -82,6 +151,8 @@ def _instrument(self, **kwargs): https://dev.mysql.com/doc/connector-python/en/ """ tracer_provider = kwargs.get("tracer_provider") + enable_sqlcommenter = kwargs.get("enable_commenter", False) + commenter_options = kwargs.get("commenter_options", {}) dbapi.wrap_connect( __name__, @@ -91,6 +162,8 @@ def _instrument(self, **kwargs): self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, + enable_commenter=enable_sqlcommenter, + commenter_options=commenter_options, ) def _uninstrument(self, **kwargs): @@ -98,7 +171,13 @@ def _uninstrument(self, **kwargs): dbapi.unwrap_connect(mysql.connector, "connect") # pylint:disable=no-self-use - def instrument_connection(self, connection, tracer_provider=None): + def instrument_connection( + self, + connection, + tracer_provider=None, + enable_commenter=None, + commenter_options=None, + ): """Enable instrumentation in a MySQL connection. Args: @@ -109,6 +188,10 @@ def instrument_connection(self, connection, tracer_provider=None): tracer_provider: An optional `TracerProvider` instance to use for tracing. If not provided, the globally configured tracer provider will be automatically used. + enable_commenter: + Optional flag to enable/disable sqlcommenter (default False). + commenter_options: + Optional configurations for tags to be appended at the sql query. Returns: An instrumented MySQL connection with OpenTelemetry tracing enabled. @@ -120,6 +203,8 @@ def instrument_connection(self, connection, tracer_provider=None): self._CONNECTION_ATTRIBUTES, version=__version__, tracer_provider=tracer_provider, + enable_commenter=enable_commenter, + commenter_options=commenter_options, ) def uninstrument_connection(self, connection): diff --git a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py index 79399cce7f..b7c06f176d 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py @@ -102,6 +102,42 @@ def test_instrument_connection_no_op_tracer_provider(self, mock_connect): spans_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans_list), 0) + @mock.patch("opentelemetry.instrumentation.dbapi.instrument_connection") + @mock.patch("mysql.connector.connect") + # pylint: disable=unused-argument + def test_instrument_connection_enable_commenter( + self, + mock_connect, + mock_instrument_connection, + ): + cnx, query = connect_and_execute_query() + cnx = MySQLInstrumentor().instrument_connection( + cnx, + enable_commenter=True, + commenter_options={"foo": True}, + ) + cursor = cnx.cursor() + cursor.execute(query) + kwargs = mock_instrument_connection.call_args[1] + self.assertEqual(kwargs["enable_commenter"], True) + self.assertEqual(kwargs["commenter_options"], {"foo": True}) + + @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") + @mock.patch("mysql.connector.connect") + # pylint: disable=unused-argument + def test__instrument_enable_commenter( + self, + mock_connect, + mock_wrap_connect, + ): + MySQLInstrumentor()._instrument( + enable_commenter=True, + commenter_options={"foo": True}, + ) + kwargs = mock_wrap_connect.call_args[1] + self.assertEqual(kwargs["enable_commenter"], True) + self.assertEqual(kwargs["commenter_options"], {"foo": True}) + @mock.patch("mysql.connector.connect") # pylint: disable=unused-argument def test_uninstrument_connection(self, mock_connect): From 8ff93f5538d1d03226b6d0ddd01d03c328a82398 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 2 Jan 2025 12:47:12 -0800 Subject: [PATCH 2/4] Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d17c18687c..126dfafd6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3111](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3111)) - `opentelemetry-instrumentation-falcon` add support version to v4 ([#3086](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3086)) +- `opentelemetry-instrumentation-mysql` Add sqlcommenter support + ([#3163](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3163)) ### Fixed From 73a841fb479d6d2d2bd293ece6755d5f59488104 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 2 Jan 2025 14:42:58 -0800 Subject: [PATCH 3/4] Fix call and add tests --- .../instrumentation/mysql/__init__.py | 1 + .../tests/test_mysql_integration.py | 218 +++++++++++++++++- 2 files changed, 213 insertions(+), 6 deletions(-) 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 13a43ccf89..9a1e851ef1 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py @@ -205,6 +205,7 @@ def instrument_connection( tracer_provider=tracer_provider, enable_commenter=enable_commenter, commenter_options=commenter_options, + connect_module=mysql.connector, ) def uninstrument_connection(self, connection): diff --git a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py index b7c06f176d..e118daf221 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py @@ -103,29 +103,129 @@ def test_instrument_connection_no_op_tracer_provider(self, mock_connect): self.assertEqual(len(spans_list), 0) @mock.patch("opentelemetry.instrumentation.dbapi.instrument_connection") - @mock.patch("mysql.connector.connect") + @mock.patch("mysql.connector") # pylint: disable=unused-argument - def test_instrument_connection_enable_commenter( + def test_instrument_connection_enable_commenter_dbapi_kwargs( self, mock_connect, mock_instrument_connection, ): - cnx, query = connect_and_execute_query() + cnx = mysql.connector.connect(database="test") cnx = MySQLInstrumentor().instrument_connection( cnx, enable_commenter=True, commenter_options={"foo": True}, ) cursor = cnx.cursor() - cursor.execute(query) + cursor.execute("SELECT * FROM test") kwargs = mock_instrument_connection.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) + def test_instrument_connection_with_dbapi_sqlcomment_enabled(self): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + cnx_proxy = MySQLInstrumentor().instrument_connection( + mock_connection, + enable_commenter=True, + ) + cnx_proxy.cursor().execute("Select 1;") + + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + span_id = format(span.get_span_context().span_id, "016x") + trace_id = format(span.get_span_context().trace_id, "032x") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) + + def test_instrument_connection_with_dbapi_sqlcomment_enabled_with_options( + self, + ): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + cnx_proxy = MySQLInstrumentor().instrument_connection( + mock_connection, + enable_commenter=True, + commenter_options={ + "dbapi_level": False, + "dbapi_threadsafety": True, + "driver_paramstyle": False, + }, + ) + cnx_proxy.cursor().execute("Select 1;") + + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + span_id = format(span.get_span_context().span_id, "016x") + trace_id = format(span.get_span_context().trace_id, "032x") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) + + def test_instrument_connection_with_dbapi_sqlcomment_not_enabled_default( + self, + ): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_cursor = mock_connect_module.connect().cursor() + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + cnx_proxy = MySQLInstrumentor().instrument_connection( + mock_connection, + ) + cnx_proxy.cursor().execute("Select 1;") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + "Select 1;", + ) + @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") - @mock.patch("mysql.connector.connect") + @mock.patch("mysql.connector") # pylint: disable=unused-argument - def test__instrument_enable_commenter( + def test_instrument_enable_commenter_dbapi_kwargs( self, mock_connect, mock_wrap_connect, @@ -138,6 +238,112 @@ def test__instrument_enable_commenter( self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) + def test_instrument_with_dbapi_sqlcomment_enabled( + self, + ): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_cursor = mock_connect_module.connect().cursor() + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + MySQLInstrumentor()._instrument( + enable_commenter=True, + ) + cnx = mock_connect_module.connect(database="test") + cursor = cnx.cursor() + cursor.execute("Select 1;") + + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + span_id = format(span.get_span_context().span_id, "016x") + trace_id = format(span.get_span_context().trace_id, "032x") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) + + def test_instrument_with_dbapi_sqlcomment_enabled_with_options( + self, + ): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_cursor = mock_connect_module.connect().cursor() + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + MySQLInstrumentor()._instrument( + enable_commenter=True, + commenter_options={ + "dbapi_level": False, + "dbapi_threadsafety": True, + "driver_paramstyle": False, + }, + ) + cnx = mock_connect_module.connect(database="test") + cursor = cnx.cursor() + cursor.execute("Select 1;") + + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + span_id = format(span.get_span_context().span_id, "016x") + trace_id = format(span.get_span_context().trace_id, "032x") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) + + def test_instrument_with_dbapi_sqlcomment_not_enabled_default( + self, + ): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_cursor = mock_connect_module.connect().cursor() + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + MySQLInstrumentor()._instrument() + cnx = mock_connect_module.connect(database="test") + cursor = cnx.cursor() + cursor.execute("Select 1;") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + "Select 1;", + ) + @mock.patch("mysql.connector.connect") # pylint: disable=unused-argument def test_uninstrument_connection(self, mock_connect): From d2c707691387fc444819c1ea25275a9b4a26def1 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Tue, 14 Jan 2025 09:08:37 -0800 Subject: [PATCH 4/4] mysql support enable_attribute_commenter --- .../instrumentation/mysql/__init__.py | 28 +++++ .../tests/test_mysql_integration.py | 113 ++++++++++++++++++ 2 files changed, 141 insertions(+) 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 9a1e851ef1..e967a1a2ea 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql/__init__.py @@ -119,6 +119,26 @@ :: Enabling this flag will add traceparent values /*traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'*/ +SQLComment in span attribute +**************************** +If sqlcommenter is enabled, you can optionally configure mysql-connector instrumentation to append sqlcomment to query span attribute for convenience of your platform. + +.. code:: python + + from opentelemetry.instrumentation.mysql import MySQLInstrumentor + + MySQLInstrumentor().instrument( + enable_commenter=True, + enable_attribute_commenter=True, + ) + + +For example, +:: + + Invoking cursor.execute("select * from auth_users") will lead to sql query "select * from auth_users" but when SQLCommenter and attribute_commenter are enabled + the query will get appended with some configurable tags like "select * from auth_users /*tag=value*/;" for both server query and `db.statement` span attribute. + API --- """ @@ -153,6 +173,9 @@ def _instrument(self, **kwargs): tracer_provider = kwargs.get("tracer_provider") enable_sqlcommenter = kwargs.get("enable_commenter", False) commenter_options = kwargs.get("commenter_options", {}) + enable_attribute_commenter = kwargs.get( + "enable_attribute_commenter", False + ) dbapi.wrap_connect( __name__, @@ -164,6 +187,7 @@ def _instrument(self, **kwargs): tracer_provider=tracer_provider, enable_commenter=enable_sqlcommenter, commenter_options=commenter_options, + enable_attribute_commenter=enable_attribute_commenter, ) def _uninstrument(self, **kwargs): @@ -177,6 +201,7 @@ def instrument_connection( tracer_provider=None, enable_commenter=None, commenter_options=None, + enable_attribute_commenter=None, ): """Enable instrumentation in a MySQL connection. @@ -192,6 +217,8 @@ def instrument_connection( Optional flag to enable/disable sqlcommenter (default False). commenter_options: 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. Returns: An instrumented MySQL connection with OpenTelemetry tracing enabled. @@ -206,6 +233,7 @@ def instrument_connection( enable_commenter=enable_commenter, commenter_options=commenter_options, connect_module=mysql.connector, + enable_attribute_commenter=enable_attribute_commenter, ) def uninstrument_connection(self, connection): diff --git a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py index e118daf221..0ef2f4e4cb 100644 --- a/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py +++ b/instrumentation/opentelemetry-instrumentation-mysql/tests/test_mysql_integration.py @@ -20,6 +20,7 @@ from opentelemetry import trace as trace_api from opentelemetry.instrumentation.mysql import MySQLInstrumentor from opentelemetry.sdk import resources +from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase @@ -115,12 +116,14 @@ def test_instrument_connection_enable_commenter_dbapi_kwargs( cnx, enable_commenter=True, commenter_options={"foo": True}, + enable_attribute_commenter=True, ) cursor = cnx.cursor() cursor.execute("SELECT * FROM test") kwargs = mock_instrument_connection.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) + self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_connection_with_dbapi_sqlcomment_enabled(self): mock_connect_module = mock.MagicMock( @@ -153,6 +156,49 @@ def test_instrument_connection_with_dbapi_sqlcomment_enabled(self): mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", ) + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + "Select 1;", + ) + + def test_instrument_connection_with_dbapi_sqlcomment_enabled_stmt_enabled( + self, + ): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + cnx_proxy = MySQLInstrumentor().instrument_connection( + mock_connection, + enable_commenter=True, + enable_attribute_commenter=True, + ) + cnx_proxy.cursor().execute("Select 1;") + + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + span_id = format(span.get_span_context().span_id, "016x") + trace_id = format(span.get_span_context().trace_id, "032x") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) def test_instrument_connection_with_dbapi_sqlcomment_enabled_with_options( self, @@ -192,6 +238,10 @@ def test_instrument_connection_with_dbapi_sqlcomment_enabled_with_options( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", ) + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + "Select 1;", + ) def test_instrument_connection_with_dbapi_sqlcomment_not_enabled_default( self, @@ -221,6 +271,12 @@ def test_instrument_connection_with_dbapi_sqlcomment_not_enabled_default( mock_cursor.execute.call_args[0][0], "Select 1;", ) + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + "Select 1;", + ) @mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect") @mock.patch("mysql.connector") @@ -233,10 +289,12 @@ def test_instrument_enable_commenter_dbapi_kwargs( MySQLInstrumentor()._instrument( enable_commenter=True, commenter_options={"foo": True}, + enable_attribute_commenter=True, ) kwargs = mock_wrap_connect.call_args[1] self.assertEqual(kwargs["enable_commenter"], True) self.assertEqual(kwargs["commenter_options"], {"foo": True}) + self.assertEqual(kwargs["enable_attribute_commenter"], True) def test_instrument_with_dbapi_sqlcomment_enabled( self, @@ -273,6 +331,51 @@ def test_instrument_with_dbapi_sqlcomment_enabled( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", ) + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + "Select 1;", + ) + + def test_instrument_with_dbapi_sqlcomment_enabled_stmt_enabled( + self, + ): + mock_connect_module = mock.MagicMock( + __name__="mysql.connector", + __version__="foobar", + threadsafety="123", + apilevel="123", + paramstyle="test", + ) + mock_cursor = mock_connect_module.connect().cursor() + mock_cursor._cnx._cmysql.get_client_info.return_value = "foobaz" + mock_cursor = mock_connect_module.connect().cursor() + mock_connection = mock.MagicMock() + mock_connection.cursor.return_value = mock_cursor + + with mock.patch( + "opentelemetry.instrumentation.mysql.mysql.connector", + mock_connect_module, + ): + MySQLInstrumentor()._instrument( + enable_commenter=True, + enable_attribute_commenter=True, + ) + cnx = mock_connect_module.connect(database="test") + cursor = cnx.cursor() + cursor.execute("Select 1;") + + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + span_id = format(span.get_span_context().span_id, "016x") + trace_id = format(span.get_span_context().trace_id, "032x") + self.assertEqual( + mock_cursor.execute.call_args[0][0], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", + ) def test_instrument_with_dbapi_sqlcomment_enabled_with_options( self, @@ -314,6 +417,10 @@ def test_instrument_with_dbapi_sqlcomment_enabled_with_options( mock_cursor.execute.call_args[0][0], f"Select 1 /*db_driver='mysql.connector%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;", ) + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + "Select 1;", + ) def test_instrument_with_dbapi_sqlcomment_not_enabled_default( self, @@ -343,6 +450,12 @@ def test_instrument_with_dbapi_sqlcomment_not_enabled_default( mock_cursor.execute.call_args[0][0], "Select 1;", ) + spans_list = self.memory_exporter.get_finished_spans() + span = spans_list[0] + self.assertEqual( + span.attributes[SpanAttributes.DB_STATEMENT], + "Select 1;", + ) @mock.patch("mysql.connector.connect") # pylint: disable=unused-argument