Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3615](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3615))
- `opentelemetry-instrumentation-fastapi`: Don't pass bounded server_request_hook when using `FastAPIInstrumentor.instrument()`
([#3701](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3701))
- `opentelemetry-instrumentation-dbapi`: Fix sqlcomment calculation of mysql_client_version field if connection reassignment, with "unknown" fallback
([#3729](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3729))

### Added

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -623,9 +623,24 @@ def _capture_mysql_version(self, cursor) -> None:
"mysql_client_version"
]
):
self._db_api_integration.commenter_data["mysql_client_version"] = (
cursor._cnx._cmysql.get_client_info()
)
try:
# Autoinstrumentation and some programmatic calls
self._db_api_integration.commenter_data[
"mysql_client_version"
] = cursor._cnx._cmysql.get_client_info()
except AttributeError:
# Other programmatic instrumentation with reassigned wrapped connection
try:
self._db_api_integration.commenter_data[
"mysql_client_version"
] = cursor._connection._cmysql.get_client_info()
except AttributeError as exc:
_logger.error(
"Could not set mysql_client_version: %s", exc
)
self._db_api_integration.commenter_data[
"mysql_client_version"
] = "unknown"

def _get_commenter_data(self) -> dict:
"""Uses DB-API integration to return commenter data for sqlcomment"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,89 @@ def test_non_string_sql_conversion(self):
spans_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans_list), 1)

def test_capture_mysql_version_primary_success(self):
connect_module = mock.MagicMock()
connect_module.__name__ = "mysql.connector"
connect_module.__version__ = "2.2.9"
db_integration = dbapi.DatabaseApiIntegration(
"instrumenting_module_test_name",
"mysql",
enable_commenter=True,
connect_module=connect_module,
)
mock_cursor = mock.MagicMock()
mock_cursor._cnx._cmysql.get_client_info.return_value = "8.0.32"
mock_connection = db_integration.wrapped_connection(
mock_connect, {}, {}
)
cursor = mock_connection.cursor()
cursor._cnx = mock_cursor._cnx
cursor.execute("SELECT 1;")
mock_cursor._cnx._cmysql.get_client_info.assert_called_once()
self.assertEqual(
db_integration.commenter_data["mysql_client_version"], "8.0.32"
)

def test_capture_mysql_version_fallback_success(self):
connect_module = mock.MagicMock()
connect_module.__name__ = "mysql.connector"
connect_module.__version__ = "2.2.9"
db_integration = dbapi.DatabaseApiIntegration(
"instrumenting_module_test_name",
"mysql",
enable_commenter=True,
connect_module=connect_module,
)
mock_cursor = mock.MagicMock()
mock_cursor._cnx._cmysql.get_client_info.side_effect = AttributeError(
"Primary method failed"
)
mock_cursor._connection._cmysql.get_client_info.return_value = "8.0.33"
mock_connection = db_integration.wrapped_connection(
mock_connect, {}, {}
)
cursor = mock_connection.cursor()
cursor._cnx = mock_cursor._cnx
cursor._connection = mock_cursor._connection
cursor.execute("SELECT 1;")
mock_cursor._cnx._cmysql.get_client_info.assert_called_once()
mock_cursor._connection._cmysql.get_client_info.assert_called_once()
self.assertEqual(
db_integration.commenter_data["mysql_client_version"], "8.0.33"
)

@mock.patch("opentelemetry.instrumentation.dbapi._logger")
def test_capture_mysql_version_fallback(self, mock_logger):
connect_module = mock.MagicMock()
connect_module.__name__ = "mysql.connector"
connect_module.__version__ = "2.2.9"
db_integration = dbapi.DatabaseApiIntegration(
"instrumenting_module_test_name",
"mysql",
enable_commenter=True,
connect_module=connect_module,
)
mock_cursor = mock.MagicMock()
mock_cursor._cnx._cmysql.get_client_info.side_effect = AttributeError(
"Primary method failed"
)
mock_cursor._connection._cmysql.get_client_info.side_effect = (
AttributeError("Fallback method failed")
)
mock_connection = db_integration.wrapped_connection(
mock_connect, {}, {}
)
cursor = mock_connection.cursor()
cursor._cnx = mock_cursor._cnx
cursor._connection = mock_cursor._connection
cursor.execute("SELECT 1;")
mock_cursor._cnx._cmysql.get_client_info.assert_called_once()
mock_cursor._connection._cmysql.get_client_info.assert_called_once()
mock_logger.error.assert_called_once()
self.assertEqual(
db_integration.commenter_data["mysql_client_version"], "unknown"
)


# pylint: disable=unused-argument
def mock_connect(*args, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2025, OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

import mysql.connector

from opentelemetry.instrumentation.mysql import MySQLInstrumentor
from opentelemetry.test.test_base import TestBase

MYSQL_USER = os.getenv("MYSQL_USER", "testuser")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "testpassword")
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME", "opentelemetry-tests")


class TestFunctionalMySqlCommenter(TestBase):
def test_commenter_enabled_direct_reference(self):
MySQLInstrumentor().instrument(enable_commenter=True)
cnx = mysql.connector.connect(
user=MYSQL_USER,
password=MYSQL_PASSWORD,
host=MYSQL_HOST,
port=MYSQL_PORT,
database=MYSQL_DB_NAME,
)
cursor = cnx.cursor()

cursor.execute("SELECT 1;")
cursor.fetchall()
self.assertRegex(
cursor.statement,
r"SELECT 1 /\*db_driver='mysql\.connector[^']*',dbapi_level='\d\.\d',dbapi_threadsafety=\d,driver_paramstyle='[^']*',mysql_client_version='[^']*',traceparent='[^']*'\*/;",
)
self.assertRegex(
cursor.statement, r"mysql_client_version='(?!unknown)[^']+"
)

cursor.close()
cnx.close()
MySQLInstrumentor().uninstrument()

def test_commenter_enabled_connection_proxy(self):
cnx = mysql.connector.connect(
user=MYSQL_USER,
password=MYSQL_PASSWORD,
host=MYSQL_HOST,
port=MYSQL_PORT,
database=MYSQL_DB_NAME,
)
instrumented_cnx = MySQLInstrumentor().instrument_connection(
connection=cnx,
enable_commenter=True,
)
cursor = instrumented_cnx.cursor()

cursor.execute("SELECT 1;")
cursor.fetchall()
self.assertRegex(
cursor.statement,
r"SELECT 1 /\*db_driver='mysql\.connector[^']*',dbapi_level='\d\.\d',dbapi_threadsafety=\d,driver_paramstyle='[^']*',mysql_client_version='[^']*',traceparent='[^']*'\*/;",
)
self.assertRegex(
cursor.statement, r"mysql_client_version='(?!unknown)[^']+"
)

cursor.close()
MySQLInstrumentor().uninstrument_connection(instrumented_cnx)
cnx.close()