Skip to content

Commit 7b1554b

Browse files
Add PyMySQL instrumentor support for sqlcommenting (open-telemetry#2942)
* 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 * try-except if NoneType module * Add pymysql sqlcomment support * Add unit tests * Update docstring * Changelog * pymysql instrument_connection specifies connect_module * lint * Add tests * Fix doc --------- Co-authored-by: Riccardo Magliocchetti <[email protected]>
1 parent beff723 commit 7b1554b

File tree

3 files changed

+327
-5
lines changed

3 files changed

+327
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5454
([#2635](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2635))
5555
- `opentelemetry-instrumentation` Add support for string based dotted module paths in unwrap
5656
([#2919](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2919))
57+
- `opentelemetry-instrumentation-pymysql` Add sqlcommenter support
58+
([#2942](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2942))
5759

5860
### Fixed
5961

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

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

60129

61130
class PyMySQLInstrumentor(BaseInstrumentor):
62-
def instrumentation_dependencies(self) -> Collection[str]:
131+
def instrumentation_dependencies(self) -> Collection[str]: # pylint: disable=no-self-use
63132
return _instruments
64133

65-
def _instrument(self, **kwargs):
134+
def _instrument(self, **kwargs): # pylint: disable=no-self-use
66135
"""Integrate with the PyMySQL library.
67136
https://github.com/PyMySQL/PyMySQL/
68137
"""
69138
tracer_provider = kwargs.get("tracer_provider")
139+
enable_sqlcommenter = kwargs.get("enable_commenter", False)
140+
commenter_options = kwargs.get("commenter_options", {})
70141

71142
dbapi.wrap_connect(
72143
__name__,
@@ -76,14 +147,21 @@ def _instrument(self, **kwargs):
76147
_CONNECTION_ATTRIBUTES,
77148
version=__version__,
78149
tracer_provider=tracer_provider,
150+
enable_commenter=enable_sqlcommenter,
151+
commenter_options=commenter_options,
79152
)
80153

81-
def _uninstrument(self, **kwargs):
154+
def _uninstrument(self, **kwargs): # pylint: disable=no-self-use
82155
""" "Disable PyMySQL instrumentation"""
83156
dbapi.unwrap_connect(pymysql, "connect")
84157

85158
@staticmethod
86-
def instrument_connection(connection, tracer_provider=None):
159+
def instrument_connection(
160+
connection,
161+
tracer_provider=None,
162+
enable_commenter=None,
163+
commenter_options=None,
164+
):
87165
"""Enable instrumentation in a PyMySQL connection.
88166
89167
Args:
@@ -102,6 +180,9 @@ def instrument_connection(connection, tracer_provider=None):
102180
_CONNECTION_ATTRIBUTES,
103181
version=__version__,
104182
tracer_provider=tracer_provider,
183+
enable_commenter=enable_commenter,
184+
commenter_options=commenter_options,
185+
connect_module=pymysql,
105186
)
106187

107188
@staticmethod

instrumentation/opentelemetry-instrumentation-pymysql/tests/test_pymysql_integration.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525

2626
class TestPyMysqlIntegration(TestBase):
27+
# pylint: disable=invalid-name
2728
def tearDown(self):
2829
super().tearDown()
2930
with self.disable_logging():
@@ -111,6 +112,244 @@ def test_instrument_connection(self, mock_connect):
111112
spans_list = self.memory_exporter.get_finished_spans()
112113
self.assertEqual(len(spans_list), 1)
113114

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

0 commit comments

Comments
 (0)