-
Notifications
You must be signed in to change notification settings - Fork 864
Adding DB API integration + MySQL connector integration #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
2c2f2ba
47824f2
b5d81b8
f9f03a8
10f3f53
e6158c8
315e583
cb8a835
857eed6
aefc210
ebee38c
9cfc808
9997b46
d47adcd
ce8e261
d7a1d07
03a25b4
4b0e20f
7a66815
7952e63
28108a1
93468c0
3f0c082
d480718
83d95e7
f5fa462
0a37e3f
26e4983
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <https://opentelemetry.io/>`_ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
hectorhdzg marked this conversation as resolved.
Outdated
|
||
| wrapt >= 1.0.0, < 2.0.0 | ||
|
|
||
| [options.packages.find] | ||
| where = src | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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__"]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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): | ||
|
hectorhdzg marked this conversation as resolved.
Outdated
|
||
| """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))) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
|
hectorhdzg marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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.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") | ||
|
|
||
|
|
||
| # 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are MockSpan and MockTracer needed? I feel like we have a no-op tracer now with the API.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to test the integration code is creating the span correctly, API tracer is no-op so it will not create anything, are you suggesting to use unittest.Mock with a side effect or some other pattern to test here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using no-op tracer in tests but still using MockSpan to check all attributes are set up correctly
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could use a Mock object which takes the Tracer as as a spec: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the purpose here is to provide a general test utility for tracers, I guess I'd rather see it shared in something like opentelemetry-api. But I'm not sure if sharing test fixtures across projects is an accepted policy.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mocking tracer using mock library now, for Span I prefer to create my own mock class so is easier to test the integration adding correct attributes to the Span |
||
| 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 | ||
Uh oh!
There was an error while loading. Please reload this page.