Skip to content

Commit 50ded73

Browse files
committed
WL#15629: Add OpenTelemetry tracing
With this worklog, OTel (OpenTelemetry) tracing support is added. OTel already provides an `opentelemetry.MySQLInstrumentor` supporting mysql-connector-python which is compatible with this new feature, however, a new dedicated MySQL instrumentor is also provided as part of this new feature. This new instrumentor, which can be found in `mysql.connector.opentelemetry`, exposes the same API and has usage similar to `opentelemetry.MySQLInstrumentor`, nevertheless, the trace morphology is different. Additionally, a new connection property named `otel_context_propagation` is added, and it can be used to control the trace context propagation behavior. A value of `True` is used as the default meaning that the trace context in progress (if any) will be propagated to the server. If the user DOES NOT want the propagation to happen no matter what, `otel_context_propagation` should be set to `False`. Change-Id: I0203924344ec6ad555fcf02531dd65b59c5a5193
1 parent 34a00b1 commit 50ded73

File tree

126 files changed

+20949
-88
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+20949
-88
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ repos:
4343
django-stubs,
4444
gssapi==1.8.1,
4545
dnspython==2.1.0,
46+
Deprecated>=1.2.6,
47+
typing-extensions>=3.7.4,
48+
zipp>=0.5,
4649
]
4750
files: lib
51+
exclude: lib/mysql/opentelemetry
4852
args: [
4953
--disallow-untyped-defs,
5054
--show-error-codes,

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ v8.1.0
1414
- WL#15749: Remove DMG and MSI support
1515
- WL#15672: Upgrade Python Protobuf version to 4.21.12
1616
- WL#15630: Remove Python 3.7 support
17+
- WL#15629: Add OpenTelemetry tracing
1718
- WL#15591: Improve the network module
1819
- BUG#35425076: Fix deallocating None error
1920
- BUG#35349093: Compression doesn't work with C extension API

cpydist/__init__.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,31 @@
119119
LOGGER.setLevel(logging.WARNING)
120120

121121

122+
def get_otel_src_package_data():
123+
"""Get a list including all py.typed and dist-info files corresponding
124+
to opentelemetry-python (see [1]) located at
125+
`mysql/opentelemetry`.
126+
127+
Returns:
128+
package_data: List[str].
129+
130+
References:
131+
[1]: https://github.com/open-telemetry/opentelemetry-python
132+
"""
133+
path_otel = os.path.join("lib", "mysql", "opentelemetry")
134+
135+
package_data = []
136+
for root, dirs, filenames in os.walk(os.path.join(os.getcwd(), path_otel, "")):
137+
offset = root.replace(os.path.join(os.getcwd(), path_otel, ""), "")
138+
for _dir in dirs:
139+
if _dir.endswith(".dist-info"):
140+
package_data.append(os.path.join(offset, _dir, "*"))
141+
for filename in filenames:
142+
if filename == "py.typed":
143+
package_data.append(os.path.join(offset, filename))
144+
return package_data
145+
146+
122147
class BaseCommand(Command):
123148
"""Base command class for Connector/Python."""
124149

@@ -228,6 +253,7 @@ def finalize_options(self):
228253
self.distribution.package_data = {
229254
"mysql.connector": ["py.typed"],
230255
"mysqlx": ["py.typed"],
256+
"mysql.opentelemetry": get_otel_src_package_data(),
231257
}
232258
if not cmd_build_ext.skip_vendor:
233259
self._copy_vendor_libraries()
@@ -519,10 +545,9 @@ def _copy_vendor_libraries(self):
519545
"vendor/private/*",
520546
"vendor/private/sasl2/*",
521547
],
522-
"mysql.connector": [
523-
"py.typed",
524-
],
548+
"mysql.connector": ["py.typed"],
525549
"mysqlx": ["py.typed"],
550+
"mysql.opentelemetry": get_otel_src_package_data(),
526551
}
527552

