From 2c2f2ba3ff30d95b83628a5122fe1952f29d3c39 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 4 Nov 2019 10:18:28 -0800 Subject: [PATCH 01/22] Adding MySQL Connector integration --- ext/opentelemetry-ext-mysql/README.rst | 29 ++++ ext/opentelemetry-ext-mysql/setup.cfg | 47 ++++++ ext/opentelemetry-ext-mysql/setup.py | 26 ++++ .../src/opentelemetry/ext/mysql/__init__.py | 110 ++++++++++++++ .../src/opentelemetry/ext/mysql/version.py | 15 ++ ext/opentelemetry-ext-mysql/tests/__init__.py | 0 .../tests/test_mysql_integration.py | 136 ++++++++++++++++++ tox.ini | 9 +- 8 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 ext/opentelemetry-ext-mysql/README.rst create mode 100644 ext/opentelemetry-ext-mysql/setup.cfg create mode 100644 ext/opentelemetry-ext-mysql/setup.py create mode 100644 ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py create mode 100644 ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py create mode 100644 ext/opentelemetry-ext-mysql/tests/__init__.py create mode 100644 ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst new file mode 100644 index 00000000000..f8bd28d0f12 --- /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.trace.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..68a3ca91751 --- /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.1.dev0 + mysql-connector >= 2.1.6, < 3.0.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..b89161ca196 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -0,0 +1,110 @@ +# 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 +import wrapt + +from opentelemetry.context import Context +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCanonicalCode + +DATABASE_COMPONENT = "mysql" +DATABASE_TYPE = "sql" + + +def trace_integration(tracer): + """Integrate with MySQL Connector/Python library. + https://dev.mysql.com/doc/connector-python/en/ + """ + + # pylint: disable=unused-argument + def wrap(wrapped, instance, args, kwargs): + """Patch MySQL Connector connect method to add tracing. + """ + mysql_tracer = MySqlTracer(tracer) + return mysql_tracer.wrap_connect(wrapped, args, kwargs) + + wrapt.wrap_function_wrapper(mysql.connector, "connect", wrap) + + +class MySqlTracer: + def __init__(self, tracer): + if tracer is None: + raise ValueError("The tracer is not provided.") + self._tracer = tracer + self._connection_props = {} + + def wrap_connect(self, wrapped, args, kwargs): + """Patch connect method to add tracing. + """ + connection = wrapped(*args, **kwargs) + self._connection_props = { + "database": connection.database, + "port": connection.server_port, + "host": connection.server_host, + "user": connection.user, + } + + wrapt.wrap_function_wrapper(connection, "cursor", self.wrap_cursor) + return connection + + # pylint: disable=unused-argument + def wrap_cursor(self, wrapped, instance, args, kwargs): + """Patch cursor instance in a specific connection. + """ + cursor = wrapped(*args, **kwargs) + wrapt.wrap_function_wrapper(cursor, "execute", self.wrap_execute) + return cursor + + # pylint: disable=unused-argument + def wrap_execute(self, wrapped, instance, args, kwargs): + """Patch execute method in cursor and create span. + """ + name = DATABASE_COMPONENT + database = self._connection_props.get("database", "") + if database: + name += "." + database + query = args[0] if args else "" + # Query with parameters + if len(args) > 1: + query += " params=" + str(args[1]) + + with self._tracer.start_current_span( + name, kind=SpanKind.CLIENT + ) as span: + span.set_attribute("component", DATABASE_COMPONENT) + span.set_attribute("db.type", DATABASE_TYPE) + span.set_attribute("db.instance", database) + span.set_attribute("db.statement", query) + span.set_attribute( + "db.user", self._connection_props.get("user", "") + ) + span.set_attribute( + "peer.hostname", self._connection_props.get("host", "") + ) + port = self._connection_props.get("port") + if port is not None: + span.set_attribute("peer.port", port) + + try: + result = wrapped(*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))) 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..a457c2b6651 --- /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.1.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..fcd8258d1d1 --- /dev/null +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -0,0 +1,136 @@ +# 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.mysql import MySqlTracer +from opentelemetry.util import time_ns + + +class TestMysqlIntegration(unittest.TestCase): + def test_span_succeeded(self): + mock_tracer = MockTracer() + connection_props = { + "database": "testdatabase", + "server_host": "testhost", + "server_port": 123, + "user": "testuser", + } + mysql_tracer = MySqlTracer(mock_tracer) + mock_connection = mysql_tracer.wrap_connect( + mock_connect, {}, connection_props + ) + cursor = mock_connection.cursor() + cursor.execute( + "Test query", {"param1": "param1Value", "param2": "param2Value"} + ) + span = mock_tracer.get_current_span() + + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(span.name, "mysql.testdatabase") + self.assertEqual(span.attributes["component"], "mysql") + self.assertEqual(span.attributes["db.type"], "sql") + self.assertEqual(span.attributes["db.instance"], "testdatabase") + self.assertEqual( + span.attributes["db.statement"], + "Test query params={'param1': 'param1Value', 'param2': 'param2Value'}", + ) + self.assertEqual(span.attributes["db.user"], "testuser") + self.assertEqual(span.attributes["peer.hostname"], "testhost") + self.assertEqual(span.attributes["peer.port"], 123) + self.assertIs( + span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + ) + + def test_span_failed(self): + mock_tracer = MockTracer() + mysql_tracer = MySqlTracer(mock_tracer) + mock_connection = mysql_tracer.wrap_connect(mock_connect, {}, {}) + cursor = mock_connection.cursor() + cursor.execute("Test query", throw_exception=True) + span = mock_tracer.get_current_span() + + self.assertEqual(span.attributes["db.statement"], "Test query") + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual(span.status.description, "Test Exception") + + +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 MockMySqlConnection(database, server_port, server_host, user) + + +class MockMySqlConnection: + 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 + + def cursor(self): + return MockMySqlCursor() + + +class MockMySqlCursor: + def execute(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 = None + self.end_time = None + + def set_attribute(self, key, value): + self.attributes[key] = value + + def set_status(self, status): + self.status = status + + def end(self, end_time=None): + self.end_time = end_time if end_time is not None else time_ns() + + +class MockTracer: + def __init__(self): + self.span = MockSpan() + self.end_span = mock.Mock() + self.span.attributes = {} + self.span.status = None + + def start_current_span(self, name, kind): + self.span.name = name + self.span.kind = kind + return self.span + + def get_current_span(self): + return self.span diff --git a/tox.ini b/tox.ini index e30cb1a14b7..d26fea17f8a 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-mysql,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-mysql,opentracing-shim} lint py37-tracecontext py37-{mypy,mypyinstalled} @@ -25,6 +25,7 @@ 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-mysql: ext/opentelemetry-ext-mysql/tests test-ext-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests test-opentracing-shim: ext/opentelemetry-ext-opentracing-shim/tests @@ -40,6 +41,7 @@ commands_pre = example-app: pip install {toxinidir}/examples/opentelemetry-example-app ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk jaeger: pip install {toxinidir}/ext/opentelemetry-ext-jaeger @@ -75,6 +77,7 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger + pip install -e {toxinidir}/ext/opentelemetry-ext-mysql pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/examples/opentelemetry-example-app pip install -e {toxinidir}/ext/opentelemetry-ext-opentracing-shim @@ -96,6 +99,8 @@ commands = ext/opentelemetry-ext-jaeger/tests/ \ ext/opentelemetry-ext-opentracing-shim/src/ \ ext/opentelemetry-ext-opentracing-shim/tests/ \ + ext/opentelemetry-ext-mysql/src/opentelemetry \ + ext/opentelemetry-ext-mysql/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ examples/opentelemetry-example-app/src/opentelemetry_example_app/ \ examples/opentelemetry-example-app/tests/ From 47824f2f6ccf151aa5daa10014cb93168edbd372 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 4 Nov 2019 10:59:04 -0800 Subject: [PATCH 02/22] Fix lint issues --- ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index fcd8258d1d1..6e762df1284 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -71,6 +71,7 @@ def test_span_failed(self): self.assertEqual(span.status.description, "Test Exception") +# pylint: disable=unused-argument def mock_connect(*args, **kwargs): database = kwargs.get("database") server_host = kwargs.get("server_host") @@ -86,11 +87,13 @@ def __init__(self, database, server_port, server_host, user): self.server_host = server_host self.user = user + # pylint: disable=no-self-use def cursor(self): return MockMySqlCursor() class MockMySqlCursor: + # pylint: disable=unused-argument, no-self-use def execute(self, query, params=None, throw_exception=False): if throw_exception: raise Exception("Test Exception") From b5d81b89e755246fbbec93a5bbeb61771f2a4a0d Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Tue, 5 Nov 2019 12:07:50 -0800 Subject: [PATCH 03/22] Addressing comments --- ext/opentelemetry-ext-mysql/setup.cfg | 4 +- .../src/opentelemetry/ext/mysql/__init__.py | 38 +++++++++++++++---- .../src/opentelemetry/ext/mysql/version.py | 2 +- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 68a3ca91751..a4038cc0b25 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -39,8 +39,8 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.1.dev0 - mysql-connector >= 2.1.6, < 3.0.0 + opentelemetry-api >= 0.3.dev0 + mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index b89161ca196..5182fd070e8 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -17,24 +17,31 @@ MySQL Connector/Python library. """ +import typing + import mysql.connector import wrapt from opentelemetry.context import Context -from opentelemetry.trace import SpanKind +from opentelemetry.trace import SpanKind, Tracer from opentelemetry.trace.status import Status, StatusCanonicalCode DATABASE_COMPONENT = "mysql" DATABASE_TYPE = "sql" -def trace_integration(tracer): +def trace_integration(tracer: Tracer): """Integrate with MySQL Connector/Python library. https://dev.mysql.com/doc/connector-python/en/ """ # pylint: disable=unused-argument - def wrap(wrapped, instance, args, kwargs): + def wrap( + wrapped: typing.Callable(any), + instance: typing.Any, + args: typing.Tuple(any), + kwargs: typing.Dict(any), + ): """Patch MySQL Connector connect method to add tracing. """ mysql_tracer = MySqlTracer(tracer) @@ -44,13 +51,18 @@ def wrap(wrapped, instance, args, kwargs): class MySqlTracer: - def __init__(self, tracer): + def __init__(self, tracer: Tracer): if tracer is None: raise ValueError("The tracer is not provided.") self._tracer = tracer self._connection_props = {} - def wrap_connect(self, wrapped, args, kwargs): + def wrap_connect( + self, + wrapped: typing.Callable(any), + args: typing.Tuple(any), + kwargs: typing.Dict(any), + ): """Patch connect method to add tracing. """ connection = wrapped(*args, **kwargs) @@ -65,7 +77,13 @@ def wrap_connect(self, wrapped, args, kwargs): return connection # pylint: disable=unused-argument - def wrap_cursor(self, wrapped, instance, args, kwargs): + def wrap_cursor( + self, + wrapped: typing.Callable(any), + instance: typing.Any, + args: typing.Tuple(any), + kwargs: typing.Dict(any), + ): """Patch cursor instance in a specific connection. """ cursor = wrapped(*args, **kwargs) @@ -73,7 +91,13 @@ def wrap_cursor(self, wrapped, instance, args, kwargs): return cursor # pylint: disable=unused-argument - def wrap_execute(self, wrapped, instance, args, kwargs): + def wrap_execute( + self, + wrapped: typing.Callable(any), + instance: typing.Any, + args: typing.Tuple(any), + kwargs: typing.Dict(any), + ): """Patch execute method in cursor and create span. """ name = DATABASE_COMPONENT diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index a457c2b6651..93ef792d051 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.1.dev0" +__version__ = "0.3.dev0" From f9f03a87b8cfeca5ac40fb16a0204541193919a7 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Tue, 5 Nov 2019 12:31:45 -0800 Subject: [PATCH 04/22] Fixing issue with typings --- .../src/opentelemetry/ext/mysql/__init__.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index 5182fd070e8..aec3a809736 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -37,10 +37,10 @@ def trace_integration(tracer: Tracer): # pylint: disable=unused-argument def wrap( - wrapped: typing.Callable(any), + wrapped: typing.Callable[..., any], instance: typing.Any, - args: typing.Tuple(any), - kwargs: typing.Dict(any), + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], ): """Patch MySQL Connector connect method to add tracing. """ @@ -59,9 +59,9 @@ def __init__(self, tracer: Tracer): def wrap_connect( self, - wrapped: typing.Callable(any), - args: typing.Tuple(any), - kwargs: typing.Dict(any), + wrapped: typing.Callable[..., any], + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], ): """Patch connect method to add tracing. """ @@ -79,10 +79,10 @@ def wrap_connect( # pylint: disable=unused-argument def wrap_cursor( self, - wrapped: typing.Callable(any), + wrapped: typing.Callable[..., any], instance: typing.Any, - args: typing.Tuple(any), - kwargs: typing.Dict(any), + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], ): """Patch cursor instance in a specific connection. """ @@ -93,10 +93,10 @@ def wrap_cursor( # pylint: disable=unused-argument def wrap_execute( self, - wrapped: typing.Callable(any), + wrapped: typing.Callable[..., any], instance: typing.Any, - args: typing.Tuple(any), - kwargs: typing.Dict(any), + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], ): """Patch execute method in cursor and create span. """ From 10f3f5324ff08edd95d8e1b7358a9db6405ccf9c Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 6 Nov 2019 11:29:09 -0800 Subject: [PATCH 05/22] Using json to store query params --- .../src/opentelemetry/ext/mysql/__init__.py | 3 ++- ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index aec3a809736..eb314115666 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -17,6 +17,7 @@ MySQL Connector/Python library. """ +import json import typing import mysql.connector @@ -107,7 +108,7 @@ def wrap_execute( query = args[0] if args else "" # Query with parameters if len(args) > 1: - query += " params=" + str(args[1]) + query += " params=" + json.dumps(args[1]) with self._tracer.start_current_span( name, kind=SpanKind.CLIENT diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 6e762df1284..888cf913e72 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -46,7 +46,7 @@ def test_span_succeeded(self): self.assertEqual(span.attributes["db.instance"], "testdatabase") self.assertEqual( span.attributes["db.statement"], - "Test query params={'param1': 'param1Value', 'param2': 'param2Value'}", + 'Test query params={"param1": "param1Value", "param2": "param2Value"}', ) self.assertEqual(span.attributes["db.user"], "testuser") self.assertEqual(span.attributes["peer.hostname"], "testhost") From e6158c8c86d1609867b1984213f7f4fd11677b23 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 6 Nov 2019 14:31:11 -0800 Subject: [PATCH 06/22] Using tuple query params in tests instead of dict --- .../src/opentelemetry/ext/mysql/__init__.py | 3 +-- ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index eb314115666..aec3a809736 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -17,7 +17,6 @@ MySQL Connector/Python library. """ -import json import typing import mysql.connector @@ -108,7 +107,7 @@ def wrap_execute( query = args[0] if args else "" # Query with parameters if len(args) > 1: - query += " params=" + json.dumps(args[1]) + query += " params=" + str(args[1]) with self._tracer.start_current_span( name, kind=SpanKind.CLIENT diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 888cf913e72..881dc91a103 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -35,7 +35,7 @@ def test_span_succeeded(self): ) cursor = mock_connection.cursor() cursor.execute( - "Test query", {"param1": "param1Value", "param2": "param2Value"} + "Test query", ("param1Value", False) ) span = mock_tracer.get_current_span() @@ -46,7 +46,7 @@ def test_span_succeeded(self): self.assertEqual(span.attributes["db.instance"], "testdatabase") self.assertEqual( span.attributes["db.statement"], - 'Test query params={"param1": "param1Value", "param2": "param2Value"}', + "Test query params=('param1Value', False)", ) self.assertEqual(span.attributes["db.user"], "testuser") self.assertEqual(span.attributes["peer.hostname"], "testhost") From 315e5839cb7dbd10afbfdfa6e84d040c28f43fa8 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 6 Nov 2019 14:38:21 -0800 Subject: [PATCH 07/22] Format issue --- ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 881dc91a103..971438c1751 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -34,9 +34,7 @@ def test_span_succeeded(self): mock_connect, {}, connection_props ) cursor = mock_connection.cursor() - cursor.execute( - "Test query", ("param1Value", False) - ) + cursor.execute("Test query", ("param1Value", False)) span = mock_tracer.get_current_span() self.assertIs(span.kind, trace_api.SpanKind.CLIENT) From cb8a8356944e1adbe0f57acd3d313796e987e85c Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 11 Nov 2019 15:06:12 -0800 Subject: [PATCH 08/22] Adding ext.dbapi package --- ext/opentelemetry-ext-dbapi/README.rst | 29 ++++ ext/opentelemetry-ext-dbapi/setup.cfg | 46 ++++++ ext/opentelemetry-ext-dbapi/setup.py | 26 ++++ .../src/opentelemetry/ext/dbapi/__init__.py | 114 ++++++++++++++ .../src/opentelemetry/ext/dbapi/version.py | 15 ++ ext/opentelemetry-ext-dbapi/tests/__init__.py | 0 .../tests/test_dbapi_integration.py | 139 ++++++++++++++++++ .../src/opentelemetry/ext/mysql/__init__.py | 94 +----------- .../tests/test_mysql_integration.py | 122 +-------------- tox.ini | 10 +- 10 files changed, 385 insertions(+), 210 deletions(-) create mode 100644 ext/opentelemetry-ext-dbapi/README.rst create mode 100644 ext/opentelemetry-ext-dbapi/setup.cfg create mode 100644 ext/opentelemetry-ext-dbapi/setup.py create mode 100644 ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py create mode 100644 ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py create mode 100644 ext/opentelemetry-ext-dbapi/tests/__init__.py create mode 100644 ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst new file mode 100644 index 00000000000..f8bd28d0f12 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/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.trace.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-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg new file mode 100644 index 00000000000..259174d49bb --- /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 DatabaseAPI 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.3.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..29e1d7105df --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -0,0 +1,114 @@ +# 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 typing + +import wrapt + +from opentelemetry.trace import SpanKind, Tracer +from opentelemetry.trace.status import Status, StatusCanonicalCode + + +class DatabaseApiTracer: + def __init__( + self, tracer: Tracer, database_component: str, database_type="sql" + ): + if tracer is None: + raise ValueError("The tracer is not provided.") + self._tracer = tracer + self._database_component = database_component + self._database_type = database_type + self._connection_props = {} + + def wrap_connect( + self, + wrapped: typing.Callable[..., any], + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], + ): + """Patch connect method to add tracing. + """ + connection = wrapped(*args, **kwargs) + self._connection_props = { + "database": connection.database, + "port": connection.server_port, + "host": connection.server_host, + "user": connection.user, + } + + wrapt.wrap_function_wrapper(connection, "cursor", self.wrap_cursor) + return connection + + # pylint: disable=unused-argument + def wrap_cursor( + self, + wrapped: typing.Callable[..., any], + instance: typing.Any, + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], + ): + """Patch cursor instance in a specific connection. + """ + cursor = wrapped(*args, **kwargs) + wrapt.wrap_function_wrapper(cursor, "execute", self.wrap_execute) + return cursor + + # pylint: disable=unused-argument + def wrap_execute( + self, + wrapped: typing.Callable[..., any], + instance: typing.Any, + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], + ): + """Patch execute method in cursor and create span. + """ + name = self._database_component + database = self._connection_props.get("database", "") + if database: + name += "." + database + query = args[0] if args else "" + # Query with parameters + if len(args) > 1: + query += " params=" + str(args[1]) + + with self._tracer.start_current_span( + name, kind=SpanKind.CLIENT + ) as span: + span.set_attribute("component", self._database_component) + span.set_attribute("db.type", self._database_type) + span.set_attribute("db.instance", database) + span.set_attribute("db.statement", query) + span.set_attribute( + "db.user", self._connection_props.get("user", "") + ) + span.set_attribute( + "peer.hostname", self._connection_props.get("host", "") + ) + port = self._connection_props.get("port") + if port is not None: + span.set_attribute("peer.port", port) + + try: + result = wrapped(*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))) 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..93ef792d051 --- /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.3.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..651f817b664 --- /dev/null +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -0,0 +1,139 @@ +# 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 DatabaseApiTracer +from opentelemetry.util import time_ns + + +class TestMysqlIntegration(unittest.TestCase): + def test_span_succeeded(self): + mock_tracer = MockTracer() + connection_props = { + "database": "testdatabase", + "server_host": "testhost", + "server_port": 123, + "user": "testuser", + } + mysql_tracer = DatabaseApiTracer( + mock_tracer, "testcomponent", "testtype" + ) + mock_connection = mysql_tracer.wrap_connect( + mock_connect, {}, connection_props + ) + cursor = mock_connection.cursor() + cursor.execute("Test query", ("param1Value", False)) + span = mock_tracer.get_current_span() + + self.assertIs(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(span.name, "testcomponent.testdatabase") + self.assertEqual(span.attributes["component"], "testcomponent") + self.assertEqual(span.attributes["db.type"], "testtype") + self.assertEqual(span.attributes["db.instance"], "testdatabase") + self.assertEqual( + span.attributes["db.statement"], + "Test query params=('param1Value', False)", + ) + self.assertEqual(span.attributes["db.user"], "testuser") + self.assertEqual(span.attributes["peer.hostname"], "testhost") + self.assertEqual(span.attributes["peer.port"], 123) + self.assertIs( + span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + ) + + def test_span_failed(self): + mock_tracer = MockTracer() + mysql_tracer = DatabaseApiTracer(mock_tracer, "testcomponent") + mock_connection = mysql_tracer.wrap_connect(mock_connect, {}, {}) + cursor = mock_connection.cursor() + cursor.execute("Test query", throw_exception=True) + span = mock_tracer.get_current_span() + + self.assertEqual(span.attributes["db.statement"], "Test query") + self.assertIs( + span.status.canonical_code, + trace_api.status.StatusCanonicalCode.UNKNOWN, + ) + self.assertEqual(span.status.description, "Test Exception") + + +# 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 MockMySqlConnection(database, server_port, server_host, user) + + +class MockMySqlConnection: + 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 MockMySqlCursor() + + +class MockMySqlCursor: + # pylint: disable=unused-argument, no-self-use + def execute(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 = None + self.end_time = None + + def set_attribute(self, key, value): + self.attributes[key] = value + + def set_status(self, status): + self.status = status + + def end(self, end_time=None): + self.end_time = end_time if end_time is not None else time_ns() + + +class MockTracer: + def __init__(self): + self.span = MockSpan() + self.end_span = mock.Mock() + self.span.attributes = {} + self.span.status = None + + def start_current_span(self, name, kind): + self.span.name = name + self.span.kind = kind + return self.span + + def get_current_span(self): + return self.span diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index aec3a809736..2cae5645807 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -22,12 +22,8 @@ import mysql.connector import wrapt -from opentelemetry.context import Context -from opentelemetry.trace import SpanKind, Tracer -from opentelemetry.trace.status import Status, StatusCanonicalCode - -DATABASE_COMPONENT = "mysql" -DATABASE_TYPE = "sql" +from opentelemetry.ext.dbapi import DatabaseApiTracer +from opentelemetry.trace import Tracer def trace_integration(tracer: Tracer): @@ -44,91 +40,7 @@ def wrap( ): """Patch MySQL Connector connect method to add tracing. """ - mysql_tracer = MySqlTracer(tracer) + mysql_tracer = DatabaseApiTracer(tracer, "mysql") return mysql_tracer.wrap_connect(wrapped, args, kwargs) wrapt.wrap_function_wrapper(mysql.connector, "connect", wrap) - - -class MySqlTracer: - def __init__(self, tracer: Tracer): - if tracer is None: - raise ValueError("The tracer is not provided.") - self._tracer = tracer - self._connection_props = {} - - def wrap_connect( - self, - wrapped: typing.Callable[..., any], - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], - ): - """Patch connect method to add tracing. - """ - connection = wrapped(*args, **kwargs) - self._connection_props = { - "database": connection.database, - "port": connection.server_port, - "host": connection.server_host, - "user": connection.user, - } - - wrapt.wrap_function_wrapper(connection, "cursor", self.wrap_cursor) - return connection - - # pylint: disable=unused-argument - def wrap_cursor( - self, - wrapped: typing.Callable[..., any], - instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], - ): - """Patch cursor instance in a specific connection. - """ - cursor = wrapped(*args, **kwargs) - wrapt.wrap_function_wrapper(cursor, "execute", self.wrap_execute) - return cursor - - # pylint: disable=unused-argument - def wrap_execute( - self, - wrapped: typing.Callable[..., any], - instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], - ): - """Patch execute method in cursor and create span. - """ - name = DATABASE_COMPONENT - database = self._connection_props.get("database", "") - if database: - name += "." + database - query = args[0] if args else "" - # Query with parameters - if len(args) > 1: - query += " params=" + str(args[1]) - - with self._tracer.start_current_span( - name, kind=SpanKind.CLIENT - ) as span: - span.set_attribute("component", DATABASE_COMPONENT) - span.set_attribute("db.type", DATABASE_TYPE) - span.set_attribute("db.instance", database) - span.set_attribute("db.statement", query) - span.set_attribute( - "db.user", self._connection_props.get("user", "") - ) - span.set_attribute( - "peer.hostname", self._connection_props.get("host", "") - ) - port = self._connection_props.get("port") - if port is not None: - span.set_attribute("peer.port", port) - - try: - result = wrapped(*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))) diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 971438c1751..72438bf1823 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -13,125 +13,13 @@ # limitations under the License. import unittest -from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.ext.mysql import MySqlTracer -from opentelemetry.util import time_ns +from opentelemetry.ext.mysql import trace_integration class TestMysqlIntegration(unittest.TestCase): - def test_span_succeeded(self): - mock_tracer = MockTracer() - connection_props = { - "database": "testdatabase", - "server_host": "testhost", - "server_port": 123, - "user": "testuser", - } - mysql_tracer = MySqlTracer(mock_tracer) - mock_connection = mysql_tracer.wrap_connect( - mock_connect, {}, connection_props - ) - cursor = mock_connection.cursor() - cursor.execute("Test query", ("param1Value", False)) - span = mock_tracer.get_current_span() - - self.assertIs(span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(span.name, "mysql.testdatabase") - self.assertEqual(span.attributes["component"], "mysql") - self.assertEqual(span.attributes["db.type"], "sql") - self.assertEqual(span.attributes["db.instance"], "testdatabase") - self.assertEqual( - span.attributes["db.statement"], - "Test query params=('param1Value', False)", - ) - self.assertEqual(span.attributes["db.user"], "testuser") - self.assertEqual(span.attributes["peer.hostname"], "testhost") - self.assertEqual(span.attributes["peer.port"], 123) - self.assertIs( - span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK - ) - - def test_span_failed(self): - mock_tracer = MockTracer() - mysql_tracer = MySqlTracer(mock_tracer) - mock_connection = mysql_tracer.wrap_connect(mock_connect, {}, {}) - cursor = mock_connection.cursor() - cursor.execute("Test query", throw_exception=True) - span = mock_tracer.get_current_span() - - self.assertEqual(span.attributes["db.statement"], "Test query") - self.assertIs( - span.status.canonical_code, - trace_api.status.StatusCanonicalCode.UNKNOWN, - ) - self.assertEqual(span.status.description, "Test Exception") - - -# 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 MockMySqlConnection(database, server_port, server_host, user) - - -class MockMySqlConnection: - 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 MockMySqlCursor() - - -class MockMySqlCursor: - # pylint: disable=unused-argument, no-self-use - def execute(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 = None - self.end_time = None - - def set_attribute(self, key, value): - self.attributes[key] = value - - def set_status(self, status): - self.status = status - - def end(self, end_time=None): - self.end_time = end_time if end_time is not None else time_ns() - - -class MockTracer: - def __init__(self): - self.span = MockSpan() - self.end_span = mock.Mock() - self.span.attributes = {} - self.span.status = None - - def start_current_span(self, name, kind): - self.span.name = name - self.span.kind = kind - return self.span - - def get_current_span(self): - return self.span + def test_trace_integration(self): + tracer = trace_api.tracer() + trace_integration(tracer) + self.assertEqual("", "") diff --git a/tox.ini b/tox.ini index d26fea17f8a..0d90c9cb5f5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,8 @@ skipsdist = True skip_missing_interpreters = True envlist = - py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-mysql,opentracing-shim} - pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-mysql,opentracing-shim} + py3{4,5,6,7,8}-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,opentracing-shim} + pypy3-test-{api,sdk,example-app,ext-wsgi,ext-http-requests,ext-jaeger,ext-dbapi,ext-mysql,opentracing-shim} lint py37-tracecontext py37-{mypy,mypyinstalled} @@ -25,6 +25,7 @@ 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-wsgi: ext/opentelemetry-ext-wsgi/tests test-example-app: examples/opentelemetry-example-app/tests @@ -41,6 +42,8 @@ commands_pre = example-app: pip install {toxinidir}/examples/opentelemetry-example-app ext: pip install {toxinidir}/opentelemetry-api wsgi: pip install {toxinidir}/ext/opentelemetry-ext-wsgi + dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi + mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql http-requests: pip install {toxinidir}/ext/opentelemetry-ext-http-requests jaeger: pip install {toxinidir}/opentelemetry-sdk @@ -77,6 +80,7 @@ commands_pre = pip install -e {toxinidir}/ext/opentelemetry-ext-azure-monitor pip install -e {toxinidir}/ext/opentelemetry-ext-http-requests pip install -e {toxinidir}/ext/opentelemetry-ext-jaeger + pip install -e {toxinidir}/ext/opentelemetry-ext-dbapi pip install -e {toxinidir}/ext/opentelemetry-ext-mysql pip install -e {toxinidir}/ext/opentelemetry-ext-wsgi pip install -e {toxinidir}/examples/opentelemetry-example-app @@ -99,6 +103,8 @@ commands = ext/opentelemetry-ext-jaeger/tests/ \ ext/opentelemetry-ext-opentracing-shim/src/ \ ext/opentelemetry-ext-opentracing-shim/tests/ \ + ext/opentelemetry-ext-dbapi/src/opentelemetry \ + ext/opentelemetry-ext-dbapi/tests/ \ ext/opentelemetry-ext-mysql/src/opentelemetry \ ext/opentelemetry-ext-mysql/tests/ \ ext/opentelemetry-ext-wsgi/tests/ \ From 857eed60a87ff8fc82499e39dda76fdc9b10f87d Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 11 Nov 2019 15:13:02 -0800 Subject: [PATCH 09/22] Updating readme --- ext/opentelemetry-ext-dbapi/README.rst | 32 +++++++++++++++----------- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index f8bd28d0f12..46c7ac61c8b 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -1,26 +1,32 @@ -OpenTelemetry MySQL integration +OpenTelemetry Database API integration ================================= -The integration with MySQL supports the `mysql-connector`_ library and is specified -to ``trace_integration`` using ``'MySQL'``. +The trace integration with Database API supports libraries following the specification. -.. mysql-connector: https://pypi.org/project/mysql-connector/ +.. PEP 249 -- Python Database API Specification v2.0: https://www.python.org/dev/peps/pep-0249/ Usage ----- .. code:: python - import mysql.connector + import wrapt from opentelemetry.trace import tracer - from opentelemetry.trace.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() + from opentelemetry.trace.ext.dbapi import DatabaseApiTracer + + def wrap( + wrapped: typing.Callable[..., any], + instance: typing.Any, + args: typing.Tuple[any, any], + kwargs: typing.Dict[any, any], + ): + """Patch MySQL Connector connect method to add tracing. + """ + mysql_tracer = DatabaseApiTracer(tracer, "mysql") + return mysql_tracer.wrap_connect(wrapped, args, kwargs) + + # Ex: mysql.connector + wrapt.wrap_function_wrapper(mysql.connector, "connect", wrap) References diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 259174d49bb..15a6fb8352e 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -14,7 +14,7 @@ # [metadata] name = opentelemetry-ext-dbapi -description = OpenTelemetry DatabaseAPI integration +description = OpenTelemetry Database API integration long_description = file: README.rst long_description_content_type = text/x-rst author = OpenTelemetry Authors From aefc210cce2a2ed2ecaae4ea9e9acff1c7189b83 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 13 Nov 2019 17:26:42 -0800 Subject: [PATCH 10/22] Adding more unit tests Adding executemany and callproc wrap --- .../src/opentelemetry/ext/dbapi/__init__.py | 11 +- .../tests/test_dbapi_integration.py | 110 +++++++++++------- .../tests/test_mysql_integration.py | 23 +++- 3 files changed, 96 insertions(+), 48 deletions(-) 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 29e1d7105df..92b1416bd05 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -25,6 +25,8 @@ from opentelemetry.trace import SpanKind, Tracer from opentelemetry.trace.status import Status, StatusCanonicalCode +QUERY_WRAP_METHODS = ["execute", "executemany", "callproc"] + class DatabaseApiTracer: def __init__( @@ -67,18 +69,19 @@ def wrap_cursor( """Patch cursor instance in a specific connection. """ cursor = wrapped(*args, **kwargs) - wrapt.wrap_function_wrapper(cursor, "execute", self.wrap_execute) + for func in QUERY_WRAP_METHODS: + wrapt.wrap_function_wrapper(cursor, func, self.add_span) return cursor # pylint: disable=unused-argument - def wrap_execute( + def add_span( self, wrapped: typing.Callable[..., any], instance: typing.Any, args: typing.Tuple[any, any], kwargs: typing.Dict[any, any], ): - """Patch execute method in cursor and create span. + """Patch execute, executeMany and callproc methods in cursor and create span. """ name = self._database_component database = self._connection_props.get("database", "") @@ -89,7 +92,7 @@ def wrap_execute( if len(args) > 1: query += " params=" + str(args[1]) - with self._tracer.start_current_span( + with self._tracer.start_as_current_span( name, kind=SpanKind.CLIENT ) as span: span.set_attribute("component", self._database_component) diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 651f817b664..23e4cd9acac 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -17,12 +17,26 @@ from opentelemetry import trace as trace_api from opentelemetry.ext.dbapi import DatabaseApiTracer -from opentelemetry.util import time_ns class TestMysqlIntegration(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): - mock_tracer = MockTracer() connection_props = { "database": "testdatabase", "server_host": "testhost", @@ -30,45 +44,67 @@ def test_span_succeeded(self): "user": "testuser", } mysql_tracer = DatabaseApiTracer( - mock_tracer, "testcomponent", "testtype" + self.tracer, "testcomponent", "testtype" ) mock_connection = mysql_tracer.wrap_connect( mock_connect, {}, connection_props ) cursor = mock_connection.cursor() cursor.execute("Test query", ("param1Value", False)) - span = mock_tracer.get_current_span() - - self.assertIs(span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(span.name, "testcomponent.testdatabase") - self.assertEqual(span.attributes["component"], "testcomponent") - self.assertEqual(span.attributes["db.type"], "testtype") - self.assertEqual(span.attributes["db.instance"], "testdatabase") + self.assertTrue(self.start_as_current_span.called) self.assertEqual( - span.attributes["db.statement"], + 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 params=('param1Value', False)", ) - self.assertEqual(span.attributes["db.user"], "testuser") - self.assertEqual(span.attributes["peer.hostname"], "testhost") - self.assertEqual(span.attributes["peer.port"], 123) + self.assertEqual(self.span.attributes["db.user"], "testuser") + self.assertEqual(self.span.attributes["peer.hostname"], "testhost") + self.assertEqual(self.span.attributes["peer.port"], 123) self.assertIs( - span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK + self.span.status.canonical_code, + trace_api.status.StatusCanonicalCode.OK, ) def test_span_failed(self): - mock_tracer = MockTracer() - mysql_tracer = DatabaseApiTracer(mock_tracer, "testcomponent") + mysql_tracer = DatabaseApiTracer(self.tracer, "testcomponent") mock_connection = mysql_tracer.wrap_connect(mock_connect, {}, {}) cursor = mock_connection.cursor() cursor.execute("Test query", throw_exception=True) - span = mock_tracer.get_current_span() - self.assertEqual(span.attributes["db.statement"], "Test query") + self.assertEqual(self.span.attributes["db.statement"], "Test query") self.assertIs( - span.status.canonical_code, + self.span.status.canonical_code, trace_api.status.StatusCanonicalCode.UNKNOWN, ) - self.assertEqual(span.status.description, "Test Exception") + self.assertEqual(self.span.status.description, "Test Exception") + + def test_executemany(self): + mysql_tracer = DatabaseApiTracer(self.tracer, "testcomponent") + mock_connection = mysql_tracer.wrap_connect(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): + mysql_tracer = DatabaseApiTracer(self.tracer, "testcomponent") + mock_connection = mysql_tracer.wrap_connect(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 @@ -98,6 +134,16 @@ 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): @@ -110,30 +156,10 @@ def __init__(self): self.status = None self.name = "" self.kind = trace_api.SpanKind.INTERNAL - self.attributes = None - self.end_time = None + self.attributes = {} def set_attribute(self, key, value): self.attributes[key] = value def set_status(self, status): self.status = status - - def end(self, end_time=None): - self.end_time = end_time if end_time is not None else time_ns() - - -class MockTracer: - def __init__(self): - self.span = MockSpan() - self.end_span = mock.Mock() - self.span.attributes = {} - self.span.status = None - - def start_current_span(self, name, kind): - self.span.name = name - self.span.kind = kind - return self.span - - def get_current_span(self): - return self.span diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 72438bf1823..2ba8cefeb36 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -13,6 +13,9 @@ # 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 @@ -21,5 +24,21 @@ class TestMysqlIntegration(unittest.TestCase): def test_trace_integration(self): tracer = trace_api.tracer() - trace_integration(tracer) - self.assertEqual("", "") + 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) From 9cfc8082addcdfb130e51555d89213a92c45c964 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 20 Nov 2019 12:46:12 -0800 Subject: [PATCH 11/22] Addressing comments --- ext/opentelemetry-ext-dbapi/README.rst | 17 ++---- .../src/opentelemetry/ext/dbapi/__init__.py | 39 +++++++++++--- .../tests/test_dbapi_integration.py | 53 +++++++++++-------- ext/opentelemetry-ext-mysql/README.rst | 2 +- .../src/opentelemetry/ext/mysql/__init__.py | 20 +------ 5 files changed, 69 insertions(+), 62 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index 46c7ac61c8b..3db7e66de30 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -11,22 +11,13 @@ Usage .. code:: python import wrapt + import mysql.connector from opentelemetry.trace import tracer - from opentelemetry.trace.ext.dbapi import DatabaseApiTracer - - def wrap( - wrapped: typing.Callable[..., any], - instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], - ): - """Patch MySQL Connector connect method to add tracing. - """ - mysql_tracer = DatabaseApiTracer(tracer, "mysql") - return mysql_tracer.wrap_connect(wrapped, args, kwargs) + from opentelemetry.ext.dbapi import trace_integration + # Ex: mysql.connector - wrapt.wrap_function_wrapper(mysql.connector, "connect", wrap) + trace_integration(tracer(), mysql.connector, "connect", "mysql") References 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 92b1416bd05..dd0ce3c1fe3 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -28,7 +28,31 @@ QUERY_WRAP_METHODS = ["execute", "executemany", "callproc"] -class DatabaseApiTracer: +def trace_integration( + tracer: Tracer, + connect_module: typing.Callable[..., any], + connect_method_name: str, + database_component: str, +): + """Integrate with DB API library. + https://www.python.org/dev/peps/pep-0249/ + """ + # 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) + return db_integration.wrapped_connection(wrapped, args, kwargs) + + wrapt.wrap_function_wrapper( + connect_module, connect_method_name, wrap_connect + ) + + +class DatabaseApiIntegration: def __init__( self, tracer: Tracer, database_component: str, database_type="sql" ): @@ -39,13 +63,13 @@ def __init__( self._database_type = database_type self._connection_props = {} - def wrap_connect( + def wrapped_connection( self, wrapped: typing.Callable[..., any], args: typing.Tuple[any, any], kwargs: typing.Dict[any, any], ): - """Patch connect method to add tracing. + """Patch connection cursor to add tracing. """ connection = wrapped(*args, **kwargs) self._connection_props = { @@ -55,18 +79,18 @@ def wrap_connect( "user": connection.user, } - wrapt.wrap_function_wrapper(connection, "cursor", self.wrap_cursor) + wrapt.wrap_function_wrapper(connection, "cursor", self.wrapped_cursor) return connection # pylint: disable=unused-argument - def wrap_cursor( + def wrapped_cursor( self, wrapped: typing.Callable[..., any], instance: typing.Any, args: typing.Tuple[any, any], kwargs: typing.Dict[any, any], ): - """Patch cursor instance in a specific connection. + """Patch cursor execute, executemany and callproc methods. """ cursor = wrapped(*args, **kwargs) for func in QUERY_WRAP_METHODS: @@ -81,8 +105,6 @@ def add_span( args: typing.Tuple[any, any], kwargs: typing.Dict[any, any], ): - """Patch execute, executeMany and callproc methods in cursor and create span. - """ name = self._database_component database = self._connection_props.get("database", "") if database: @@ -115,3 +137,4 @@ def add_span( 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/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 23e4cd9acac..0c33e87600d 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -16,10 +16,10 @@ from unittest import mock from opentelemetry import trace as trace_api -from opentelemetry.ext.dbapi import DatabaseApiTracer +from opentelemetry.ext.dbapi import DatabaseApiIntegration -class TestMysqlIntegration(unittest.TestCase): +class TestDBApiIntegration(unittest.TestCase): def setUp(self): self.tracer = trace_api.tracer() self.span = MockSpan() @@ -43,10 +43,10 @@ def test_span_succeeded(self): "server_port": 123, "user": "testuser", } - mysql_tracer = DatabaseApiTracer( + db_integration = DatabaseApiIntegration( self.tracer, "testcomponent", "testtype" ) - mock_connection = mysql_tracer.wrap_connect( + mock_connection = db_integration.wrapped_connection( mock_connect, {}, connection_props ) cursor = mock_connection.cursor() @@ -76,29 +76,38 @@ def test_span_succeeded(self): ) def test_span_failed(self): - mysql_tracer = DatabaseApiTracer(self.tracer, "testcomponent") - mock_connection = mysql_tracer.wrap_connect(mock_connect, {}, {}) - cursor = mock_connection.cursor() - cursor.execute("Test query", throw_exception=True) - - self.assertEqual(self.span.attributes["db.statement"], "Test query") - self.assertIs( - self.span.status.canonical_code, - trace_api.status.StatusCanonicalCode.UNKNOWN, + db_integration = DatabaseApiIntegration(self.tracer, "testcomponent") + mock_connection = db_integration.wrapped_connection( + mock_connect, {}, {} ) - self.assertEqual(self.span.status.description, "Test Exception") + 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): - mysql_tracer = DatabaseApiTracer(self.tracer, "testcomponent") - mock_connection = mysql_tracer.wrap_connect(mock_connect, {}, {}) + 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): - mysql_tracer = DatabaseApiTracer(self.tracer, "testcomponent") - mock_connection = mysql_tracer.wrap_connect(mock_connect, {}, {}) + 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) @@ -113,10 +122,10 @@ def mock_connect(*args, **kwargs): server_host = kwargs.get("server_host") server_port = kwargs.get("server_port") user = kwargs.get("user") - return MockMySqlConnection(database, server_port, server_host, user) + return MockConnection(database, server_port, server_host, user) -class MockMySqlConnection: +class MockConnection: def __init__(self, database, server_port, server_host, user): self.database = database self.server_port = server_port @@ -125,10 +134,10 @@ def __init__(self, database, server_port, server_host, user): # pylint: disable=no-self-use def cursor(self): - return MockMySqlCursor() + return MockCursor() -class MockMySqlCursor: +class MockCursor: # pylint: disable=unused-argument, no-self-use def execute(self, query, params=None, throw_exception=False): if throw_exception: diff --git a/ext/opentelemetry-ext-mysql/README.rst b/ext/opentelemetry-ext-mysql/README.rst index f8bd28d0f12..e819a637694 100644 --- a/ext/opentelemetry-ext-mysql/README.rst +++ b/ext/opentelemetry-ext-mysql/README.rst @@ -13,7 +13,7 @@ Usage import mysql.connector from opentelemetry.trace import tracer - from opentelemetry.trace.ext.mysql import trace_integration + from opentelemetry.ext.mysql import trace_integration trace_integration(tracer()) cnx = mysql.connector.connect(database='MySQL_Database') diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index 2cae5645807..bbd2c869441 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -17,12 +17,9 @@ MySQL Connector/Python library. """ -import typing - import mysql.connector -import wrapt -from opentelemetry.ext.dbapi import DatabaseApiTracer +from opentelemetry.ext.dbapi import trace_integration as db_integration from opentelemetry.trace import Tracer @@ -30,17 +27,4 @@ def trace_integration(tracer: Tracer): """Integrate with MySQL Connector/Python library. https://dev.mysql.com/doc/connector-python/en/ """ - - # pylint: disable=unused-argument - def wrap( - wrapped: typing.Callable[..., any], - instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], - ): - """Patch MySQL Connector connect method to add tracing. - """ - mysql_tracer = DatabaseApiTracer(tracer, "mysql") - return mysql_tracer.wrap_connect(wrapped, args, kwargs) - - wrapt.wrap_function_wrapper(mysql.connector, "connect", wrap) + db_integration(tracer, mysql.connector, "connect", "mysql") From d47adcd2cc87536df1cec3dc0acca0262d9bb315 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 20 Nov 2019 14:07:51 -0800 Subject: [PATCH 12/22] Adding check for cursor method --- .../src/opentelemetry/ext/dbapi/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 dd0ce3c1fe3..84d15658d96 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -94,7 +94,8 @@ def wrapped_cursor( """ cursor = wrapped(*args, **kwargs) for func in QUERY_WRAP_METHODS: - wrapt.wrap_function_wrapper(cursor, func, self.add_span) + if getattr(cursor, func, None): + wrapt.wrap_function_wrapper(cursor, func, self.add_span) return cursor # pylint: disable=unused-argument From ce8e261bf8a1aeece1bc613888513e4d08448af0 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 9 Dec 2019 13:33:23 -0800 Subject: [PATCH 13/22] Addressing comments --- .../src/opentelemetry/ext/dbapi/__init__.py | 20 +++++++++++-------- .../tests/test_dbapi_integration.py | 6 +++++- 2 files changed, 17 insertions(+), 9 deletions(-) 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 84d15658d96..d6439d18b48 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -18,6 +18,7 @@ https://www.python.org/dev/peps/pep-0249/ """ +import logging import typing import wrapt @@ -47,9 +48,12 @@ def wrap_connect( db_integration = DatabaseApiIntegration(tracer, database_component) return db_integration.wrapped_connection(wrapped, args, kwargs) - wrapt.wrap_function_wrapper( - connect_module, connect_method_name, wrap_connect - ) + try: + wrapt.wrap_function_wrapper( + connect_module, connect_method_name, wrap_connect + ) + except Exception as ex: # pylint: disable=broad-except + logging.warning("Failed to integrate with DB API. %s", str(ex)) class DatabaseApiIntegration: @@ -110,10 +114,7 @@ def add_span( database = self._connection_props.get("database", "") if database: name += "." + database - query = args[0] if args else "" - # Query with parameters - if len(args) > 1: - query += " params=" + str(args[1]) + statement = args[0] if args else "" with self._tracer.start_as_current_span( name, kind=SpanKind.CLIENT @@ -121,7 +122,7 @@ def add_span( span.set_attribute("component", self._database_component) span.set_attribute("db.type", self._database_type) span.set_attribute("db.instance", database) - span.set_attribute("db.statement", query) + span.set_attribute("db.statement", statement) span.set_attribute( "db.user", self._connection_props.get("user", "") ) @@ -132,6 +133,9 @@ def add_span( if port is not None: span.set_attribute("peer.port", port) + if len(args) > 1: + span.set_attribute("db.statement.parameters", str(args[1])) + try: result = wrapped(*args, **kwargs) span.set_status(Status(StatusCanonicalCode.OK)) diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 0c33e87600d..ce82ff419ba 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -65,7 +65,11 @@ def test_span_succeeded(self): self.assertEqual(self.span.attributes["db.instance"], "testdatabase") self.assertEqual( self.span.attributes["db.statement"], - "Test query params=('param1Value', False)", + "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["peer.hostname"], "testhost") From 03a25b447f6f825aac22761f40cf35644c2a33d1 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 9 Dec 2019 13:46:31 -0800 Subject: [PATCH 14/22] Fix format issue --- ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index ce82ff419ba..c3cb5f86e73 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -63,10 +63,7 @@ def test_span_succeeded(self): 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"], "Test query") self.assertEqual( self.span.attributes["db.statement.parameters"], "('param1Value', False)", From 4b0e20f5acd90c6f43b105128f12f8c0c4cf0bf9 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Mon, 9 Dec 2019 16:17:59 -0800 Subject: [PATCH 15/22] Refactor span creation so it can be used by other DB integrations like psycopg --- .../src/opentelemetry/ext/dbapi/__init__.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) 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 d6439d18b48..9a5247f6799 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -99,22 +99,40 @@ def wrapped_cursor( cursor = wrapped(*args, **kwargs) for func in QUERY_WRAP_METHODS: if getattr(cursor, func, None): - wrapt.wrap_function_wrapper(cursor, func, self.add_span) + wrapt.wrap_function_wrapper( + cursor, func, self.wrapped_query_methods + ) return cursor # pylint: disable=unused-argument - def add_span( + def wrapped_query_methods( self, wrapped: typing.Callable[..., any], instance: typing.Any, args: typing.Tuple[any, any], kwargs: typing.Dict[any, any], + ): + statement = args[0] if args else "" + parameters = str(args[1]) if len(args) > 1 else None + span = self.create_span(self._connection_props, statement, parameters) + try: + result = wrapped(*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 + + def create_span( + self, + connection_properties: typing.Dict, + statement: str, + parameters: str, ): name = self._database_component - database = self._connection_props.get("database", "") + database = connection_properties.get("database", "") if database: name += "." + database - statement = args[0] if args else "" with self._tracer.start_as_current_span( name, kind=SpanKind.CLIENT @@ -124,22 +142,15 @@ def add_span( span.set_attribute("db.instance", database) span.set_attribute("db.statement", statement) span.set_attribute( - "db.user", self._connection_props.get("user", "") + "db.user", connection_properties.get("user", "") ) span.set_attribute( - "peer.hostname", self._connection_props.get("host", "") + "peer.hostname", connection_properties.get("host", "") ) - port = self._connection_props.get("port") + port = connection_properties.get("port") if port is not None: span.set_attribute("peer.port", port) - if len(args) > 1: - span.set_attribute("db.statement.parameters", str(args[1])) - - try: - result = wrapped(*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 + if parameters: + span.set_attribute("db.statement.parameters", parameters) + return span From 7a668153163e53b9e592f1a77546e08253187a37 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 11 Dec 2019 14:36:52 -0800 Subject: [PATCH 16/22] Refactored integration for easier reuse by other integrations Updated versions Added capabilities to specify connection attribute names --- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- .../src/opentelemetry/ext/dbapi/__init__.py | 176 +++++++++++------- .../src/opentelemetry/ext/dbapi/version.py | 2 +- .../tests/test_dbapi_integration.py | 8 +- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- .../src/opentelemetry/ext/mysql/__init__.py | 15 +- .../src/opentelemetry/ext/mysql/version.py | 2 +- 7 files changed, 135 insertions(+), 72 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index 15a6fb8352e..f0d84093f8d 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.3.dev0 + opentelemetry-api >= 0.3a0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] 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 9a5247f6799..673ef2c156a 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -26,14 +26,14 @@ from opentelemetry.trace import SpanKind, Tracer from opentelemetry.trace.status import Status, StatusCanonicalCode -QUERY_WRAP_METHODS = ["execute", "executemany", "callproc"] - 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/ @@ -45,7 +45,12 @@ def wrap_connect( args: typing.Tuple[any, any], kwargs: typing.Dict[any, any], ): - db_integration = DatabaseApiIntegration(tracer, database_component) + db_integration = DatabaseApiIntegration( + tracer, + database_component, + database_type=database_type, + connection_attributes=connection_attributes, + ) return db_integration.wrapped_connection(wrapped, args, kwargs) try: @@ -57,100 +62,139 @@ def wrap_connect( class DatabaseApiIntegration: + # pylint: disable=unused-argument def __init__( - self, tracer: Tracer, database_component: str, database_type="sql" + 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._tracer = tracer - self._database_component = database_component - self._database_type = database_type - self._connection_props = {} + 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 = {} def wrapped_connection( self, - wrapped: typing.Callable[..., any], + connect_method: typing.Callable[..., any], args: typing.Tuple[any, any], kwargs: typing.Dict[any, any], ): - """Patch connection cursor to add tracing. + """Add object proxy to connection object. """ - connection = wrapped(*args, **kwargs) - self._connection_props = { - "database": connection.database, - "port": connection.server_port, - "host": connection.server_host, - "user": connection.user, - } + connection = connect_method(*args, **kwargs) - wrapt.wrap_function_wrapper(connection, "cursor", self.wrapped_cursor) - return connection + 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 + + +class TracedConnection(wrapt.ObjectProxy): # pylint: disable=unused-argument - def wrapped_cursor( + def __init__( self, - wrapped: typing.Callable[..., any], - instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], + connection, + db_api_integration: DatabaseApiIntegration, + *args, + **kwargs, ): - """Patch cursor execute, executemany and callproc methods. - """ - cursor = wrapped(*args, **kwargs) - for func in QUERY_WRAP_METHODS: - if getattr(cursor, func, None): - wrapt.wrap_function_wrapper( - cursor, func, self.wrapped_query_methods - ) - return cursor + wrapt.ObjectProxy.__init__(self, connection) + self._db_api_integration = db_api_integration + + def cursor(self, *args, **kwargs): + return TracedCursor( + self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration + ) + +class TracedCursor(wrapt.ObjectProxy): + # pylint: disable=unused-argument - def wrapped_query_methods( + def __init__( self, - wrapped: typing.Callable[..., any], - instance: typing.Any, - args: typing.Tuple[any, any], - kwargs: typing.Dict[any, any], + cursor, + db_api_integration: DatabaseApiIntegration, + *args, + **kwargs, ): - statement = args[0] if args else "" - parameters = str(args[1]) if len(args) > 1 else None - span = self.create_span(self._connection_props, statement, parameters) - try: - result = wrapped(*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 - - def create_span( + 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, - connection_properties: typing.Dict, - statement: str, - parameters: str, + query_method: typing.Callable[..., any], + *args: typing.Tuple[any, any], + **kwargs: typing.Dict[any, any], ): - name = self._database_component - database = connection_properties.get("database", "") + + statement = args[0] if args else "" + name = self._db_api_integration.database_component + database = self._db_api_integration.connection_props.get( + "database", "" + ) if database: name += "." + database - with self._tracer.start_as_current_span( + with self._db_api_integration.tracer.start_as_current_span( name, kind=SpanKind.CLIENT ) as span: - span.set_attribute("component", self._database_component) - span.set_attribute("db.type", self._database_type) - span.set_attribute("db.instance", database) - span.set_attribute("db.statement", statement) span.set_attribute( - "db.user", connection_properties.get("user", "") + "component", self._db_api_integration.database_component ) span.set_attribute( - "peer.hostname", connection_properties.get("host", "") + "db.type", self._db_api_integration.database_type ) - port = connection_properties.get("port") + span.set_attribute("db.instance", database) + span.set_attribute("db.statement", statement) + + user = self._db_api_integration.connection_props.get("user") + if user is not None: + span.set_attribute("db.user", user) + host = self._db_api_integration.connection_props.get("host") + if host is not None: + span.set_attribute("peer.hostname", host) + port = self._db_api_integration.connection_props.get("port") if port is not None: span.set_attribute("peer.port", port) - if parameters: - span.set_attribute("db.statement.parameters", parameters) - return span + 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 index 93ef792d051..0a0ce4958ae 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.3a0" diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index c3cb5f86e73..374595e984d 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -43,8 +43,14 @@ def test_span_succeeded(self): "server_port": 123, "user": "testuser", } + connection_attributes = { + "database": "database", + "port": "server_port", + "host": "server_host", + "user": "user", + } db_integration = DatabaseApiIntegration( - self.tracer, "testcomponent", "testtype" + self.tracer, "testcomponent", "testtype", connection_attributes ) mock_connection = db_integration.wrapped_connection( mock_connect, {}, connection_props diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index a4038cc0b25..0e5dcfbb2e3 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.3.dev0 + opentelemetry-api >= 0.3a0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py index bbd2c869441..9c8c3e9da70 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/__init__.py @@ -27,4 +27,17 @@ def trace_integration(tracer: Tracer): """Integrate with MySQL Connector/Python library. https://dev.mysql.com/doc/connector-python/en/ """ - db_integration(tracer, mysql.connector, "connect", "mysql") + 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 index 93ef792d051..0a0ce4958ae 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3.dev0" +__version__ = "0.3a0" From 28108a1abfce888773b3eca2af60d427923231e2 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 11 Dec 2019 14:51:49 -0800 Subject: [PATCH 17/22] Updating to 0.4.dev0 --- ext/opentelemetry-ext-dbapi/setup.cfg | 2 +- .../src/opentelemetry/ext/dbapi/version.py | 2 +- ext/opentelemetry-ext-mysql/setup.cfg | 2 +- .../src/opentelemetry/ext/mysql/version.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/setup.cfg b/ext/opentelemetry-ext-dbapi/setup.cfg index f0d84093f8d..f0de68dc263 100644 --- a/ext/opentelemetry-ext-dbapi/setup.cfg +++ b/ext/opentelemetry-ext-dbapi/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.3a0 + opentelemetry-api >= 0.4.dev0 wrapt >= 1.0.0, < 2.0.0 [options.packages.find] diff --git a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py index 0a0ce4958ae..2f792fff802 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3a0" +__version__ = "0.4.dev0" diff --git a/ext/opentelemetry-ext-mysql/setup.cfg b/ext/opentelemetry-ext-mysql/setup.cfg index 0e5dcfbb2e3..fdc608bb3d1 100644 --- a/ext/opentelemetry-ext-mysql/setup.cfg +++ b/ext/opentelemetry-ext-mysql/setup.cfg @@ -39,7 +39,7 @@ package_dir= =src packages=find_namespace: install_requires = - opentelemetry-api >= 0.3a0 + opentelemetry-api >= 0.4.dev0 mysql-connector-python ~= 8.0 wrapt >= 1.0.0, < 2.0.0 diff --git a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py index 0a0ce4958ae..2f792fff802 100644 --- a/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py +++ b/ext/opentelemetry-ext-mysql/src/opentelemetry/ext/mysql/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.3a0" +__version__ = "0.4.dev0" From 3f0c0824e8fa8ee88632cd3c127d3d311628906d Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 11 Dec 2019 15:56:17 -0800 Subject: [PATCH 18/22] Fixing invalid syntax in parameters --- .../src/opentelemetry/ext/dbapi/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 673ef2c156a..26ec2ccd8f9 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -112,7 +112,7 @@ def __init__( connection, db_api_integration: DatabaseApiIntegration, *args, - **kwargs, + **kwargs ): wrapt.ObjectProxy.__init__(self, connection) self._db_api_integration = db_api_integration @@ -124,14 +124,14 @@ def cursor(self, *args, **kwargs): class TracedCursor(wrapt.ObjectProxy): - + # pylint: disable=unused-argument def __init__( self, cursor, db_api_integration: DatabaseApiIntegration, *args, - **kwargs, + **kwargs ): wrapt.ObjectProxy.__init__(self, cursor) self._db_api_integration = db_api_integration @@ -155,7 +155,7 @@ def _traced_execution( self, query_method: typing.Callable[..., any], *args: typing.Tuple[any, any], - **kwargs: typing.Dict[any, any], + **kwargs: typing.Dict[any, any] ): statement = args[0] if args else "" From d480718eafe7c4c220ab20b5227601945ed67651 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Wed, 11 Dec 2019 16:10:57 -0800 Subject: [PATCH 19/22] Ignore lint abstract-method --- .../src/opentelemetry/ext/dbapi/__init__.py | 2 ++ 1 file changed, 2 insertions(+) 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 26ec2ccd8f9..f675985fba3 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -104,6 +104,7 @@ def wrapped_connection( return traced_connection +# pylint: disable=abstract-method class TracedConnection(wrapt.ObjectProxy): # pylint: disable=unused-argument @@ -123,6 +124,7 @@ def cursor(self, *args, **kwargs): ) +# pylint: disable=abstract-method class TracedCursor(wrapt.ObjectProxy): # pylint: disable=unused-argument From 83d95e7c8f9768fbd7476d1c1071035388615f04 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Thu, 2 Jan 2020 12:02:42 -0800 Subject: [PATCH 20/22] Addressing comments --- ext/opentelemetry-ext-dbapi/README.rst | 1 - .../src/opentelemetry/ext/dbapi/__init__.py | 45 +++++++++++++------ .../tests/test_dbapi_integration.py | 4 +- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/ext/opentelemetry-ext-dbapi/README.rst b/ext/opentelemetry-ext-dbapi/README.rst index 3db7e66de30..6dc31216039 100644 --- a/ext/opentelemetry-ext-dbapi/README.rst +++ b/ext/opentelemetry-ext-dbapi/README.rst @@ -10,7 +10,6 @@ Usage .. code:: python - import wrapt import mysql.connector from opentelemetry.trace import tracer from opentelemetry.ext.dbapi import trace_integration 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 f675985fba3..5d552d13d92 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -26,6 +26,8 @@ from opentelemetry.trace import SpanKind, Tracer from opentelemetry.trace.status import Status, StatusCanonicalCode +logger = logging.getLogger(__name__) + def trace_integration( tracer: Tracer, @@ -36,8 +38,16 @@ def trace_integration( connection_attributes: typing.Dict = None, ): """Integrate with DB API library. - https://www.python.org/dev/peps/pep-0249/ + 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], @@ -58,7 +68,7 @@ def wrap_connect( connect_module, connect_method_name, wrap_connect ) except Exception as ex: # pylint: disable=broad-except - logging.warning("Failed to integrate with DB API. %s", str(ex)) + logger.warning("Failed to integrate with DB API. %s", str(ex)) class DatabaseApiIntegration: @@ -84,6 +94,8 @@ def __init__( self.database_component = database_component self.database_type = database_type self.connection_props = {} + self.name = "" + self.database = "" def wrapped_connection( self, @@ -99,7 +111,6 @@ def wrapped_connection( attribute = getattr(connection, value, None) if attribute: self.connection_props[key] = attribute - traced_connection = TracedConnection(connection, self) return traced_connection @@ -118,6 +129,17 @@ def __init__( 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 + ) + def cursor(self, *args, **kwargs): return TracedCursor( self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration @@ -161,15 +183,8 @@ def _traced_execution( ): statement = args[0] if args else "" - name = self._db_api_integration.database_component - database = self._db_api_integration.connection_props.get( - "database", "" - ) - if database: - name += "." + database - with self._db_api_integration.tracer.start_as_current_span( - name, kind=SpanKind.CLIENT + self._db_api_integration.name, kind=SpanKind.CLIENT ) as span: span.set_attribute( "component", self._db_api_integration.database_component @@ -177,7 +192,9 @@ def _traced_execution( span.set_attribute( "db.type", self._db_api_integration.database_type ) - span.set_attribute("db.instance", database) + span.set_attribute( + "db.instance", self._db_api_integration.database + ) span.set_attribute("db.statement", statement) user = self._db_api_integration.connection_props.get("user") @@ -185,10 +202,10 @@ def _traced_execution( span.set_attribute("db.user", user) host = self._db_api_integration.connection_props.get("host") if host is not None: - span.set_attribute("peer.hostname", host) + span.set_attribute("net.peer.hostname", host) port = self._db_api_integration.connection_props.get("port") if port is not None: - span.set_attribute("peer.port", port) + span.set_attribute("net.peer.port", port) if len(args) > 1: span.set_attribute("db.statement.parameters", str(args[1])) diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 374595e984d..1fac00645c2 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -75,8 +75,8 @@ def test_span_succeeded(self): "('param1Value', False)", ) self.assertEqual(self.span.attributes["db.user"], "testuser") - self.assertEqual(self.span.attributes["peer.hostname"], "testhost") - self.assertEqual(self.span.attributes["peer.port"], 123) + self.assertEqual(self.span.attributes["net.peer.hostname"], "testhost") + self.assertEqual(self.span.attributes["net.peer.port"], 123) self.assertIs( self.span.status.canonical_code, trace_api.status.StatusCanonicalCode.OK, From 0a37e3f2f0f4da32e987258dba5651e71bc04120 Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Thu, 2 Jan 2020 12:22:33 -0800 Subject: [PATCH 21/22] Fixing tests --- .../src/opentelemetry/ext/dbapi/__init__.py | 2 +- ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py | 4 ++-- ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 5d552d13d92..458814269dd 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -202,7 +202,7 @@ def _traced_execution( span.set_attribute("db.user", user) host = self._db_api_integration.connection_props.get("host") if host is not None: - span.set_attribute("net.peer.hostname", host) + span.set_attribute("net.peer.name", host) port = self._db_api_integration.connection_props.get("port") if port is not None: span.set_attribute("net.peer.port", port) diff --git a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py index 1fac00645c2..f5d1299e838 100644 --- a/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py +++ b/ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py @@ -21,7 +21,7 @@ class TestDBApiIntegration(unittest.TestCase): def setUp(self): - self.tracer = trace_api.tracer() + self.tracer = trace_api.Tracer() self.span = MockSpan() self.start_current_span_patcher = mock.patch.object( self.tracer, @@ -75,7 +75,7 @@ def test_span_succeeded(self): "('param1Value', False)", ) self.assertEqual(self.span.attributes["db.user"], "testuser") - self.assertEqual(self.span.attributes["net.peer.hostname"], "testhost") + 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, diff --git a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py index 2ba8cefeb36..1bcd851750c 100644 --- a/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py +++ b/ext/opentelemetry-ext-mysql/tests/test_mysql_integration.py @@ -23,7 +23,7 @@ class TestMysqlIntegration(unittest.TestCase): def test_trace_integration(self): - tracer = trace_api.tracer() + tracer = trace_api.Tracer() span = mock.create_autospec(trace_api.Span, spec_set=True) start_current_span_patcher = mock.patch.object( tracer, From 26e4983effb7b82678bed41af4cef8b6ac104b3a Mon Sep 17 00:00:00 2001 From: Hector Hernandez Guzman Date: Fri, 3 Jan 2020 11:36:01 -0800 Subject: [PATCH 22/22] Addressing feedback --- .../src/opentelemetry/ext/dbapi/__init__.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) 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 458814269dd..7ba1de1795a 100644 --- a/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py +++ b/ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py @@ -94,6 +94,7 @@ def __init__( self.database_component = database_component self.database_type = database_type self.connection_props = {} + self.span_attributes = {} self.name = "" self.database = "" @@ -139,6 +140,15 @@ def __init__( 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( @@ -197,15 +207,11 @@ def _traced_execution( ) span.set_attribute("db.statement", statement) - user = self._db_api_integration.connection_props.get("user") - if user is not None: - span.set_attribute("db.user", user) - host = self._db_api_integration.connection_props.get("host") - if host is not None: - span.set_attribute("net.peer.name", host) - port = self._db_api_integration.connection_props.get("port") - if port is not None: - span.set_attribute("net.peer.port", port) + 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]))