Skip to content

Commit d242454

Browse files
Add mysql-connector sqlcomment support
1 parent 5e1e57a commit d242454

File tree

1 file changed

+215
-6
lines changed
  • instrumentation/opentelemetry-instrumentation-mysql/src/opentelemetry/instrumentation/mysql

1 file changed

+215
-6
lines changed

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

Lines changed: 215 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,94 @@
3434
cursor.close()
3535
cnx.close()
3636
37+
SQLCOMMENTER
38+
*****************************************
39+
You can optionally configure mysql-connector instrumentation to enable sqlcommenter which enriches
40+
the query with contextual information.
41+
42+
Usage
43+
-----
44+
45+
.. code:: python
46+
47+
import mysql.connector
48+
from opentelemetry.instrumentation.mysql import MySQLInstrumentor
49+
50+
MySQLInstrumentor().instrument(enable_commenter=True, commenter_options={})
51+
52+
cnx = mysql.connector.connect(database="MySQL_Database")
53+
cursor = cnx.cursor()
54+
cursor.execute("INSERT INTO test (testField) VALUES (123)")
55+
cursor.close()
56+
cnx.close()
57+
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+
66+
SQLCommenter Configurations
67+
***************************
68+
We can configure the tags to be appended to the sqlquery log by adding configuration inside commenter_options(default:{}) keyword
69+
70+
db_driver = True(Default) or False
71+
72+
For example,
73+
::
74+
Enabling this flag will add mysql.connector and its version, e.g. /*mysql.connector%%3A1.2.3*/
75+
76+
dbapi_threadsafety = True(Default) or False
77+
78+
For example,
79+
::
80+
Enabling this flag will add threadsafety /*dbapi_threadsafety=2*/
81+
82+
dbapi_level = True(Default) or False
83+
84+
For example,
85+
::
86+
Enabling this flag will add dbapi_level /*dbapi_level='2.0'*/
87+
88+
mysql_client_version = True(Default) or False
89+
90+
For example,
91+
::
92+
Enabling this flag will add mysql_client_version /*mysql_client_version='123'*/
93+
94+
driver_paramstyle = True(Default) or False
95+
96+
For example,
97+
::
98+
Enabling this flag will add driver_paramstyle /*driver_paramstyle='pyformat'*/
99+
100+
opentelemetry_values = True(Default) or False
101+
102+
For example,
103+
::
104+
Enabling this flag will add traceparent values /*traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'*/
105+
37106
API
38107
---
39108
"""
40109

41-
from typing import Collection
110+
import logging
111+
import typing
112+
from importlib import import_module
42113

43114
import mysql.connector
115+
import wrapt
44116

117+
from opentelemetry import trace as trace_api
45118
from opentelemetry.instrumentation import dbapi
46119
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
47120
from opentelemetry.instrumentation.mysql.package import _instruments
48121
from opentelemetry.instrumentation.mysql.version import __version__
49122

123+
_logger = logging.getLogger(__name__)
124+
50125

51126
class MySQLInstrumentor(BaseInstrumentor):
52127
_CONNECTION_ATTRIBUTES = {
@@ -58,14 +133,16 @@ class MySQLInstrumentor(BaseInstrumentor):
58133

59134
_DATABASE_SYSTEM = "mysql"
60135

61-
def instrumentation_dependencies(self) -> Collection[str]:
136+
def instrumentation_dependencies(self) -> typing.Collection[str]:
62137
return _instruments
63138

64139
def _instrument(self, **kwargs):
65140
"""Integrate with MySQL Connector/Python library.
66141
https://dev.mysql.com/doc/connector-python/en/
67142
"""
68143
tracer_provider = kwargs.get("tracer_provider")
144+
enable_sqlcommenter = kwargs.get("enable_commenter", False)
145+
commenter_options = kwargs.get("commenter_options", {})
69146

70147
dbapi.wrap_connect(
71148
__name__,
@@ -75,34 +152,56 @@ def _instrument(self, **kwargs):
75152
self._CONNECTION_ATTRIBUTES,
76153
version=__version__,
77154
tracer_provider=tracer_provider,
155+
db_api_integration_factory=DatabaseApiIntegration,
156+
enable_commenter=enable_sqlcommenter,
157+
commenter_options=commenter_options,
78158
)
79159

80160
def _uninstrument(self, **kwargs):
81161
""" "Disable MySQL instrumentation"""
82162
dbapi.unwrap_connect(mysql.connector, "connect")
83163

