diff --git a/docs/ext/pymysql/pymysql.rst b/docs/ext/pymysql/pymysql.rst new file mode 100644 index 00000000000..23dca80c4f2 --- /dev/null +++ b/docs/ext/pymysql/pymysql.rst @@ -0,0 +1,7 @@ +OpenTelemetry PyMySQL Integration +================================= + +.. automodule:: opentelemetry.ext.pymysql + :members: + :undoc-members: + :show-inheritance: diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py index 441434b1897..7a94b58957f 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -178,6 +178,9 @@ def get_connection_attributes(self, connection): self.name = self.database_component self.database = self.connection_props.get("database", "") if self.database: + # PyMySQL encodes names with utf-8 + if hasattr(self.database, "decode"): + self.database = self.database.decode(errors="ignore") self.name += "." + self.database user = self.connection_props.get("user") if user is not None: diff --git a/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py new file mode 100644 index 00000000000..e4ee65af30f --- /dev/null +++ b/ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py @@ -0,0 +1,108 @@ +# Copyright The 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 time +import unittest + +import pymysql as pymy + +from opentelemetry import trace as trace_api +from opentelemetry.ext.pymysql import trace_integration +from opentelemetry.sdk.trace import Tracer, TracerProvider +from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +MYSQL_USER = os.getenv("MYSQL_USER ", "testuser") +MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword") +MYSQL_HOST = os.getenv("MYSQL_HOST ", "localhost") +MYSQL_PORT = int(os.getenv("MYSQL_PORT ", "3306")) +MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests") + + +class TestFunctionalPyMysql(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._connection = None + cls._cursor = None + cls._tracer_provider = TracerProvider() + cls._tracer = Tracer(cls._tracer_provider, None) + cls._span_exporter = InMemorySpanExporter() + cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter) + cls._tracer_provider.add_span_processor(cls._span_processor) + trace_integration(cls._tracer_provider) + cls._connection = pymy.connect( + user=MYSQL_USER, + password=MYSQL_PASSWORD, + host=MYSQL_HOST, + port=MYSQL_PORT, + database=MYSQL_DB_NAME, + ) + cls._cursor = cls._connection.cursor() + + @classmethod + def tearDownClass(cls): + if cls._connection: + cls._connection.close() + + def setUp(self): + self._span_exporter.clear() + + def validate_spans(self): + spans = self._span_exporter.get_finished_spans() + self.assertEqual(len(spans), 2) + for span in spans: + if span.name == "rootSpan": + root_span = span + else: + db_span = span + self.assertIsInstance(span.start_time, int) + self.assertIsInstance(span.end_time, int) + self.assertIsNotNone(root_span) + self.assertIsNotNone(db_span) + self.assertEqual(root_span.name, "rootSpan") + self.assertEqual(db_span.name, "mysql.opentelemetry-tests") + self.assertIsNotNone(db_span.parent) + self.assertEqual(db_span.parent.name, root_span.name) + self.assertIs(db_span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(db_span.attributes["db.instance"], MYSQL_DB_NAME) + self.assertEqual(db_span.attributes["net.peer.name"], MYSQL_HOST) + self.assertEqual(db_span.attributes["net.peer.port"], MYSQL_PORT) + + def test_execute(self): + """Should create a child span for execute + """ + with self._tracer.start_as_current_span("rootSpan"): + self._cursor.execute("CREATE TABLE IF NOT EXISTS test (id INT)") + self.validate_spans() + + def test_executemany(self): + """Should create a child span for executemany + """ + with self._tracer.start_as_current_span("rootSpan"): + data = ["1", "2", "3"] + stmt = "INSERT INTO test (id) VALUES (%s)" + self._cursor.executemany(stmt, data) + self.validate_spans() + + def test_callproc(self): + """Should create a child span for callproc + """ + with self._tracer.start_as_current_span("rootSpan"), self.assertRaises( + Exception + ): + self._cursor.callproc("test", ()) + self.validate_spans() diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 3da2aad0f01..0b592641ce3 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -41,6 +41,7 @@ package_dir= packages=find_namespace: install_requires = opentelemetry-api == 0.7.dev0 + opentelemetry-ext-dbapi == 0.7.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-pymysql/CHANGELOG.md b/ext/opentelemetry-ext-pymysql/CHANGELOG.md new file mode 100644 index 00000000000..1512c421622 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## Unreleased diff --git a/ext/opentelemetry-ext-pymysql/README.rst b/ext/opentelemetry-ext-pymysql/README.rst new file mode 100644 index 00000000000..3cf845366b9 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry PyMySQL integration +================================= + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymysql.svg + :target: https://pypi.org/project/opentelemetry-ext-pymysql/ + +Integration with PyMySQL that supports the PyMySQL library and is +specified to trace_integration using 'PyMySQL'. + + +Installation +------------ + +:: + + pip install opentelemetry-ext-pymysql + + +References +---------- +* `OpenTelemetry PyMySQL Integration `_ +* `OpenTelemetry Project `_ diff --git a/ext/opentelemetry-ext-pymysql/setup.cfg b/ext/opentelemetry-ext-pymysql/setup.cfg new file mode 100644 index 00000000000..9f44953a402 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/setup.cfg @@ -0,0 +1,48 @@ +# Copyright The 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-pymysql +description = OpenTelemetry PyMySQL 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/tree/master/ext/opentelemetry-ext-pymysql +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + 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 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api == 0.7.dev0 + opentelemetry-ext-dbapi == 0.7.dev0 + PyMySQL ~= 0.9.3 + +[options.packages.find] +where = src diff --git a/ext/opentelemetry-ext-pymysql/setup.py b/ext/opentelemetry-ext-pymysql/setup.py new file mode 100644 index 00000000000..a3f057b310b --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/setup.py @@ -0,0 +1,26 @@ +# Copyright The 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", "pymysql", "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-pymysql/src/opentelemetry/ext/pymysql/__init__.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py new file mode 100644 index 00000000000..0f06578e0bc --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py @@ -0,0 +1,68 @@ +# Copyright The 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 integration with PyMySQL supports the `PyMySQL`_ library and is specified +to ``trace_integration`` using ``'PyMySQL'``. + +.. _PyMySQL: https://pypi.org/project/PyMySQL/ + +Usage +----- + +.. code:: python + + import pymysql + from opentelemetry import trace + from opentelemetry.ext.pymysql import trace_integration + from opentelemetry.sdk.trace import TracerProvider + + trace.set_tracer_provider(TracerProvider()) + trace_integration() + cnx = pymysql.connect(database="MySQL_Database") + cursor = cnx.cursor() + cursor.execute("INSERT INTO test (testField) VALUES (123)" + cnx.commit() + cursor.close() + cnx.close() + +API +--- +""" + +import typing + +import pymysql + +from opentelemetry.ext.dbapi import wrap_connect +from opentelemetry.ext.pymysql.version import __version__ +from opentelemetry.trace import TracerProvider, get_tracer + + +def trace_integration(tracer_provider: typing.Optional[TracerProvider] = None): + """Integrate with the PyMySQL library. + https://github.com/PyMySQL/PyMySQL/ + """ + + tracer = get_tracer(__name__, __version__, tracer_provider) + + connection_attributes = { + "database": "db", + "port": "port", + "host": "host", + "user": "user", + } + wrap_connect( + tracer, pymysql, "connect", "mysql", "sql", connection_attributes + ) diff --git a/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py new file mode 100644 index 00000000000..86c61362ab5 --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/version.py @@ -0,0 +1,15 @@ +# Copyright The 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.7.dev0" diff --git a/ext/opentelemetry-ext-pymysql/tests/__init__.py b/ext/opentelemetry-ext-pymysql/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py new file mode 100644 index 00000000000..c452c31ec6d --- /dev/null +++ b/ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py @@ -0,0 +1,38 @@ +# Copyright The 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. + +from unittest import mock + +import pymysql + +import opentelemetry.ext.pymysql +from opentelemetry.ext.pymysql import trace_integration +from opentelemetry.test.test_base import TestBase + + +class TestPyMysqlIntegration(TestBase): + def test_trace_integration(self): + with mock.patch("pymysql.connect"): + trace_integration() + cnx = pymysql.connect(database="test") + cursor = cnx.cursor() + query = "SELECT * FROM test" + cursor.execute(query) + + spans_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans_list), 1) + span = spans_list[0] + + # Check version and name in span's instrumentation info + self.check_span_instrumentation_info(span, opentelemetry.ext.pymysql) diff --git a/tox.ini b/tox.ini index dc7f20aa8d1..e0c4314bc07 100644 --- a/tox.ini +++ b/tox.ini @@ -64,6 +64,10 @@ envlist = py3{4,5,6,7,8}-test-ext-pymongo pypy3-test-ext-pymongo + ; opentelemetry-ext-pymysql + py3{4,5,6,7,8}-test-ext-pymysql + pypy3-test-ext-pymysql + ; opentelemetry-ext-wsgi py3{4,5,6,7,8}-test-ext-wsgi pypy3-test-ext-wsgi @@ -120,6 +124,7 @@ changedir = test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests test-ext-pymongo: ext/opentelemetry-ext-pymongo/tests test-ext-psycopg2: ext/opentelemetry-ext-psycopg2/tests + test-ext-pymysql: ext/opentelemetry-ext-pymysql/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-ext-zipkin: ext/opentelemetry-ext-zipkin/tests test-ext-flask: ext/opentelemetry-ext-flask/tests @@ -171,6 +176,9 @@ commands_pre = psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2 + pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi + pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql + http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests[test] jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger @@ -232,6 +240,7 @@ deps = thrift pymongo flask + pymysql mysql-connector-python wrapt psycopg2-binary @@ -268,6 +277,7 @@ deps = docker-compose >= 1.25.2 mysql-connector-python ~= 8.0 pymongo ~= 3.1 + pymysql ~= 0.9.3 psycopg2-binary ~= 2.8.4 changedir = @@ -280,7 +290,8 @@ commands_pre = -e {toxinidir}/ext/opentelemetry-ext-dbapi \ -e {toxinidir}/ext/opentelemetry-ext-mysql \ -e {toxinidir}/ext/opentelemetry-ext-psycopg2 \ - -e {toxinidir}/ext/opentelemetry-ext-pymongo + -e {toxinidir}/ext/opentelemetry-ext-pymongo \ + -e {toxinidir}/ext/opentelemetry-ext-pymysql docker-compose up -d python check_availability.py commands =