Skip to content

Commit beff723

Browse files
Add mysqlclient instrumentor support for sqlcommenting (#2941)
* WIP * Add _DB_DRIVER_ALIASES * Add mysql_client_version to sqlcomment * lint * Fix existing tests * lint test * Add PyMySQL dbapi commenter case * Add test * Add test * Add test * Add tests * Changelog * calculate_commenter_data at init of DatabaseApiIntegration * Add mysqlclient sqlcomment support * Fix typo * try-except if NoneType module * Add unit test * CHangelog * mysqlclient instrument_connection with connect_module * add tests * more tests * more tests 2 * lint * Redesign tests * Rm unnecessary mocks
1 parent 4e992dd commit beff723

File tree

3 files changed

+335
-4
lines changed

3 files changed

+335
-4
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
([#2935](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2935))
2424
- `opentelemetry-instrumentation-dbapi` instrument_connection accepts optional connect_module
2525
([#3027](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3027))
26+
- `opentelemetry-instrumentation-mysqlclient` Add sqlcommenter support
27+
([#2941](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2941))
2628

2729
### Fixed
2830

Diff for: instrumentation/opentelemetry-instrumentation-mysqlclient/src/opentelemetry/instrumentation/mysqlclient/__init__.py

+82-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,72 @@
3636
cursor.close()
3737
cnx.close()
3838
39+
SQLCOMMENTER
40+
*****************************************
41+
You can optionally configure MySQLClient instrumentation to enable sqlcommenter which enriches
42+
the query with contextual information.
43+
44+
.. code:: python
45+
46+
import MySQLdb
47+
from opentelemetry.instrumentation.mysqlclient import MySQLClientInstrumentor
48+
49+
50+
MySQLClientInstrumentor().instrument(enable_commenter=True, commenter_options={})
51+
52+
cnx = MySQLdb.connect(database="MySQL_Database")
53+
cursor = cnx.cursor()
54+
cursor.execute("INSERT INTO test (testField) VALUES (123)"
55+
cnx.commit()
56+
cursor.close()
57+
cnx.close()
58+
59+
For example,
60+
::
61+
62+
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
63+
the query will get appended with some configurable tags like "INSERT INTO test (testField) VALUES (123) /*tag=value*/;"
64+
65+
SQLCommenter Configurations
66+
***************************
67+
We can configure the tags to be appended to the sqlquery log by adding configuration inside commenter_options(default:{}) keyword
68+
69+
db_driver = True(Default) or False
70+
71+
For example,
72+
::
73+
Enabling this flag will add MySQLdb and its version, e.g. /*MySQLdb%%3A1.2.3*/
74+
75+
dbapi_threadsafety = True(Default) or False
76+
77+
For example,
78+
::
79+
Enabling this flag will add threadsafety /*dbapi_threadsafety=2*/
80+
81+
dbapi_level = True(Default) or False
82+
83+
For example,
84+
::
85+
Enabling this flag will add dbapi_level /*dbapi_level='2.0'*/
86+
87+
mysql_client_version = True(Default) or False
88+
89+
For example,
90+
::
91+
Enabling this flag will add mysql_client_version /*mysql_client_version='123'*/
92+
93+
driver_paramstyle = True(Default) or False
94+
95+
For example,
96+
::
97+
Enabling this flag will add driver_paramstyle /*driver_paramstyle='pyformat'*/
98+
99+
opentelemetry_values = True(Default) or False
100+
101+
For example,
102+
::
103+
Enabling this flag will add traceparent values /*traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'*/
104+
39105
API
40106
---
41107
"""
@@ -59,14 +125,16 @@
59125

60126

61127
class MySQLClientInstrumentor(BaseInstrumentor):
62-
def instrumentation_dependencies(self) -> Collection[str]:
128+
def instrumentation_dependencies(self) -> Collection[str]: # pylint: disable=no-self-use
63129
return _instruments
64130

65-
def _instrument(self, **kwargs):
131+
def _instrument(self, **kwargs): # pylint: disable=no-self-use
66132
"""Integrate with the mysqlclient library.
67133
https://github.com/PyMySQL/mysqlclient/
68134
"""
69135
tracer_provider = kwargs.get("tracer_provider")
136+
enable_sqlcommenter = kwargs.get("enable_commenter", False)
137+
commenter_options = kwargs.get("commenter_options", {})
70138

71139
dbapi.wrap_connect(
72140
__name__,
@@ -76,14 +144,21 @@ def _instrument(self, **kwargs):
76144
_CONNECTION_ATTRIBUTES,
77145
version=__version__,
78146
tracer_provider=tracer_provider,
147+
enable_commenter=enable_sqlcommenter,
148+
commenter_options=commenter_options,
79149
)
80150

81-
def _uninstrument(self, **kwargs):
151+
def _uninstrument(self, **kwargs): # pylint: disable=no-self-use
82152
""" "Disable mysqlclient instrumentation"""
83153
dbapi.unwrap_connect(MySQLdb, "connect")
84154

85155
@staticmethod
86-
def instrument_connection(connection, tracer_provider=None):
156+
def instrument_connection(
157+
connection,
158+
tracer_provider=None,
159+
enable_commenter=None,
160+
commenter_options=None,
161+
):
87162
"""Enable instrumentation in a mysqlclient connection.
88163
89164
Args:
@@ -102,6 +177,9 @@ def instrument_connection(connection, tracer_provider=None):
102177
_CONNECTION_ATTRIBUTES,
103178
version=__version__,
104179
tracer_provider=tracer_provider,
180+
enable_commenter=enable_commenter,
181+
commenter_options=commenter_options,
182+
connect_module=MySQLdb,
105183
)
106184

107185
@staticmethod

Diff for: instrumentation/opentelemetry-instrumentation-mysqlclient/tests/test_mysqlclient_integration.py

+251
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424

2525
class TestMySQLClientIntegration(TestBase):
26+
# pylint: disable=invalid-name
2627
def tearDown(self):
2728
super().tearDown()
2829
with self.disable_logging():
@@ -96,6 +97,256 @@ def test_instrument_connection(self, mock_connect):
9697
spans_list = self.memory_exporter.get_finished_spans()
9798
self.assertEqual(len(spans_list), 1)
9899

100+
@mock.patch("opentelemetry.instrumentation.dbapi.instrument_connection")
101+
@mock.patch("MySQLdb.connect")
102+
# pylint: disable=unused-argument
103+
def test_instrument_connection_enable_commenter_dbapi_kwargs(
104+
self,
105+
mock_connect,
106+
mock_instrument_connection,
107+
):
108+
cnx = MySQLdb.connect(database="test")
109+
cnx = MySQLClientInstrumentor().instrument_connection(
110+
cnx,
111+
enable_commenter=True,
112+
commenter_options={"foo": True},
113+
)
114+
cursor = cnx.cursor()
115+
cursor.execute("Select 1;")
116+
kwargs = mock_instrument_connection.call_args[1]
117+
self.assertEqual(kwargs["enable_commenter"], True)
118+
self.assertEqual(kwargs["commenter_options"], {"foo": True})
119+
120+
def test_instrument_connection_with_dbapi_sqlcomment_enabled(self):
121+
mock_connect_module = mock.MagicMock(
122+
__name__="MySQLdb",
123+
threadsafety="123",
124+
apilevel="123",
125+
paramstyle="test",
126+
)
127+
mock_connect_module._mysql.get_client_info.return_value = "foobaz"
128+
mock_cursor = mock_connect_module.connect().cursor()
129+
mock_connection = mock.MagicMock()
130+
mock_connection.cursor.return_value = mock_cursor
131+
132+
with mock.patch(
133+
"opentelemetry.instrumentation.mysqlclient.MySQLdb",
134+
mock_connect_module,
135+
), mock.patch(
136+
"opentelemetry.instrumentation.dbapi.util_version",
137+
return_value="foobar",
138+
):
139+
cnx_proxy = MySQLClientInstrumentor().instrument_connection(
140+
mock_connection,
141+
enable_commenter=True,
142+
)
143+
cnx_proxy.cursor().execute("Select 1;")
144+
145+
spans_list = self.memory_exporter.get_finished_spans()
146+
span = spans_list[0]
147+
span_id = format(span.get_span_context().span_id, "016x")
148+
trace_id = format(span.get_span_context().trace_id, "032x")
149+
self.assertEqual(
150+
mock_cursor.execute.call_args[0][0],
151+
f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;",
152+
)
153+
154+
def test_instrument_connection_with_dbapi_sqlcomment_enabled_with_options(
155+
self,
156+
):
157+
mock_connect_module = mock.MagicMock(
158+
__name__="MySQLdb",
159+
threadsafety="123",
160+
apilevel="123",
161+
paramstyle="test",
162+
)
163+
mock_connect_module._mysql.get_client_info.return_value = "foobaz"
164+
mock_cursor = mock_connect_module.connect().cursor()
165+
mock_connection = mock.MagicMock()
166+
mock_connection.cursor.return_value = mock_cursor
167+
168+
with mock.patch(
169+
"opentelemetry.instrumentation.mysqlclient.MySQLdb",
170+
mock_connect_module,
171+
), mock.patch(
172+
"opentelemetry.instrumentation.dbapi.util_version",
173+
return_value="foobar",
174+
):
175+
cnx_proxy = MySQLClientInstrumentor().instrument_connection(
176+
mock_connection,
177+
enable_commenter=True,
178+
commenter_options={
179+
"dbapi_level": False,
180+
"dbapi_threadsafety": True,
181+
"driver_paramstyle": False,
182+
},
183+
)
184+
cnx_proxy.cursor().execute("Select 1;")
185+
186+
spans_list = self.memory_exporter.get_finished_spans()
187+
span = spans_list[0]
188+
span_id = format(span.get_span_context().span_id, "016x")
189+
trace_id = format(span.get_span_context().trace_id, "032x")
190+
self.assertEqual(
191+
mock_cursor.execute.call_args[0][0],
192+
f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;",
193+
)
194+
195+
def test_instrument_connection_with_dbapi_sqlcomment_not_enabled_default(
196+
self,
197+
):
198+
mock_connect_module = mock.MagicMock(
199+
__name__="MySQLdb",
200+
threadsafety="123",
201+
apilevel="123",
202+
paramstyle="test",
203+
)
204+
mock_connect_module._mysql.get_client_info.return_value = "foobaz"
205+
mock_cursor = mock_connect_module.connect().cursor()
206+
mock_connection = mock.MagicMock()
207+
mock_connection.cursor.return_value = mock_cursor
208+
209+
with mock.patch(
210+
"opentelemetry.instrumentation.mysqlclient.MySQLdb",
211+
mock_connect_module,
212+
), mock.patch(
213+
"opentelemetry.instrumentation.dbapi.util_version",
214+
return_value="foobar",
215+
):
216+
cnx_proxy = MySQLClientInstrumentor().instrument_connection(
217+
mock_connection,
218+
)
219+
cnx_proxy.cursor().execute("Select 1;")
220+
self.assertEqual(
221+
mock_cursor.execute.call_args[0][0],
222+
"Select 1;",
223+
)
224+
225+
@mock.patch("opentelemetry.instrumentation.dbapi.wrap_connect")
226+
@mock.patch("MySQLdb.connect")
227+
# pylint: disable=unused-argument
228+
def test_instrument_enable_commenter_dbapi_kwargs(
229+
self,
230+
mock_connect,
231+
mock_wrap_connect,
232+
):
233+
MySQLClientInstrumentor()._instrument(
234+
enable_commenter=True,
235+
commenter_options={"foo": True},
236+
)
237+
kwargs = mock_wrap_connect.call_args[1]
238+
self.assertEqual(kwargs["enable_commenter"], True)
239+
self.assertEqual(kwargs["commenter_options"], {"foo": True})
240+
241+
def test_instrument_with_dbapi_sqlcomment_enabled(
242+
self,
243+
):
244+
mock_connect_module = mock.MagicMock(
245+
__name__="MySQLdb",
246+
threadsafety="123",
247+
apilevel="123",
248+
paramstyle="test",
249+
)
250+
mock_connect_module._mysql.get_client_info.return_value = "foobaz"
251+
mock_cursor = mock_connect_module.connect().cursor()
252+
mock_connection = mock.MagicMock()
253+
mock_connection.cursor.return_value = mock_cursor
254+
255+
with mock.patch(
256+
"opentelemetry.instrumentation.mysqlclient.MySQLdb",
257+
mock_connect_module,
258+
), mock.patch(
259+
"opentelemetry.instrumentation.dbapi.util_version",
260+
return_value="foobar",
261+
):
262+
MySQLClientInstrumentor()._instrument(
263+
enable_commenter=True,
264+
)
265+
cnx = mock_connect_module.connect(database="test")
266+
cursor = cnx.cursor()
267+
cursor.execute("Select 1;")
268+
269+
spans_list = self.memory_exporter.get_finished_spans()
270+
span = spans_list[0]
271+
span_id = format(span.get_span_context().span_id, "016x")
272+
trace_id = format(span.get_span_context().trace_id, "032x")
273+
self.assertEqual(
274+
mock_cursor.execute.call_args[0][0],
275+
f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_level='123',dbapi_threadsafety='123',driver_paramstyle='test',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;",
276+
)
277+
278+
def test_instrument_with_dbapi_sqlcomment_enabled_with_options(
279+
self,
280+
):
281+
mock_connect_module = mock.MagicMock(
282+
__name__="MySQLdb",
283+
threadsafety="123",
284+
apilevel="123",
285+
paramstyle="test",
286+
)
287+
mock_connect_module._mysql.get_client_info.return_value = "foobaz"
288+
mock_cursor = mock_connect_module.connect().cursor()
289+
mock_connection = mock.MagicMock()
290+
mock_connection.cursor.return_value = mock_cursor
291+
292+
with mock.patch(
293+
"opentelemetry.instrumentation.mysqlclient.MySQLdb",
294+
mock_connect_module,
295+
), mock.patch(
296+
"opentelemetry.instrumentation.dbapi.util_version",
297+
return_value="foobar",
298+
):
299+
MySQLClientInstrumentor()._instrument(
300+
enable_commenter=True,
301+
commenter_options={
302+
"dbapi_level": False,
303+
"dbapi_threadsafety": True,
304+
"driver_paramstyle": False,
305+
},
306+
)
307+
cnx = mock_connect_module.connect(database="test")
308+
cursor = cnx.cursor()
309+
cursor.execute("Select 1;")
310+
311+
spans_list = self.memory_exporter.get_finished_spans()
312+
span = spans_list[0]
313+
span_id = format(span.get_span_context().span_id, "016x")
314+
trace_id = format(span.get_span_context().trace_id, "032x")
315+
self.assertEqual(
316+
mock_cursor.execute.call_args[0][0],
317+
f"Select 1 /*db_driver='MySQLdb%%3Afoobar',dbapi_threadsafety='123',mysql_client_version='foobaz',traceparent='00-{trace_id}-{span_id}-01'*/;",
318+
)
319+
320+
def test_instrument_with_dbapi_sqlcomment_not_enabled_default(
321+
self,
322+
):
323+
mock_connect_module = mock.MagicMock(
324+
__name__="MySQLdb",
325+
threadsafety="123",
326+
apilevel="123",
327+
paramstyle="test",
328+
)
329+
mock_connect_module._mysql.get_client_info.return_value = "foobaz"
330+
mock_cursor = mock_connect_module.connect().cursor()
331+
mock_connection = mock.MagicMock()
332+
mock_connection.cursor.return_value = mock_cursor
333+
334+
with mock.patch(
335+
"opentelemetry.instrumentation.mysqlclient.MySQLdb",
336+
mock_connect_module,
337+
), mock.patch(
338+
"opentelemetry.instrumentation.dbapi.util_version",
339+
return_value="foobar",
340+
):
341+
MySQLClientInstrumentor()._instrument()
342+
cnx = mock_connect_module.connect(database="test")
343+
cursor = cnx.cursor()
344+
cursor.execute("Select 1;")
345+
self.assertEqual(
346+
mock_cursor.execute.call_args[0][0],
347+
"Select 1;",
348+
)
349+
99350
@mock.patch("MySQLdb.connect")
100351
# pylint: disable=unused-argument
101352
def test_uninstrument_connection(self, mock_connect):

0 commit comments

Comments
 (0)