Skip to content

Commit 650a167

Browse files
committed
BUG#36227964: Improve OpenTelemetry span coverage
This patch expands `query span` coverage by instrumenting connection methods that are part of the public API. A query span is generated for connection methods related to queries and utility commands. Change-Id: Ia69194194a10d7551159285ced365b11e0a26700
1 parent fdf9b38 commit 650a167

File tree

4 files changed

+439
-88
lines changed

4 files changed

+439
-88
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ v8.4.0
1616
- WL#16164: Implement support for new vector data type
1717
- WL#16127: Remove the FIDO authentication mechanism
1818
- WL#16053: Support GSSAPI/Kerberos authentication on Windows using authentication_ldap_sasl_client plug-in for C-extension
19+
- BUG#36227964: Improve OpenTelemetry span coverage
1920

2021
v8.3.0
2122
======

mysql-connector-python/lib/mysql/connector/opentelemetry/instrumentation.py

Lines changed: 216 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
"""MySQL instrumentation supporting mysql-connector."""
3030
# mypy: disable-error-code="no-redef"
31-
# pylint: disable=protected-access,global-statement,invalid-name
31+
# pylint: disable=protected-access,global-statement,invalid-name,unused-argument
3232

3333
from __future__ import annotations
3434

@@ -48,15 +48,8 @@
4848
# is a circular import issue when there isn't.
4949

5050
from ..abstracts import MySQLConnectionAbstract, MySQLCursorAbstract
51-
from ..connection import MySQLConnection
5251
from ..pooling import PooledMySQLConnection
5352

