Skip to content

Commit 3b357c0

Browse files
Add DB-API sqlcomment mysql_client_version fallback (#3729)
* Add dbapi comment mysql_client_version fallback * Changelog * Add docker test_mysql_sqlcommenter * Update dbapi docker-tests * Add test fetchall * Add test case for cmysql unknown * Change to patch * Rm test attempt * Changelog * simplify commenter_data assn * Add Pure Python mysql_client_version unknown test --------- Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com>
1 parent 0b379be commit 3b357c0

File tree

4 files changed

+204
-1
lines changed

4 files changed

+204
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9696
([#4081](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4081))
9797
- `opentelemetry-instrumentation-system-metrics`: Use proper numeric `cpython.gc.generation` attribute in CPython metrics, out of spec `generation` attribute is deprecated and will be removed in the future
9898
([#4092](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4092))
99+
- `opentelemetry-instrumentation-dbapi`: Fix sqlcomment calculation of mysql_client_version field if connection reassignment, with "unknown" fallback
100+
([#3729](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3729))
99101
- `opentelemetry-instrumentation-confluent-kafka`: Fix incorrect number of argument to `_inner_wrap_close`
100102
([#3922](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3922))
101103

instrumentation/opentelemetry-instrumentation-dbapi/src/opentelemetry/instrumentation/dbapi/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,22 @@ def _capture_mysql_version(self, cursor) -> None:
632632
"mysql_client_version"
633633
]
634634
):
635+
try:
636+
# Autoinstrumentation and some programmatic calls
637+
client_version = cursor._cnx._cmysql.get_client_info()
638+
except AttributeError:
639+
# Other programmatic instrumentation with reassigned wrapped connection
640+
try:
641+
client_version = (
642+
cursor._connection._cmysql.get_client_info()
643+
)
644+
except AttributeError as exc:
645+
_logger.debug(
646+
"Could not set mysql_client_version: %s", exc
647+
)
648+
client_version = "unknown"
635649
self._db_api_integration.commenter_data["mysql_client_version"] = (
636-
cursor._cnx._cmysql.get_client_info()
650+
client_version
637651
)
638652

639653
def _get_commenter_data(self) -> dict:

instrumentation/opentelemetry-instrumentation-dbapi/tests/test_dbapi_integration.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,89 @@ def test_non_string_sql_conversion(self):
11801180
spans_list = self.memory_exporter.get_finished_spans()
11811181
self.assertEqual(len(spans_list), 1)
11821182

1183+
def test_capture_mysql_version_primary_success(self):
1184+
connect_module = mock.MagicMock()
1185+
connect_module.__name__ = "mysql.connector"
1186+
connect_module.__version__ = "2.2.9"
1187+
db_integration = dbapi.DatabaseApiIntegration(
1188+
"instrumenting_module_test_name",
1189+
"mysql",
1190+
enable_commenter=True,
1191+
connect_module=connect_module,
1192+
)
1193+
mock_cursor = mock.MagicMock()
1194+
mock_cursor._cnx._cmysql.get_client_info.return_value = "8.0.32"
1195+
mock_connection = db_integration.wrapped_connection(
1196+
mock_connect, {}, {}
1197+
)
1198+
cursor = mock_connection.cursor()
1199+
cursor._cnx = mock_cursor._cnx
1200+
cursor.execute("SELECT 1;")
1201+
mock_cursor._cnx._cmysql.get_client_info.assert_called_once()
1202+
self.assertEqual(
1203+
db_integration.commenter_data["mysql_client_version"], "8.0.32"
1204+
)
1205+
1206+
def test_capture_mysql_version_fallback_success(self):
1207+
connect_module = mock.MagicMock()
1208+
connect_module.__name__ = "mysql.connector"
1209+
connect_module.__version__ = "2.2.9"
1210+
db_integration = dbapi.DatabaseApiIntegration(
1211+
"instrumenting_module_test_name",
1212+
"mysql",
1213+
enable_commenter=True,
1214+
connect_module=connect_module,
1215+
)
1216+
mock_cursor = mock.MagicMock()
1217+
mock_cursor._cnx._cmysql.get_client_info.side_effect = AttributeError(
1218+
"Primary method failed"
1219+
)
1220+
mock_cursor._connection._cmysql.get_client_info.return_value = "8.0.33"
1221+
mock_connection = db_integration.wrapped_connection(
1222+
mock_connect, {}, {}
1223+
)
1224+
cursor = mock_connection.cursor()
1225+
cursor._cnx = mock_cursor._cnx
1226+
cursor._connection = mock_cursor._connection
1227+
cursor.execute("SELECT 1;")
1228+
mock_cursor._cnx._cmysql.get_client_info.assert_called_once()
1229+
mock_cursor._connection._cmysql.get_client_info.assert_called_once()
1230+
self.assertEqual(
1231+
db_integration.commenter_data["mysql_client_version"], "8.0.33"
1232+
)
1233+
1234+
@mock.patch("opentelemetry.instrumentation.dbapi._logger")
1235+
def test_capture_mysql_version_fallback(self, mock_logger):
1236+
connect_module = mock.MagicMock()
1237+
connect_module.__name__ = "mysql.connector"
1238+
connect_module.__version__ = "2.2.9"
1239+
db_integration = dbapi.DatabaseApiIntegration(
1240+
"instrumenting_module_test_name",
1241+
"mysql",
1242+
enable_commenter=True,
1243+
connect_module=connect_module,
1244+
)
1245+
mock_cursor = mock.MagicMock()
1246+
mock_cursor._cnx._cmysql.get_client_info.side_effect = AttributeError(
1247+
"Primary method failed"
1248+
)
1249+
mock_cursor._connection._cmysql.get_client_info.side_effect = (
1250+
AttributeError("Fallback method failed")
1251+
)
1252+
mock_connection = db_integration.wrapped_connection(
1253+
mock_connect, {}, {}
1254+
)
1255+
cursor = mock_connection.cursor()
1256+
cursor._cnx = mock_cursor._cnx
1257+
cursor._connection = mock_cursor._connection
1258+
cursor.execute("SELECT 1;")
1259+
mock_cursor._cnx._cmysql.get_client_info.assert_called_once()
1260+
mock_cursor._connection._cmysql.get_client_info.assert_called_once()
1261+
mock_logger.debug.assert_called_once()
1262+
self.assertEqual(
1263+
db_integration.commenter_data["mysql_client_version"], "unknown"
1264+
)
1265+
11831266

11841267
# pylint: disable=unused-argument
11851268
def mock_connect(*args, **kwargs):
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright 2025, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
17+
import mysql.connector
18+
19+
from opentelemetry.instrumentation.mysql import MySQLInstrumentor
20+
from opentelemetry.test.test_base import TestBase
21+
22+
MYSQL_USER = os.getenv("MYSQL_USER", "testuser")
23+
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "testpassword")
24+
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
25+
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
26+
MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME", "opentelemetry-tests")
27+
28+
29+
class TestFunctionalMySqlCommenter(TestBase):
30+
def test_commenter_enabled_direct_reference(self):
31+
MySQLInstrumentor().instrument(enable_commenter=True)
32+
cnx = mysql.connector.connect(
33+
user=MYSQL_USER,
34+
password=MYSQL_PASSWORD,
35+
host=MYSQL_HOST,
36+
port=MYSQL_PORT,
37+
database=MYSQL_DB_NAME,
38+
)
39+
cursor = cnx.cursor()
40+
41+
cursor.execute("SELECT 1;")
42+
cursor.fetchall()
43+
self.assertRegex(
44+
cursor.statement,
45+
r"SELECT 1 /\*db_driver='mysql\.connector[^']*',dbapi_level='\d\.\d',dbapi_threadsafety=\d,driver_paramstyle='[^']*',mysql_client_version='[^']*',traceparent='[^']*'\*/;",
46+
)
47+
self.assertRegex(
48+
cursor.statement, r"mysql_client_version='(?!unknown)[^']+"
49+
)
50+
51+
cursor.close()
52+
cnx.close()
53+
MySQLInstrumentor().uninstrument()
54+
55+
def test_commenter_enabled_connection_proxy(self):
56+
cnx = mysql.connector.connect(
57+
user=MYSQL_USER,
58+
password=MYSQL_PASSWORD,
59+
host=MYSQL_HOST,
60+
port=MYSQL_PORT,
61+
database=MYSQL_DB_NAME,
62+
)
63+
instrumented_cnx = MySQLInstrumentor().instrument_connection(
64+
connection=cnx,
65+
enable_commenter=True,
66+
)
67+
cursor = instrumented_cnx.cursor()
68+
69+
cursor.execute("SELECT 1;")
70+
cursor.fetchall()
71+
self.assertRegex(
72+
cursor.statement,
73+
r"SELECT 1 /\*db_driver='mysql\.connector[^']*',dbapi_level='\d\.\d',dbapi_threadsafety=\d,driver_paramstyle='[^']*',mysql_client_version='[^']*',traceparent='[^']*'\*/;",
74+
)
75+
self.assertRegex(
76+
cursor.statement, r"mysql_client_version='(?!unknown)[^']+"
77+
)
78+
79+
cursor.close()
80+
MySQLInstrumentor().uninstrument_connection(instrumented_cnx)
81+
cnx.close()
82+
83+
def test_commenter_mysql_client_version_fallback_to_unknown(self):
84+
"""Test that mysql_client_version falls back to 'unknown' with pure Python implementation"""
85+
MySQLInstrumentor().instrument(enable_commenter=True)
86+
cnx = mysql.connector.connect(
87+
user=MYSQL_USER,
88+
password=MYSQL_PASSWORD,
89+
host=MYSQL_HOST,
90+
port=MYSQL_PORT,
91+
database=MYSQL_DB_NAME,
92+
use_pure=True, # Use pure Python - no _cmysql module
93+
)
94+
cursor = cnx.cursor()
95+
96+
cursor.execute("SELECT 1;")
97+
cursor.fetchall()
98+
99+
# With use_pure=True, _cmysql is not available, should fall back to 'unknown'
100+
self.assertRegex(cursor.statement, r"mysql_client_version='unknown'")
101+
102+
cursor.close()
103+
cnx.close()
104+
MySQLInstrumentor().uninstrument()

0 commit comments

Comments
 (0)