107107---
108108"""
109109
110- from typing import Collection
110+ import logging
111+ from typing import (
112+ Any ,
113+ Callable ,
114+ Collection ,
115+ Dict ,
116+ Tuple ,
117+ )
111118
112119import mysql .connector
120+ import wrapt
121+ from mysql .connector .cursor_cext import CMySQLCursor
113122
123+ from opentelemetry import trace as trace_api
114124from opentelemetry .instrumentation import dbapi
115125from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
116126from opentelemetry .instrumentation .mysql .package import _instruments
117127from opentelemetry .instrumentation .mysql .version import __version__
118128
129+ _logger = logging .getLogger (__name__ )
130+ _OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory"
131+
119132
120133class MySQLInstrumentor (BaseInstrumentor ):
121134 _CONNECTION_ATTRIBUTES = {
@@ -146,6 +159,7 @@ def _instrument(self, **kwargs):
146159 self ._CONNECTION_ATTRIBUTES ,
147160 version = __version__ ,
148161 tracer_provider = tracer_provider ,
162+ db_api_integration_factory = DatabaseApiIntegration ,
149163 enable_commenter = enable_sqlcommenter ,
150164 commenter_options = commenter_options ,
151165 )
@@ -162,34 +176,220 @@ def instrument_connection(
162176 enable_commenter = None ,
163177 commenter_options = None ,
164178 ):
165- """Enable instrumentation in a MySQL connection.
179+ if not hasattr (connection , "_is_instrumented_by_opentelemetry" ):
180+ connection ._is_instrumented_by_opentelemetry = False
181+
182+ if not connection ._is_instrumented_by_opentelemetry :
183+ setattr (
184+ connection , _OTEL_CURSOR_FACTORY_KEY , connection .cursor_factory
185+ )
186+ connection .cursor_factory = _new_cursor_factory (
187+ tracer_provider = tracer_provider
188+ )
189+ connection ._is_instrumented_by_opentelemetry = True
190+ else :
191+ _logger .warning (
192+ "Attempting to instrument mysql-connector connection while already instrumented"
193+ )
194+ return connection
195+
196+ def uninstrument_connection (
197+ self ,
198+ connection ,
199+ ):
200+ connection .cursor_factory = getattr (
201+ connection , _OTEL_CURSOR_FACTORY_KEY , None
202+ )
166203
167- Args:
168- connection: The connection to instrument.
169- tracer_provider: The optional tracer provider to use. If omitted
170- the current globally configured one is used.
204+ return connection
171205
172- Returns:
173- An instrumented connection.
174- """
175- return dbapi .instrument_connection (
206+
207+ class DatabaseApiIntegration (dbapi .DatabaseApiIntegration ):
208+ def wrapped_connection (
209+ self ,
210+ connect_method : Callable [..., Any ],
211+ args : Tuple [Any , Any ],
212+ kwargs : Dict [Any , Any ],
213+ ):
214+ """Add object proxy to connection object."""
215+ connection = connect_method (* args , ** kwargs )
216+ self .get_connection_attributes (connection )
217+ return get_traced_connection_proxy (connection , self )
218+
219+
220+ def get_traced_connection_proxy (
221+ connection , db_api_integration , * args , ** kwargs
222+ ):
223+ # pylint: disable=abstract-method
224+ class TracedConnectionProxy (wrapt .ObjectProxy ):
225+ # pylint: disable=unused-argument
226+ def __init__ (self , connection , * args , ** kwargs ):
227+ wrapt .ObjectProxy .__init__ (self , connection )
228+
229+ def __getattribute__ (self , name ):
230+ if object .__getattribute__ (self , name ):
231+ return object .__getattribute__ (self , name )
232+
233+ return object .__getattribute__ (
234+ object .__getattribute__ (self , "_connection" ), name
235+ )
236+
237+ def cursor (self , * args , ** kwargs ):
238+ wrapped_cursor = self .__wrapped__ .cursor (* args , ** kwargs )
239+
240+ # It's common to have multiple db client cursors per app,
241+ # so enable_commenter is set at the cursor level and used
242+ # during traced query execution.
243+ enable_commenter_cursor = db_api_integration .enable_commenter
244+
245+ # If a mysql-connector cursor was created with prepared=True,
246+ # then MySQL statements will be prepared and executed natively.
247+ # 1:1 sqlcomment and span correlation in instrumentation would
248+ # break, so sqlcomment is not supported for this use case.
249+ # This is here because wrapped cursor is created when application
250+ # side creates cursor. After that, the instrumentor knows what
251+ # kind of cursor was initialized.
252+ if enable_commenter_cursor :
253+ is_prepared = False
254+ if (
255+ db_api_integration .database_system == "mysql"
256+ and db_api_integration .connect_module .__name__
257+ == "mysql.connector"
258+ ):
259+ is_prepared = self .is_mysql_connector_cursor_prepared (
260+ wrapped_cursor
261+ )
262+ if is_prepared :
263+ _logger .warning (
264+ "sqlcomment is not supported for query statements executed by cursors with native prepared statement support. Disabling sqlcommenting for instrumentation of %s." ,
265+ db_api_integration .connect_module .__name__ ,
266+ )
267+ enable_commenter_cursor = False
268+ return get_traced_cursor_proxy (
269+ wrapped_cursor ,
270+ db_api_integration ,
271+ enable_commenter = enable_commenter_cursor ,
272+ )
273+
274+ def is_mysql_connector_cursor_prepared (self , cursor ): # pylint: disable=no-self-use
275+ try :
276+ from mysql .connector .cursor_cext import ( # pylint: disable=import-outside-toplevel
277+ CMySQLCursorPrepared ,
278+ CMySQLCursorPreparedDict ,
279+ CMySQLCursorPreparedNamedTuple ,
280+ CMySQLCursorPreparedRaw ,
281+ )
282+
283+ if type (cursor ) in [
284+ CMySQLCursorPrepared ,
285+ CMySQLCursorPreparedDict ,
286+ CMySQLCursorPreparedNamedTuple ,
287+ CMySQLCursorPreparedRaw ,
288+ ]:
289+ return True
290+
291+ except ImportError as exc :
292+ _logger .warning (
293+ "Could not verify mysql.connector cursor, skipping prepared cursor check: %s" ,
294+ exc ,
295+ )
296+
297+ return False
298+
299+ def __enter__ (self ):
300+ self .__wrapped__ .__enter__ ()
301+ return self
302+
303+ def __exit__ (self , * args , ** kwargs ):
304+ self .__wrapped__ .__exit__ (* args , ** kwargs )
305+
306+ return TracedConnectionProxy (connection , * args , ** kwargs )
307+
308+
309+ class CursorTracer (dbapi .CursorTracer ):
310+ def __init__ (
311+ self ,
312+ db_api_integration : DatabaseApiIntegration ,
313+ enable_commenter : bool = False ,
314+ ) -> None :
315+ super ().__init__ (db_api_integration )
316+ # It's common to have multiple db client cursors per app,
317+ # so enable_commenter is set at the cursor level and used
318+ # during traced query execution for mysql-connector
319+ self ._commenter_enabled = enable_commenter
320+
321+
322+ def get_traced_cursor_proxy (cursor , db_api_integration , * args , ** kwargs ):
323+ enable_commenter = kwargs .get ("enable_commenter" , False )
324+ _cursor_tracer = CursorTracer (db_api_integration , enable_commenter )
325+
326+ # pylint: disable=abstract-method
327+ class TracedCursorProxy (wrapt .ObjectProxy ):
328+ # pylint: disable=unused-argument
329+ def __init__ (self , cursor , * args , ** kwargs ):
330+ wrapt .ObjectProxy .__init__ (self , cursor )
331+
332+ def execute (self , * args , ** kwargs ):
333+ return _cursor_tracer .traced_execution (
334+ self .__wrapped__ , self .__wrapped__ .execute , * args , ** kwargs
335+ )
336+
337+ def executemany (self , * args , ** kwargs ):
338+ return _cursor_tracer .traced_execution (
339+ self .__wrapped__ , self .__wrapped__ .executemany , * args , ** kwargs
340+ )
341+
342+ def callproc (self , * args , ** kwargs ):
343+ return _cursor_tracer .traced_execution (
344+ self .__wrapped__ , self .__wrapped__ .callproc , * args , ** kwargs
345+ )
346+
347+ def __enter__ (self ):
348+ self .__wrapped__ .__enter__ ()
349+ return self
350+
351+ def __exit__ (self , * args , ** kwargs ):
352+ self .__wrapped__ .__exit__ (* args , ** kwargs )
353+
354+ return TracedCursorProxy (cursor , * args , ** kwargs )
355+
356+
357+ def _new_cursor_factory (
358+ db_api : DatabaseApiIntegration = None ,
359+ base_factory : CMySQLCursor = None ,
360+ tracer_provider : trace_api .TracerProvider = None ,
361+ enable_commenter : bool = False ,
362+ ):
363+ if not db_api :
364+ db_api = DatabaseApiIntegration (
176365 __name__ ,
177- connection ,
178- self ._DATABASE_SYSTEM ,
179- self ._CONNECTION_ATTRIBUTES ,
366+ MySQLInstrumentor ._DATABASE_SYSTEM ,
367+ MySQLInstrumentor ._CONNECTION_ATTRIBUTES ,
180368 version = __version__ ,
181369 tracer_provider = tracer_provider ,
182- enable_commenter = enable_commenter ,
183- commenter_options = commenter_options ,
184370 )
185371
186- def uninstrument_connection (self , connection ):
187- """Disable instrumentation in a MySQL connection.
188-
189- Args:
190- connection: The connection to uninstrument.
191-
192- Returns:
193- An uninstrumented connection.
194- """
195- return dbapi .uninstrument_connection (connection )
372+ # Latter is base class for all mysql-connector cursors
373+ base_factory = base_factory or CMySQLCursor
374+ _cursor_tracer = CursorTracer (
375+ db_api ,
376+ enable_commenter ,
377+ )
378+
379+ class TracedCursorFactory (base_factory ):
380+ def execute (self , * args , ** kwargs ):
381+ return _cursor_tracer .traced_execution (
382+ self , super ().execute , * args , ** kwargs
383+ )
384+
385+ def executemany (self , * args , ** kwargs ):
386+ return _cursor_tracer .traced_execution (
387+ self , super ().executemany , * args , ** kwargs
388+ )
389+
390+ def callproc (self , * args , ** kwargs ):
391+ return _cursor_tracer .traced_execution (
392+ self , super ().callproc , * args , ** kwargs
393+ )
394+
395+ return TracedCursorFactory
0 commit comments