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