528553

lib/mysql/connector/abstracts.py

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,26 @@
8686
OperationalError,
8787
ProgrammingError,
8888
)
89+
from .opentelemetry.constants import (
90+
CONNECTION_SPAN_NAME,
91+
OPTION_CNX_SPAN,
92+
OPTION_CNX_TRACER,
93+
OTEL_ENABLED,
94+
)
95+
96+
if OTEL_ENABLED:
97+
from .opentelemetry.instrumentation import (
98+
end_span,
99+
record_exception_event,
100+
set_connection_span_attrs,
101+
trace,
102+
)
103+
89104
from .optionfiles import read_option_files
90105
from .types import (
91106
ConnAttrsType,
92107
DescriptionType,
93108
HandShakeType,
94-
QueryAttrType,
95109
StrOrBytes,
96110
SupportedMysqlBinaryProtocolTypes,
97111
WarningType,
@@ -142,6 +156,11 @@ class MySQLConnectionAbstract(ABC):
142156

143157
def __init__(self) -> None:
144158
"""Initialize"""
159+
# opentelemetry related
160+
self._tracer: Any = None
161+
self._span: Any = None
162+
self.otel_context_propagation: bool = True
163+
145164
self._client_flags: int = ClientFlag.get_default()
146165
self._charset_id: int = 45
147166
self._sql_mode: Optional[str] = None
@@ -187,7 +206,7 @@ def __init__(self) -> None:
187206
]
188207

189208
self._prepared_statements: Any = None
190-
self._query_attrs: QueryAttrType = []
209+
self._query_attrs: Dict[str, Any] = {}
191210

192211
self._ssl_active: bool = False
193212
self._auth_plugin: Optional[str] = None
@@ -233,19 +252,32 @@ def have_next_result(self) -> bool:
233252
return self._have_next_result
234253

235254
@property
236-
def query_attrs(self) -> QueryAttrType:
255+
def query_attrs(self) -> List[Tuple[str, Any]]:
237256
"""Return query attributes list."""
238-
return self._query_attrs
257+
return list(self._query_attrs.items())
239258

240259
def query_attrs_append(
241260
self, value: Tuple[str, SupportedMysqlBinaryProtocolTypes]
242261
) -> None:
243-
"""Add element to the query attributes list."""
244-
self._query_attrs.append(value)
262+
"""Add element to the query attributes list.
263+
264+
If an element in the query attributes list already matches
265+
the attribute name provided, the new element will NOT be added.
266+
"""
267+
attr_name, attr_value = value
268+
if attr_name not in self._query_attrs:
269+
self._query_attrs[attr_name] = attr_value
270+
271+
def query_attrs_remove(self, name: str) -> Any:
272+
"""Remove element by name from the query attributes list.
273+
274+
If no match, `None` is returned; else the corresponding value is returned.
275+
"""
276+
return self._query_attrs.pop(name, None)
245277

246278
def query_attrs_clear(self) -> None:
247279
"""Clear query attributes list."""
248-
del self._query_attrs[:]
280+
self._query_attrs = {}
249281

