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+
37106API
38107---
39108"""
40109
41- from typing import Collection
110+ import logging
111+ import typing
112+ from importlib import import_module
42113
43114import mysql .connector
115+ import wrapt
44116
117+ from opentelemetry import trace as trace_api
45118from opentelemetry .instrumentation import dbapi
46119from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
47120from opentelemetry .instrumentation .mysql .package import _instruments
48121from opentelemetry .instrumentation .mysql .version import __version__
49122
123+ _logger = logging .getLogger (__name__ )
124+
50125
51126class 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