Skip to content

Commit b01f7e8

Browse files
hectorhdzgtoumorokoshi
authored andcommitted
Adding DB API integration + MySQL connector integration (#264)
Adding the ext.dbapi and ext.mysql package.
1 parent b72cab5 commit b01f7e8

File tree

15 files changed

+731
-4
lines changed

15 files changed

+731
-4
lines changed
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
OpenTelemetry Database API integration
2+
=================================
3+
4+
The trace integration with Database API supports libraries following the specification.
5+
6+
.. PEP 249 -- Python Database API Specification v2.0: https://www.python.org/dev/peps/pep-0249/
7+
8+
Usage
9+
-----
10+
11+
.. code:: python
12+
13+
import mysql.connector
14+
from opentelemetry.trace import tracer
15+
from opentelemetry.ext.dbapi import trace_integration
16+
17+
18+
# Ex: mysql.connector
19+
trace_integration(tracer(), mysql.connector, "connect", "mysql")
20+
21+
22+
References
23+
----------
24+
25+
* `OpenTelemetry Project <https://opentelemetry.io/>`_

ext/opentelemetry-ext-dbapi/setup.cfg

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
[metadata]
16+
name = opentelemetry-ext-dbapi
17+
description = OpenTelemetry Database API integration
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = [email protected]
22+
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-dbapi
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 3 - Alpha
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.4
32+
Programming Language :: Python :: 3.5
33+
Programming Language :: Python :: 3.6
34+
Programming Language :: Python :: 3.7
35+
36+
[options]
37+
python_requires = >=3.4
38+
package_dir=
39+
=src
40+
packages=find_namespace:
41+
install_requires =
42+
opentelemetry-api >= 0.4.dev0
43+
wrapt >= 1.0.0, < 2.0.0
44+
45+
[options.packages.find]
46+
where = src

ext/opentelemetry-ext-dbapi/setup.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import os
15+
16+
import setuptools
17+
18+
BASE_DIR = os.path.dirname(__file__)
19+
VERSION_FILENAME = os.path.join(
20+
BASE_DIR, "src", "opentelemetry", "ext", "dbapi", "version.py"
21+
)
22+
PACKAGE_INFO = {}
23+
with open(VERSION_FILENAME) as f:
24+
exec(f.read(), PACKAGE_INFO)
25+
26+
setuptools.setup(version=PACKAGE_INFO["__version__"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
The opentelemetry-ext-dbapi package allows tracing queries made by the
17+
ibraries following Ptyhon Database API specification:
18+
https://www.python.org/dev/peps/pep-0249/
19+
"""
20+
21+
import logging
22+
import typing
23+
24+
import wrapt
25+
26+
from opentelemetry.trace import SpanKind, Tracer
27+
from opentelemetry.trace.status import Status, StatusCanonicalCode
28+
29+
logger = logging.getLogger(__name__)
30+
31+
32+
def trace_integration(
33+
tracer: Tracer,
34+
connect_module: typing.Callable[..., any],
35+
connect_method_name: str,
36+
database_component: str,
37+
database_type: str = "",
38+
connection_attributes: typing.Dict = None,
39+
):
40+
"""Integrate with DB API library.
41+
https://www.python.org/dev/peps/pep-0249/
42+
Args:
43+
tracer: The :class:`Tracer` to use.
44+
connect_module: Module name where connect method is available.
45+
connect_method_name: The connect method name.
46+
database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL".
47+
database_type: The Database type. For any SQL database, "sql".
48+
connection_attributes: Attribute names for database, port, host and user in Connection object.
49+
"""
50+
51+
# pylint: disable=unused-argument
52+
def wrap_connect(
53+
wrapped: typing.Callable[..., any],
54+
instance: typing.Any,
55+
args: typing.Tuple[any, any],
56+
kwargs: typing.Dict[any, any],
57+
):
58+
db_integration = DatabaseApiIntegration(
59+
tracer,
60+
database_component,
61+
database_type=database_type,
62+
connection_attributes=connection_attributes,
63+
)
64+
return db_integration.wrapped_connection(wrapped, args, kwargs)
65+
66+
try:
67+
wrapt.wrap_function_wrapper(
68+
connect_module, connect_method_name, wrap_connect
69+
)
70+
except Exception as ex: # pylint: disable=broad-except
71+
logger.warning("Failed to integrate with DB API. %s", str(ex))
72+
73+
74+
class DatabaseApiIntegration:
75+
# pylint: disable=unused-argument
76+
def __init__(
77+
self,
78+
tracer: Tracer,
79+
database_component: str,
80+
database_type: str = "sql",
81+
connection_attributes=None,
82+
):
83+
if tracer is None:
84+
raise ValueError("The tracer is not provided.")
85+
self.connection_attributes = connection_attributes
86+
if self.connection_attributes is None:
87+
self.connection_attributes = {
88+
"database": "database",
89+
"port": "port",
90+
"host": "host",
91+
"user": "user",
92+
}
93+
self.tracer = tracer
94+
self.database_component = database_component
95+
self.database_type = database_type
96+
self.connection_props = {}
97+
self.span_attributes = {}
98+
self.name = ""
99+
self.database = ""
100+
101+
def wrapped_connection(
102+
self,
103+
connect_method: typing.Callable[..., any],
104+
args: typing.Tuple[any, any],
105+
kwargs: typing.Dict[any, any],
106+
):
107+
"""Add object proxy to connection object.
108+
"""
109+
connection = connect_method(*args, **kwargs)
110+
111+
for key, value in self.connection_attributes.items():
112+
attribute = getattr(connection, value, None)
113+
if attribute:
114+
self.connection_props[key] = attribute
115+
traced_connection = TracedConnection(connection, self)
116+
return traced_connection
117+
118+
119+
# pylint: disable=abstract-method
120+
class TracedConnection(wrapt.ObjectProxy):
121+
122+
# pylint: disable=unused-argument
123+
def __init__(
124+
self,
125+
connection,
126+
db_api_integration: DatabaseApiIntegration,
127+
*args,
128+
**kwargs
129+
):
130+
wrapt.ObjectProxy.__init__(self, connection)
131+
self._db_api_integration = db_api_integration
132+
133+
self._db_api_integration.name = (
134+
self._db_api_integration.database_component
135+
)
136+
self._db_api_integration.database = self._db_api_integration.connection_props.get(
137+
"database", ""
138+
)
139+
if self._db_api_integration.database:
140+
self._db_api_integration.name += (
141+
"." + self._db_api_integration.database
142+
)
143+
user = self._db_api_integration.connection_props.get("user")
144+
if user is not None:
145+
self._db_api_integration.span_attributes["db.user"] = user
146+
host = self._db_api_integration.connection_props.get("host")
147+
if host is not None:
148+
self._db_api_integration.span_attributes["net.peer.name"] = host
149+
port = self._db_api_integration.connection_props.get("port")
150+
if port is not None:
151+
self._db_api_integration.span_attributes["net.peer.port"] = port
152+
153+
def cursor(self, *args, **kwargs):
154+
return TracedCursor(
155+
self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration
156+
)
157+
158+
159+
# pylint: disable=abstract-method
160+
class TracedCursor(wrapt.ObjectProxy):
161+
162+
# pylint: disable=unused-argument
163+
def __init__(
164+
self,
165+
cursor,
166+
db_api_integration: DatabaseApiIntegration,
167+
*args,
168+
**kwargs
169+
):
170+
wrapt.ObjectProxy.__init__(self, cursor)
171+
self._db_api_integration = db_api_integration
172+
173+
def execute(self, *args, **kwargs):
174+
return self._traced_execution(
175+
self.__wrapped__.execute, *args, **kwargs
176+
)
177+
178+
def executemany(self, *args, **kwargs):
179+
return self._traced_execution(
180+
self.__wrapped__.executemany, *args, **kwargs
181+
)
182+
183+
def callproc(self, *args, **kwargs):
184+
return self._traced_execution(
185+
self.__wrapped__.callproc, *args, **kwargs
186+
)
187+
188+
def _traced_execution(
189+
self,
190+
query_method: typing.Callable[..., any],
191+
*args: typing.Tuple[any, any],
192+
**kwargs: typing.Dict[any, any]
193+
):
194+
195+
statement = args[0] if args else ""
196+
with self._db_api_integration.tracer.start_as_current_span(
197+
self._db_api_integration.name, kind=SpanKind.CLIENT
198+
) as span:
199+
span.set_attribute(
200+
"component", self._db_api_integration.database_component
201+
)
202+
span.set_attribute(
203+
"db.type", self._db_api_integration.database_type
204+
)
205+
span.set_attribute(
206+
"db.instance", self._db_api_integration.database
207+
)
208+
span.set_attribute("db.statement", statement)
209+
210+
for (
211+
attribute_key,
212+
attribute_value,
213+
) in self._db_api_integration.span_attributes.items():
214+
span.set_attribute(attribute_key, attribute_value)
215+
216+
if len(args) > 1:
217+
span.set_attribute("db.statement.parameters", str(args[1]))
218+
219+
try:
220+
result = query_method(*args, **kwargs)
221+
span.set_status(Status(StatusCanonicalCode.OK))
222+
return result
223+
except Exception as ex: # pylint: disable=broad-except
224+
span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex)))
225+
raise ex
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
__version__ = "0.4.dev0"

ext/opentelemetry-ext-dbapi/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)