54-
try:
55-
from ..connection_cext import CMySQLConnection
56-
except ImportError:
57-
# The cext is not available.
58-
pass
59-
6053
from ... import connector
6154
from ..constants import CNX_POOL_ARGS, DEFAULT_CONFIGURATION
6255
from ..logger import logger
@@ -214,12 +207,10 @@ def set_connection_span_attrs(
214207
cnx_span.set_attributes(attrs)
215208

216209

217-
def with_connection_span_attached(method: Callable) -> Callable:
218-
"""Attach the connection span while executing a connection method."""
210+
def with_cnx_span_attached(method: Callable) -> Callable:
211+
"""Attach the connection span while executing the connection method."""
219212

220-
def wrapper(
221-
cnx: Union["MySQLConnection", "CMySQLConnection"], *args: Any, **kwargs: Any
222-
) -> Any:
213+
def wrapper(cnx: "MySQLConnectionAbstract", *args: Any, **kwargs: Any) -> Any:
223214
"""Connection span attacher decorator."""
224215
with trace.use_span(
225216
cnx._span, end_on_exit=False
@@ -229,41 +220,62 @@ def wrapper(
229220
return wrapper
230221

231222

232-
def _instrument_execution(
233-
query_method: Callable,
234-
tracer: trace.Tracer,
235-
connection_span_link: trace.Link,
236-
wrapped: "MySQLCursorAbstract",
237-
*args: Any,
238-
**kwargs: Any,
239-
) -> Callable:
240-
"""Instruments the execution of `query_method`.
223+
def with_cnx_query_span(method: Callable) -> Callable:
224+
"""Create a query span while executing the connection method."""
241225

242-
A query span with a link to the corresponding connection span is generated.
243-
"""
244-
connection: Union["MySQLConnection", "CMySQLConnection"] = (
245-
getattr(wrapped, "_connection")
246-
if hasattr(wrapped, "_connection")
247-
else getattr(wrapped, "_cnx")
248-
)
249-
250-
# SpanAttributes.DB_NAME: connection.database or ""; introduces performance
251-
# degradation, at this time the database attribute is something nice to have but
252-
# not a requirement.
253-
query_span_attributes: Dict = {
254-
SpanAttributes.DB_SYSTEM: DB_SYSTEM,
255-
SpanAttributes.DB_USER: connection._user,
256-
SpanAttributes.THREAD_ID: DEFAULT_THREAD_ID,
257-
SpanAttributes.THREAD_NAME: DEFAULT_THREAD_NAME,
258-
"cursor_type": wrapped.__class__.__name__,
259-
}
260-
with tracer.start_as_current_span(
261-
name=get_operation_name(args[0]) or "SQL statement",
262-
kind=trace.SpanKind.CLIENT,
263-
links=[connection_span_link],
264-
attributes=query_span_attributes,
265-
):
266-
return query_method(*args, **kwargs)
226+
def wrapper(cnx: TracedMySQLConnection, *args: Any, **kwargs: Any) -> Any:
227+
"""Query span creator decorator."""
228+
logger.info("Creating query span for connection.%s", method.__name__)
229+
230+
query_span_attributes: Dict = {
231+
SpanAttributes.DB_SYSTEM: DB_SYSTEM,
232+
SpanAttributes.DB_USER: cnx._user,
233+
SpanAttributes.THREAD_ID: DEFAULT_THREAD_ID,
234+
SpanAttributes.THREAD_NAME: DEFAULT_THREAD_NAME,
235+
"connection_type": cnx.get_wrapped_class(),
236+
}
237+
238+
with cnx._tracer.start_as_current_span(
239+
name=method.__name__.upper(),
240+
kind=trace.SpanKind.CLIENT,
241+
links=[trace.Link(cnx._span.get_span_context())],
242+
attributes=query_span_attributes,
243+
) if cnx._span and cnx._span.is_recording() else nullcontext():
244+
return method(cnx, *args, **kwargs)
245+
246+
return wrapper
247+
248+
249+
def with_cursor_query_span(method: Callable) -> Callable:
250+
"""Create a query span while executing the cursor method."""
251+
252+
def wrapper(cur: TracedMySQLCursor, *args: Any, **kwargs: Any) -> Any:
253+
"""Query span creator decorator."""
254+
logger.info("Creating query span for cursor.%s", method.__name__)
255+
256+
connection: "MySQLConnectionAbstract" = (
257+
getattr(cur._wrapped, "_connection")
258+
if hasattr(cur._wrapped, "_connection")
259+
else getattr(cur._wrapped, "_cnx")
260+
)
261+
262+
query_span_attributes: Dict = {
263+
SpanAttributes.DB_SYSTEM: DB_SYSTEM,
264+
SpanAttributes.DB_USER: connection._user,
265+
SpanAttributes.THREAD_ID: DEFAULT_THREAD_ID,
266+
SpanAttributes.THREAD_NAME: DEFAULT_THREAD_NAME,
267+
"cursor_type": cur.get_wrapped_class(),
268+
}
269+
270+
with cur._tracer.start_as_current_span(
271+
name=get_operation_name(args[0]) or "SQL statement",
272+
kind=trace.SpanKind.CLIENT,
273+
links=[cur._connection_span_link],
274+
attributes=query_span_attributes,
275+
):
276+
return method(cur, *args, **kwargs)
277+
278+
return wrapper
267279

268280

269281
class BaseMySQLTracer(ABC):
@@ -291,7 +303,7 @@ def __setattr__(self, name: str, value: Any) -> None:
291303
self.__dict__["_wrapped"] = value
292304
return
293305

294-
if name in self.__dict__:
306+
if name in self.__dict__ or name == "autocommit":
295307
# this object has it
296308
super().__setattr__(name, value)
297309
return
@@ -328,38 +340,20 @@ def __init__(
328340
connection_span.get_span_context()
329341
)
330342

343+
@with_cursor_query_span
331344
def execute(self, *args: Any, **kwargs: Any) -> Any:
332-
"""Instruments execute method."""
333-
return _instrument_execution(
334-
self._wrapped.execute,
335-
self._tracer,
336-
self._connection_span_link,
337-
self._wrapped,
338-
*args,
339-
**kwargs,
340-
)
345+
"""Instrument method."""
346+
return self._wrapped.execute(*args, **kwargs)
341347

348+
@with_cursor_query_span
342349
def executemany(self, *args: Any, **kwargs: Any) -> Any:
343-
"""Instruments executemany method."""
344-
return _instrument_execution(
345-
self._wrapped.executemany,
346-
self._tracer,
347-
self._connection_span_link,
348-
self._wrapped,
349-
*args,
350-
**kwargs,
351-
)
350+
"""Instrument method."""
351+
return self._wrapped.executemany(*args, **kwargs)
352352

353+
@with_cursor_query_span
353354
def callproc(self, *args: Any, **kwargs: Any) -> Any:
354-
"""Instruments callproc method."""
355-
return _instrument_execution(
356-
self._wrapped.callproc,
357-
self._tracer,
358-
self._connection_span_link,
359-
self._wrapped,
360-
*args,
361-
**kwargs,
362-
)
355+
"""Instrument method."""
356+
return self._wrapped.callproc(*args, **kwargs)
363357

364358

365359
class TracedMySQLConnection(BaseMySQLTracer):
@@ -381,11 +375,156 @@ def cursor(self, *args: Any, **kwargs: Any) -> TracedMySQLCursor:
381375
connection_span=self._span,
382376
)
383377

384-
@with_connection_span_attached
378+
@with_cnx_query_span
385379
def cmd_change_user(self, *args: Any, **kwargs: Any) -> Any:
386-
"""Wraps the object method."""
380+
"""Instrument method."""
387381
return self._wrapped.cmd_change_user(*args, **kwargs)
388382

383+
@with_cnx_query_span
384+
def commit(self, *args: Any, **kwargs: Any) -> Any:
385+
"""Instrument method."""
386+
return self._wrapped.commit(*args, **kwargs)
387+
388+
@with_cnx_query_span
389+
def rollback(self, *args: Any, **kwargs: Any) -> Any:
390+
"""Instrument method."""
391+
return self._wrapped.rollback(*args, **kwargs)
392+
393+
@with_cnx_query_span
394+
def cmd_query(self, *args: Any, **kwargs: Any) -> Any:
395+
"""Instrument method."""
396+
return self._wrapped.cmd_query(*args, **kwargs)
397+
398+
@with_cnx_query_span
399+
def cmd_init_db(self, *args: Any, **kwargs: Any) -> Any:
400+
"""Instrument method."""
401+
return self._wrapped.cmd_init_db(*args, **kwargs)
402+
403+
@with_cnx_query_span
404+
def cmd_refresh(self, *args: Any, **kwargs: Any) -> Any:
405+
"""Instrument method."""
406+
return self._wrapped.cmd_refresh(*args, **kwargs)
407+
408+
@with_cnx_query_span
409+
def cmd_quit(self, *args: Any, **kwargs: Any) -> Any:
410+
"""Instrument method."""
411+
return self._wrapped.cmd_quit(*args, **kwargs)
412+
413+
@with_cnx_query_span
414+
def cmd_shutdown(self, *args: Any, **kwargs: Any) -> Any:
415+
"""Instrument method."""
416+
return self._wrapped.cmd_shutdown(*args, **kwargs)
417+
418+
@with_cnx_query_span
419+
def cmd_statistics(self, *args: Any, **kwargs: Any) -> Any:
420+
"""Instrument method."""
421+
return self._wrapped.cmd_statistics(*args, **kwargs)
422+
423+
@with_cnx_query_span
424+
def cmd_process_kill(self, *args: Any, **kwargs: Any) -> Any:
425+
"""Instrument method."""
426+
return self._wrapped.cmd_process_kill(*args, **kwargs)
427+
428+
@with_cnx_query_span
429+
def cmd_debug(self, *args: Any, **kwargs: Any) -> Any:
430+
"""Instrument method."""
431+
return self._wrapped.cmd_debug(*args, **kwargs)
432+
433+
@with_cnx_query_span
434+
def cmd_ping(self, *args: Any, **kwargs: Any) -> Any:
435+
"""Instrument method."""
436+
return self._wrapped.cmd_ping(*args, **kwargs)
437+
438+
@property
439+
@with_cnx_query_span
440+
def database(self, *args: Any, **kwargs: Any) -> str:
441+
"""Instrument method."""
442+
return self._wrapped.database
443+
444+
@with_cnx_query_span
445+
def is_connected(self, *args: Any, **kwargs: Any) -> Any:
446+
"""Instrument method."""
447+
return self._wrapped.is_connected(*args, **kwargs)
448+
449+
@with_cnx_query_span
450+
def reset_session(self, *args: Any, **kwargs: Any) -> Any:
451+
"""Instrument method."""
452+
return self._wrapped.reset_session(*args, **kwargs)
453+
454+
@with_cnx_query_span
455+
def ping(self, *args: Any, **kwargs: Any) -> Any:
456+
"""Instrument method."""
457+
return self._wrapped.ping(*args, **kwargs)
458+
459+
@with_cnx_query_span
460+
def info_query(self, *args: Any, **kwargs: Any) -> Any:
461+
"""Instrument method."""
462+
return self._wrapped.info_query(*args, **kwargs)
463+
464+
@with_cnx_query_span
465+
def cmd_stmt_prepare(self, *args: Any, **kwargs: Any) -> Any:
466+
"""Instrument method."""
467+
return self._wrapped.cmd_stmt_prepare(*args, **kwargs)
468+
469+
@with_cnx_query_span
470+
def cmd_stmt_execute(self, *args: Any, **kwargs: Any) -> Any:
471+
"""Instrument method."""
472+
return self._wrapped.cmd_stmt_execute(*args, **kwargs)
473+
474+
@with_cnx_query_span
475+
def cmd_stmt_close(self, *args: Any, **kwargs: Any) -> Any:
476+
"""Instrument method."""
477+
return self._wrapped.cmd_stmt_close(*args, **kwargs)
478+
479+
@with_cnx_query_span
480+
def cmd_stmt_send_long_data(self, *args: Any, **kwargs: Any) -> Any:
481+
"""Instrument method."""
482+
return self._wrapped.cmd_stmt_send_long_data(*args, **kwargs)
483+
484+
@with_cnx_query_span
485+
def cmd_stmt_reset(self, *args: Any, **kwargs: Any) -> Any:
486+
"""Instrument method."""
487+
return self._wrapped.cmd_stmt_reset(*args, **kwargs)
488+
489+
@with_cnx_query_span
490+
def cmd_reset_connection(self, *args: Any, **kwargs: Any) -> Any:
491+
"""Instrument method."""
492+
return self._wrapped.cmd_reset_connection(*args, **kwargs)
493+
494+
@property
495+
@with_cnx_query_span
496+
def time_zone(self, *args: Any, **kwargs: Any) -> str:
497+
"""Instrument method."""
498+
return self._wrapped.time_zone
499+
500+
@property
501+
@with_cnx_query_span
502+
def sql_mode(self, *args: Any, **kwargs: Any) -> str:
503+
"""Instrument method."""
504+
return self._wrapped.sql_mode
505+
506+
@property
507+
@with_cnx_query_span
508+
def autocommit(self, *args: Any, **kwargs: Any) -> bool:
509+
"""Instrument method."""
510+
return self._wrapped.autocommit
511+
512+
@autocommit.setter
513+
@with_cnx_query_span
514+
def autocommit(self, value: bool) -> None:
515+
"""Instrument method."""
516+
self._wrapped.autocommit = value
517+
518+
@with_cnx_query_span
519+
def set_charset_collation(self, *args: Any, **kwargs: Any) -> Any:
520+
"""Instrument method."""
521+
return self._wrapped.set_charset_collation(*args, **kwargs)
522+
523+
@with_cnx_query_span
524+
def start_transaction(self, *args: Any, **kwargs: Any) -> Any:
525+
"""Instrument method."""
526+
return self._wrapped.start_transaction(*args, **kwargs)
527+
389528

390529
def _instrument_connect(
391530
connect: Callable[..., Union["MySQLConnectionAbstract", "PooledMySQLConnection"]],

0 commit comments

Comments
 (0)