diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst
new file mode 100644
index 00000000000..6dc31216039
--- /dev/null
+++ b/ext/opentelemetry-ext-dbapi/README.rst
@@ -0,0 +1,25 @@
+OpenTelemetry Database API integration
+=================================
+
+The trace integration with Database API supports libraries following the specification.
+
+.. PEP 249 -- Python Database API Specification v2.0: https://www.python.org/dev/peps/pep-0249/
+
+Usage
+-----
+
+.. code:: python
+
+ import mysql.connector
+ from opentelemetry.trace import tracer
+ from opentelemetry.ext.dbapi import trace_integration
+
+
+ # Ex: mysql.connector
+ trace_integration(tracer(), mysql.connector, "connect", "mysql")
+
+
+References
+----------
+
+* `OpenTelemetry Project `_
diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg
new file mode 100644
index 00000000000..f0de68dc263
--- /dev/null
+++ b/ext/opentelemetry-ext-dbapi/setup.cfg
@@ -0,0 +1,46 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+[metadata]
+name = opentelemetry-ext-dbapi
+description = OpenTelemetry Database API integration
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = OpenTelemetry Authors
+author_email = cncf-opentelemetry-contributors@lists.cncf.io
+url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-dbapi
+platforms = any
+license = Apache-2.0
+classifiers =
+ Development Status :: 3 - Alpha
+ Intended Audience :: Developers
+ License :: OSI Approved :: Apache Software License
+ Programming Language :: Python
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.4
+ Programming Language :: Python :: 3.5
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+
+[options]
+python_requires = >=3.4
+package_dir=
+ =src
+packages=find_namespace:
+install_requires =
+ opentelemetry-api >= 0.4.dev0
+ wrapt >= 1.0.0, < 2.0.0
+
+[options.packages.find]
+where = src
diff --git a/ext/opentelemetry-ext-dbapi/setup.py b/ext/opentelemetry-ext-dbapi/setup.py
new file mode 100644
index 00000000000..5e1a68ac51b
--- /dev/null
+++ b/ext/opentelemetry-ext-dbapi/setup.py
@@ -0,0 +1,26 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import os
+
+import setuptools
+
+BASE_DIR = os.path.dirname(__file__)
+VERSION_FILENAME = os.path.join(
+ BASE_DIR, "src", "opentelemetry", "ext", "dbapi", "version.py"
+)
+PACKAGE_INFO = {}
+with open(VERSION_FILENAME) as f:
+ exec(f.read(), PACKAGE_INFO)
+
+setuptools.setup(version=PACKAGE_INFO["__version__"])
diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py
new file mode 100644
index 00000000000..7ba1de1795a
--- /dev/null
+++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py
@@ -0,0 +1,225 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+The opentelemetry-ext-dbapi package allows tracing queries made by the
+ibraries following Ptyhon Database API specification:
+https://www.python.org/dev/peps/pep-0249/
+"""
+
+import logging
+import typing
+
+import wrapt
+
+from opentelemetry.trace import SpanKind, Tracer
+from opentelemetry.trace.status import Status, StatusCanonicalCode
+
+logger = logging.getLogger(__name__)
+
+
+def trace_integration(
+ tracer: Tracer,
+ connect_module: typing.Callable[..., any],
+ connect_method_name: str,
+ database_component: str,
+ database_type: str = "",
+ connection_attributes: typing.Dict = None,
+):
+ """Integrate with DB API library.
+ https://www.python.org/dev/peps/pep-0249/
+ Args:
+ tracer: The :class:`Tracer` to use.
+ connect_module: Module name where connect method is available.
+ connect_method_name: The connect method name.
+ database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL".
+ database_type: The Database type. For any SQL database, "sql".
+ connection_attributes: Attribute names for database, port, host and user in Connection object.
+ """
+
+ # pylint: disable=unused-argument
+ def wrap_connect(
+ wrapped: typing.Callable[..., any],
+ instance: typing.Any,
+ args: typing.Tuple[any, any],
+ kwargs: typing.Dict[any, any],
+ ):
+ db_integration = DatabaseApiIntegration(
+ tracer,
+ database_component,
+ database_type=database_type,
+ connection_attributes=connection_attributes,
+ )
+ return db_integration.wrapped_connection(wrapped, args, kwargs)
+
+ try:
+ wrapt.wrap_function_wrapper(
+ connect_module, connect_method_name, wrap_connect
+ )
+ except Exception as ex: # pylint: disable=broad-except
+ logger.warning("Failed to integrate with DB API. %s", str(ex))
+
+
+class DatabaseApiIntegration:
+ # pylint: disable=unused-argument
+ def __init__(
+ self,
+ tracer: Tracer,
+ database_component: str,
+ database_type: str = "sql",
+ connection_attributes=None,
+ ):
+ if tracer is None:
+ raise ValueError("The tracer is not provided.")
+ self.connection_attributes = connection_attributes
+ if self.connection_attributes is None:
+ self.connection_attributes = {
+ "database": "database",
+ "port": "port",
+ "host": "host",
+ "user": "user",
+ }
+ self.tracer = tracer
+ self.database_component = database_component
+ self.database_type = database_type
+ self.connection_props = {}
+ self.span_attributes = {}
+ self.name = ""
+ self.database = ""
+
+ def wrapped_connection(
+ self,
+ connect_method: typing.Callable[..., any],
+ args: typing.Tuple[any, any],
+ kwargs: typing.Dict[any, any],
+ ):
+ """Add object proxy to connection object.
+ """
+ connection = connect_method(*args, **kwargs)
+
+ for key, value in self.connection_attributes.items():
+ attribute = getattr(connection, value, None)
+ if attribute:
+ self.connection_props[key] = attribute
+ traced_connection = TracedConnection(connection, self)
+ return traced_connection
+
+
+# pylint: disable=abstract-method
+class TracedConnection(wrapt.ObjectProxy):
+
+ # pylint: disable=unused-argument
+ def __init__(
+ self,
+ connection,
+ db_api_integration: DatabaseApiIntegration,
+ *args,
+ **kwargs
+ ):
+ wrapt.ObjectProxy.__init__(self, connection)
+ self._db_api_integration = db_api_integration
+
+ self._db_api_integration.name = (
+ self._db_api_integration.database_component
+ )
+ self._db_api_integration.database = self._db_api_integration.connection_props.get(
+ "database", ""
+ )
+ if self._db_api_integration.database:
+ self._db_api_integration.name += (
+ "." + self._db_api_integration.database
+ )
+ user = self._db_api_integration.connection_props.get("user")
+ if user is not None:
+ self._db_api_integration.span_attributes["db.user"] = user
+ host = self._db_api_integration.connection_props.get("host")
+ if host is not None:
+ self._db_api_integration.span_attributes["net.peer.name"] = host
+ port = self._db_api_integration.connection_props.get("port")
+ if port is not None:
+ self._db_api_integration.span_attributes["net.peer.port"] = port
+
+ def cursor(self, *args, **kwargs):
+ return TracedCursor(
+ self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration
+ )
+
+
+# pylint: disable=abstract-method
+class TracedCursor(wrapt.ObjectProxy):
+
+ # pylint: disable=unused-argument
+ def __init__(
+ self,
+ cursor,
+ db_api_integration: DatabaseApiIntegration,
+ *args,
+ **kwargs
+ ):
+ wrapt.ObjectProxy.__init__(self, cursor)
+ self._db_api_integration = db_api_integration
+
+ def execute(self, *args, **kwargs):
+ return self._traced_execution(
+ self.__wrapped__.execute, *args, **kwargs
+ )
+
+ def executemany(self, *args, **kwargs):
+ return self._traced_execution(
+ self.__wrapped__.executemany, *args, **kwargs
+ )
+
+ def callproc(self, *args, **kwargs):
+ return self._traced_execution(
+ self.__wrapped__.callproc, *args, **kwargs
+ )
+
+ def _traced_execution(
+ self,
+ query_method: typing.Callable[..., any],
+ *args: typing.Tuple[any, any],
+ **kwargs: typing.Dict[any, any]
+ ):
+
+ statement = args[0] if args else ""
+ with self._db_api_integration.tracer.start_as_current_span(
+ self._db_api_integration.name, kind=SpanKind.CLIENT
+ ) as span:
+ span.set_attribute(
+ "component", self._db_api_integration.database_component
+ )
+ span.set_attribute(
+ "db.type", self._db_api_integration.database_type
+ )
+ span.set_attribute(
+ "db.instance", self._db_api_integration.database
+ )
+ span.set_attribute("db.statement", statement)
+
+ for (
+ attribute_key,
+ attribute_value,
+ ) in self._db_api_integration.span_attributes.items():
+ span.set_attribute(attribute_key, attribute_value)
+
+ if len(args) > 1:
+ span.set_attribute("db.statement.parameters", str(args[1]))
+
+ try:
+ result = query_method(*args, **kwargs)
+ span.set_status(Status(StatusCanonicalCode.OK))
+ return result
+ except Exception as ex: # pylint: disable=broad-except
+ span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex)))
+ raise ex
diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py
new file mode 100644
index 00000000000..2f792fff802
--- /dev/null
+++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py
@@ -0,0 +1,15 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+__version__ = "0.4.dev0"
diff --git a/ext/opentelemetry-ext-dbapi/tests/__init__.py b/ext/opentelemetry-ext-dbapi/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py
new file mode 100644
index 00000000000..f5d1299e838
--- /dev/null
+++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py
@@ -0,0 +1,181 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+from unittest import mock
+
+from opentelemetry import trace as trace_api
+from opentelemetry.ext.dbapi import DatabaseApiIntegration
+
+
+class TestDBApiIntegration(unittest.TestCase):
+ def setUp(self):
+ self.tracer = trace_api.Tracer()
+ self.span = MockSpan()
+ self.start_current_span_patcher = mock.patch.object(
+ self.tracer,
+ "start_as_current_span",
+ autospec=True,
+ spec_set=True,
+ return_value=self.span,
+ )
+
+ self.start_as_current_span = self.start_current_span_patcher.start()
+
+ def tearDown(self):
+ self.start_current_span_patcher.stop()
+
+ def test_span_succeeded(self):
+ connection_props = {
+ "database": "testdatabase",
+ "server_host": "testhost",
+ "server_port": 123,
+ "user": "testuser",
+ }
+ connection_attributes = {
+ "database": "database",
+ "port": "server_port",
+ "host": "server_host",
+ "user": "user",
+ }
+ db_integration = DatabaseApiIntegration(
+ self.tracer, "testcomponent", "testtype", connection_attributes
+ )
+ mock_connection = db_integration.wrapped_connection(
+ mock_connect, {}, connection_props
+ )
+ cursor = mock_connection.cursor()
+ cursor.execute("Test query", ("param1Value", False))
+ self.assertTrue(self.start_as_current_span.called)
+ self.assertEqual(
+ self.start_as_current_span.call_args[0][0],
+ "testcomponent.testdatabase",
+ )
+ self.assertIs(
+ self.start_as_current_span.call_args[1]["kind"],
+ trace_api.SpanKind.CLIENT,
+ )
+ self.assertEqual(self.span.attributes["component"], "testcomponent")
+ self.assertEqual(self.span.attributes["db.type"], "testtype")
+ self.assertEqual(self.span.attributes["db.instance"], "testdatabase")
+ self.assertEqual(self.span.attributes["db.statement"], "Test query")
+ self.assertEqual(
+ self.span.attributes["db.statement.parameters"],
+ "('param1Value', False)",
+ )
+ self.assertEqual(self.span.attributes["db.user"], "testuser")
+ self.assertEqual(self.span.attributes["net.peer.name"], "testhost")
+ self.assertEqual(self.span.attributes["net.peer.port"], 123)
+ self.assertIs(
+ self.span.status.canonical_code,
+ trace_api.status.StatusCanonicalCode.OK,
+ )
+
+ def test_span_failed(self):
+ db_integration = DatabaseApiIntegration(self.tracer, "testcomponent")
+ mock_connection = db_integration.wrapped_connection(
+ mock_connect, {}, {}
+ )
+ cursor = mock_connection.cursor()
+ try:
+ cursor.execute("Test query", throw_exception=True)
+ except Exception: # pylint: disable=broad-except
+ self.assertEqual(
+ self.span.attributes["db.statement"], "Test query"
+ )
+ self.assertIs(
+ self.span.status.canonical_code,
+ trace_api.status.StatusCanonicalCode.UNKNOWN,
+ )
+ self.assertEqual(self.span.status.description, "Test Exception")
+
+ def test_executemany(self):
+ db_integration = DatabaseApiIntegration(self.tracer, "testcomponent")
+ mock_connection = db_integration.wrapped_connection(
+ mock_connect, {}, {}
+ )
+ cursor = mock_connection.cursor()
+ cursor.executemany("Test query")
+ self.assertTrue(self.start_as_current_span.called)
+ self.assertEqual(self.span.attributes["db.statement"], "Test query")
+
+ def test_callproc(self):
+ db_integration = DatabaseApiIntegration(self.tracer, "testcomponent")
+ mock_connection = db_integration.wrapped_connection(
+ mock_connect, {}, {}
+ )
+ cursor = mock_connection.cursor()
+ cursor.callproc("Test stored procedure")
+ self.assertTrue(self.start_as_current_span.called)
+ self.assertEqual(
+ self.span.attributes["db.statement"], "Test stored procedure"
+ )
+
+
+# pylint: disable=unused-argument
+def mock_connect(*args, **kwargs):
+ database = kwargs.get("database")
+ server_host = kwargs.get("server_host")
+ server_port = kwargs.get("server_port")
+ user = kwargs.get("user")
+ return MockConnection(database, server_port, server_host, user)
+
+
+class MockConnection:
+ def __init__(self, database, server_port, server_host, user):
+ self.database = database
+ self.server_port = server_port
+ self.server_host = server_host
+ self.user = user
+
+ # pylint: disable=no-self-use
+ def cursor(self):
+ return MockCursor()
+
+
+class MockCursor:
+ # pylint: disable=unused-argument, no-self-use
+ def execute(self, query, params=None, throw_exception=False):
+ if throw_exception:
+ raise Exception("Test Exception")
+
+ # pylint: disable=unused-argument, no-self-use
+ def executemany(self, query, params=None, throw_exception=False):
+ if throw_exception:
+ raise Exception("Test Exception")
+
+ # pylint: disable=unused-argument, no-self-use
+ def callproc(self, query, params=None, throw_exception=False):
+ if throw_exception:
+ raise Exception("Test Exception")
+
+
+class MockSpan:
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ return False
+
+ def __init__(self):
+ self.status = None
+ self.name = ""
+ self.kind = trace_api.SpanKind.INTERNAL
+ self.attributes = {}
+
+ def set_attribute(self, key, value):
+ self.attributes[key] = value
+
+ def set_status(self, status):
+ self.status = status
diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst
new file mode 100644
index 00000000000..e819a637694
--- /dev/null
+++ b/ext/opentelemetry-ext-mysql/README.rst
@@ -0,0 +1,29 @@
+OpenTelemetry MySQL integration
+=================================
+
+The integration with MySQL supports the `mysql-connector`_ library and is specified
+to ``trace_integration`` using ``'MySQL'``.
+
+.. mysql-connector: https://pypi.org/project/mysql-connector/
+
+Usage
+-----
+
+.. code:: python
+
+ import mysql.connector
+ from opentelemetry.trace import tracer
+ from opentelemetry.ext.mysql import trace_integration
+
+ trace_integration(tracer())
+ cnx = mysql.connector.connect(database='MySQL_Database')
+ cursor = cnx.cursor()
+ cursor.execute("INSERT INTO test (testField) VALUES (123)"
+ cursor.close()
+ cnx.close()
+
+
+References
+----------
+
+* `OpenTelemetry Project `_
diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg
new file mode 100644
index 00000000000..fdc608bb3d1
--- /dev/null
+++ b/ext/opentelemetry-ext-mysql/setup.cfg
@@ -0,0 +1,47 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+[metadata]
+name = opentelemetry-ext-mysql
+description = OpenTelemetry MySQL integration
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = OpenTelemetry Authors
+author_email = cncf-opentelemetry-contributors@lists.cncf.io
+url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-mysql
+platforms = any
+license = Apache-2.0
+classifiers =
+ Development Status :: 3 - Alpha
+ Intended Audience :: Developers
+ License :: OSI Approved :: Apache Software License
+ Programming Language :: Python
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.4
+ Programming Language :: Python :: 3.5
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+
+[options]
+python_requires = >=3.4
+package_dir=
+ =src
+packages=find_namespace:
+install_requires =
+ opentelemetry-api >= 0.4.dev0
+ mysql-connector-python ~= 8.0
+ wrapt >= 1.0.0, < 2.0.0
+
+[options.packages.find]
+where = src
diff --git a/ext/opentelemetry-ext-mysql/setup.py b/ext/opentelemetry-ext-mysql/setup.py
new file mode 100644
index 00000000000..b2c62679e1f
--- /dev/null
+++ b/ext/opentelemetry-ext-mysql/setup.py
@@ -0,0 +1,26 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import os
+
+import setuptools
+
+BASE_DIR = os.path.dirname(__file__)
+VERSION_FILENAME = os.path.join(
+ BASE_DIR, "src", "opentelemetry", "ext", "mysql", "version.py"
+)
+PACKAGE_INFO = {}
+with open(VERSION_FILENAME) as f:
+ exec(f.read(), PACKAGE_INFO)
+
+setuptools.setup(version=PACKAGE_INFO["__version__"])
diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py
new file mode 100644
index 00000000000..9c8c3e9da70
--- /dev/null
+++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py
@@ -0,0 +1,43 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+The opentelemetry-ext-mysql package allows tracing MySQL queries made by the
+MySQL Connector/Python library.
+"""
+
+import mysql.connector
+
+from opentelemetry.ext.dbapi import trace_integration as db_integration
+from opentelemetry.trace import Tracer
+
+
+def trace_integration(tracer: Tracer):
+ """Integrate with MySQL Connector/Python library.
+ https://dev.mysql.com/doc/connector-python/en/
+ """
+ connection_attributes = {
+ "database": "database",
+ "port": "server_port",
+ "host": "server_host",
+ "user": "user",
+ }
+ db_integration(
+ tracer,
+ mysql.connector,
+ "connect",
+ "mysql",
+ "sql",
+ connection_attributes,
+ )
diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py
new file mode 100644
index 00000000000..2f792fff802
--- /dev/null
+++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py
@@ -0,0 +1,15 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+__version__ = "0.4.dev0"
diff --git a/ext/opentelemetry-ext-mysql/tests/__init__.py b/ext/opentelemetry-ext-mysql/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py
new file mode 100644
index 00000000000..1bcd851750c
--- /dev/null
+++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py
@@ -0,0 +1,44 @@
+# Copyright 2019, OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+from unittest import mock
+
+import mysql.connector
+
+from opentelemetry import trace as trace_api
+from opentelemetry.ext.mysql import trace_integration
+
+
+class TestMysqlIntegration(unittest.TestCase):
+ def test_trace_integration(self):
+ tracer = trace_api.Tracer()
+ span = mock.create_autospec(trace_api.Span, spec_set=True)
+ start_current_span_patcher = mock.patch.object(
+ tracer,
+ "start_as_current_span",
+ autospec=True,
+ spec_set=True,
+ return_value=span,
+ )
+ start_as_current_span = start_current_span_patcher.start()
+
+ with mock.patch("mysql.connector.connect") as mock_connect:
+ mock_connect.get.side_effect = mysql.connector.MySQLConnection()
+ trace_integration(tracer)
+ cnx = mysql.connector.connect(database="test")
+ cursor = cnx.cursor()
+ query = "SELECT * FROM test"
+ cursor.execute(query)
+ self.assertTrue(start_as_current_span.called)
diff --git a/tox.ini b/tox.ini
index 8a4321a95c5..9f2741db820 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,10 +2,10 @@
skipsdist = True
skip_missing_interpreters = True
envlist =
- py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim}
- pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim}
- py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim}
- pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-pymongo,ext-zipkin,opentracing-shim}
+ py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim}
+ pypy3-test-{api,sdk,example-app,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim}
+ py3{4,5,6,7,8}-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim}
+ pypy3-test-{api,sdk,example-app,example-basic-tracer,example-http,ext-wsgi,ext-flask,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,ext-pymongo,ext-zipkin,opentracing-shim}
py3{4,5,6,7,8}-coverage
; Coverage is temporarily disabled for pypy3 due to the pytest bug.
@@ -36,6 +36,8 @@ changedir =
test-sdk: opentelemetry-sdk/tests
test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests
test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests
+ test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests
+ test-ext-mysql: ext/opentelemetry-ext-mysql/tests
test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests
test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests
test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests
@@ -67,6 +69,9 @@ commands_pre =
wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil
wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi
flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test]
+ dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi
+ mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi
+ mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql
pymongo: pip install {toxinidir}/ext/opentelemetry-ext-pymongo
http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests
jaeger: pip install {toxinidir}/opentelemetry-sdk