84164
# pylint:disable=no-self-use
85-
def instrument_connection(self, connection, tracer_provider=None):
165+
def instrument_connection(
166+
self,
167+
connection,
168+
tracer_provider: typing.Optional[trace_api.TracerProvider] = None,
169+
enable_commenter: bool = False,
170+
commenter_options: dict = None,
171+
):
86172
"""Enable instrumentation in a MySQL connection.
87173
88174
Args:
89175
connection: The connection to instrument.
90176
tracer_provider: The optional tracer provider to use. If omitted
91177
the current globally configured one is used.
178+
enable_commenter: Flag to enable/disable sqlcommenter.
179+
commenter_options: Configurations for tags to be appended at the sql query.
92180
93181
Returns:
94182
An instrumented connection.
95183
"""
96-
return dbapi.instrument_connection(
184+
if isinstance(connection, wrapt.ObjectProxy):
185+
_logger.warning("Connection already instrumented")
186+
return connection
187+
188+
db_integration = DatabaseApiIntegration(
97189
__name__,
98-
connection,
99190
self._DATABASE_SYSTEM,
100191
self._CONNECTION_ATTRIBUTES,
101192
version=__version__,
102193
tracer_provider=tracer_provider,
194+
enable_commenter=enable_commenter,
195+
commenter_options=commenter_options,
196+
connect_module=import_module("mysql.connector"),
103197
)
198+
db_integration.get_connection_attributes(connection)
199+
return get_traced_connection_proxy(connection, db_integration)
104200

105-
def uninstrument_connection(self, connection):
201+
def uninstrument_connection(
202+
self,
203+
connection,
204+
):
106205
"""Disable instrumentation in a MySQL connection.
107206
108207
Args:
@@ -112,3 +211,113 @@ def uninstrument_connection(self, connection):
112211
An uninstrumented connection.
113212
"""
114213
return dbapi.uninstrument_connection(connection)
214+
215+
216+
class DatabaseApiIntegration(dbapi.DatabaseApiIntegration):
217+
def wrapped_connection(
218+
self,
219+
connect_method: typing.Callable[..., typing.Any],
220+
args: typing.Tuple[typing.Any, typing.Any],
221+
kwargs: typing.Dict[typing.Any, typing.Any],
222+
):
223+
"""Add object proxy to connection object that checks cursor type."""
224+
connection = connect_method(*args, **kwargs)
225+
self.get_connection_attributes(connection)
226+
return get_traced_connection_proxy(connection, self)
227+
228+
229+
def get_traced_connection_proxy(
230+
connection, db_api_integration, *args, **kwargs
231+
):
232+
# pylint: disable=abstract-method
233+
class TracedConnectionProxy(dbapi.BaseTracedConnectionProxy):
234+
def cursor(self, *args, **kwargs):
235+
wrapped_cursor = self.__wrapped__.cursor(*args, **kwargs)
236+
237+
# It's common to have multiple db client cursors per app,
238+
# so enable_commenter is calculated for the cursor level for
239+
# traced query execution.
240+
enable_commenter_cursor = db_api_integration.enable_commenter
241+
242+
# If a mysql-connector cursor was created with prepared=True,
243+
# then MySQL statements will be prepared and executed natively.
244+
# 1:1 sqlcomment and span correlation in instrumentation would
245+
# break, so sqlcomment is not supported for this use case.
246+
# This is here because wrapped cursor is created when application
247+
# side creates cursor. After that, the instrumentor knows what
248+
# kind of cursor was initialized.
249+
if enable_commenter_cursor:
250+
is_prepared = False
251+
if (
252+
db_api_integration.database_system == "mysql"
253+
and db_api_integration.connect_module.__name__
254+
== "mysql.connector"
255+
):
256+
is_prepared = self.is_mysql_connector_cursor_prepared(
257+
wrapped_cursor
258+
)
259+
if is_prepared:
260+
_logger.warning(
261+
"sqlcomment is not supported for query statements executed by cursors with native prepared statement support. Disabling sqlcommenting for instrumentation of %s.",
262+
db_api_integration.connect_module.__name__,
263+
)
264+
enable_commenter_cursor = False
265+
return get_traced_cursor_proxy(
266+
wrapped_cursor,
267+
db_api_integration,
268+
enable_commenter_cursor,
269+
)
270+
271+
def is_mysql_connector_cursor_prepared(self, cursor): # pylint: disable=no-self-use
272+
try:
273+
from mysql.connector.cursor_cext import ( # pylint: disable=import-outside-toplevel
274+
CMySQLCursorPrepared,
275+
CMySQLCursorPreparedDict,
276+
CMySQLCursorPreparedNamedTuple,
277+
CMySQLCursorPreparedRaw,
278+
)
279+
280+
if type(cursor) in [
281+
CMySQLCursorPrepared,
282+
CMySQLCursorPreparedDict,
283+
CMySQLCursorPreparedNamedTuple,
284+
CMySQLCursorPreparedRaw,
285+
]:
286+
return True
287+
288+
except ImportError as exc:
289+
_logger.warning(
290+
"Could not verify mysql.connector cursor, skipping prepared cursor check: %s",
291+
exc,
292+
)
293+
294+
return False
295+
296+
return TracedConnectionProxy(connection, *args, **kwargs)
297+
298+
299+
class CursorTracer(dbapi.CursorTracer):
300+
def __init__(
301+
self,
302+
db_api_integration: DatabaseApiIntegration,
303+
enable_commenter: bool = False,
304+
) -> None:
305+
super().__init__(db_api_integration)
306+
# It's common to have multiple db client cursors per app,
307+
# so enable_commenter is set at the cursor level and used
308+
# during CursorTracer.traced_execution for mysql-connector
309+
self._commenter_enabled = enable_commenter
310+
311+
312+
def get_traced_cursor_proxy(
313+
cursor, db_api_integration, enable_commenter_cursor, *args, **kwargs
314+
):
315+
class TracedCursorProxy(dbapi.BaseTracedCursorProxy):
316+
def __init__(self, *args, **kwargs):
317+
super().__init__(*args, **kwargs)
318+
self._cursor_tracer = CursorTracer(
319+
db_api_integration,
320+
enable_commenter_cursor,
321+
)
322+
323+
return TracedCursorProxy(cursor, *args, **kwargs)

0 commit comments

Comments
 (0)