250282
def _validate_tls_ciphersuites(self) -> None:
251283
"""Validates the tls_ciphersuites option."""
@@ -470,6 +502,10 @@ def config(self, **kwargs: Any) -> None:
470502
471503
Raises on errors.
472504
"""
505+
# opentelemetry related
506+
self._span = kwargs.pop(OPTION_CNX_SPAN, None)
507+
self._tracer = kwargs.pop(OPTION_CNX_TRACER, None)
508+
473509
config = kwargs.copy()
474510
if "dsn" in config:
475511
raise NotSupportedError("Data source name is not supported")
@@ -1198,22 +1234,40 @@ def reconnect(self, attempts: int = 1, delay: int = 0) -> None:
11981234
Raises InterfaceError on errors.
11991235
"""
12001236
counter = 0
1201-
while counter != attempts:
1202-
counter = counter + 1
1203-
try:
1204-
self.disconnect()
1205-
self.connect()
1206-
if self.is_connected():
1207-
break
1208-
except (Error, IOError) as err:
1209-
if counter == attempts:
1210-
msg = (
1211-
f"Can not reconnect to MySQL after {attempts} "
1212-
f"attempt(s): {err}"
1213-
)
1214-
raise InterfaceError(msg) from err
1215-
if delay > 0:
1216-
sleep(delay)
1237+
span = None
1238+
1239+
if self._tracer:
1240+
span = self._tracer.start_span(
1241+
name=CONNECTION_SPAN_NAME, kind=trace.SpanKind.CLIENT
1242+
)
1243+
1244+
try:
1245+
while counter != attempts:
1246+
counter = counter + 1
1247+
try:
1248+
self.disconnect()
1249+
self.connect()
1250+
if self.is_connected():
1251+
break
1252+
except (Error, IOError) as err:
1253+
if counter == attempts:
1254+
msg = (
1255+
f"Can not reconnect to MySQL after {attempts} "
1256+
f"attempt(s): {err}"
1257+
)
1258+
raise InterfaceError(msg) from err
1259+
if delay > 0:
1260+
sleep(delay)
1261+
except InterfaceError as interface_err:
1262+
if OTEL_ENABLED:
1263+
set_connection_span_attrs(self, span)
1264+
record_exception_event(span, interface_err)
1265+
end_span(span)
1266+
raise
1267+
1268+
self._span = span
1269+
if OTEL_ENABLED:
1270+
set_connection_span_attrs(self, self._span)
12171271

12181272
@abstractmethod
12191273
def is_connected(self) -> Any:
@@ -1553,7 +1607,7 @@ def close(self) -> Any:
15531607
@abstractmethod
15541608
def execute(
15551609
self,
1556-
operation: Any,
1610+
operation: str,
15571611
params: Union[Sequence[Any], Dict[str, Any]] = (),
15581612
multi: bool = False,
15591613
) -> Any:
@@ -1577,7 +1631,7 @@ def execute(
15771631

15781632
@abstractmethod
15791633
def executemany(
1580-
self, operation: Any, seq_params: Sequence[Union[Sequence[Any], Dict[str, Any]]]
1634+
self, operation: str, seq_params: Sequence[Union[Sequence[Any], Dict[str, Any]]]
15811635
) -> Any:
15821636
"""Execute the given operation multiple times
15831637
@@ -1710,7 +1764,7 @@ def fetchwarnings(self) -> Optional[List[WarningType]]:
17101764
"""Returns Warnings."""
17111765
return self._warnings
17121766

1713-
def get_attributes(self) -> Optional[List[Tuple[Any, Any]]]:
1767+
def get_attributes(self) -> Optional[List[Tuple[str, Any]]]:
17141768
"""Get the added query attributes so far."""
17151769
if hasattr(self, "_cnx"):
17161770
return self._cnx.query_attrs
@@ -1731,6 +1785,19 @@ def add_attribute(self, name: str, value: Any) -> None:
17311785
elif hasattr(self, "_connection"):
17321786
self._connection.query_attrs_append((name, value))
17331787

1788+
def remove_attribute(self, name: str) -> Any:
1789+
"""Remove a query attribute by name.
1790+
1791+
If no match, `None` is returned; else the corresponding value is returned.
1792+
"""
1793+
if not isinstance(name, str):
1794+
raise ProgrammingError("Parameter `name` must be a string type")
1795+
if hasattr(self, "_cnx"):
1796+
return self._cnx.query_attrs_remove(name)
1797+
if hasattr(self, "_connection"):
1798+
return self._connection.query_attrs_remove(name)
1799+
return None
1800+
17341801
def clear_attributes(self) -> None:
17351802
"""Remove all the query attributes."""
17361803
if hasattr(self, "_cnx"):

0 commit comments

Comments
 (0)