Skip to content

Commit 2992950

Browse files
ext/pymysql: Add Instrumentor (#611)
Co-authored-by: Diego Hurtado <[email protected]>
1 parent e6a9d97 commit 2992950

File tree

10 files changed

+149
-57
lines changed

10 files changed

+149
-57
lines changed

ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ def wrap_connect_(
125125
logger.warning("Failed to integrate with DB API. %s", str(ex))
126126

127127

128+
def unwrap_connect(
129+
connect_module: typing.Callable[..., any], connect_method_name: str,
130+
):
131+
conn = getattr(connect_module, connect_method_name, None)
132+
if isinstance(conn, wrapt.ObjectProxy):
133+
setattr(connect_module, connect_method_name, conn.__wrapped__)
134+
135+
128136
class DatabaseApiIntegration:
129137
def __init__(
130138
self,
@@ -184,7 +192,7 @@ def get_connection_attributes(self, connection):
184192
self.name += "." + self.database
185193
user = self.connection_props.get("user")
186194
if user is not None:
187-
self.span_attributes["db.user"] = user
195+
self.span_attributes["db.user"] = str(user)
188196
host = self.connection_props.get("host")
189197
if host is not None:
190198
self.span_attributes["net.peer.name"] = host

ext/opentelemetry-ext-dbapi/tests/test_dbapi_integration.py

+33-5
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from unittest import mock
16+
1517
from opentelemetry import trace as trace_api
16-
from opentelemetry.ext.dbapi import DatabaseApiIntegration
18+
from opentelemetry.ext import dbapi
1719
from opentelemetry.test.test_base import TestBase
1820

1921

@@ -35,7 +37,7 @@ def test_span_succeeded(self):
3537
"host": "server_host",
3638
"user": "user",
3739
}
38-
db_integration = DatabaseApiIntegration(
40+
db_integration = dbapi.DatabaseApiIntegration(
3941
self.tracer, "testcomponent", "testtype", connection_attributes
4042
)
4143
mock_connection = db_integration.wrapped_connection(
@@ -66,7 +68,9 @@ def test_span_succeeded(self):
6668
)
6769

6870
def test_span_failed(self):
69-
db_integration = DatabaseApiIntegration(self.tracer, "testcomponent")
71+
db_integration = dbapi.DatabaseApiIntegration(
72+
self.tracer, "testcomponent"
73+
)
7074
mock_connection = db_integration.wrapped_connection(
7175
mock_connect, {}, {}
7276
)
@@ -85,7 +89,9 @@ def test_span_failed(self):
8589
self.assertEqual(span.status.description, "Test Exception")
8690

8791
def test_executemany(self):
88-
db_integration = DatabaseApiIntegration(self.tracer, "testcomponent")
92+
db_integration = dbapi.DatabaseApiIntegration(
93+
self.tracer, "testcomponent"
94+
)
8995
mock_connection = db_integration.wrapped_connection(
9096
mock_connect, {}, {}
9197
)
@@ -97,7 +103,9 @@ def test_executemany(self):
97103
self.assertEqual(span.attributes["db.statement"], "Test query")
98104

99105
def test_callproc(self):
100-
db_integration = DatabaseApiIntegration(self.tracer, "testcomponent")
106+
db_integration = dbapi.DatabaseApiIntegration(
107+
self.tracer, "testcomponent"
108+
)
101109
mock_connection = db_integration.wrapped_connection(
102110
mock_connect, {}, {}
103111
)
@@ -110,6 +118,26 @@ def test_callproc(self):
110118
span.attributes["db.statement"], "Test stored procedure"
111119
)
112120

121+
@mock.patch("opentelemetry.ext.dbapi")
122+
def test_wrap_connect(self, mock_dbapi):
123+
dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-")
124+
connection = mock_dbapi.connect()
125+
self.assertEqual(mock_dbapi.connect.call_count, 1)
126+
self.assertIsInstance(connection, dbapi.TracedConnectionProxy)
127+
self.assertIsInstance(connection.__wrapped__, mock.Mock)
128+
129+
@mock.patch("opentelemetry.ext.dbapi")
130+
def test_unwrap_connect(self, mock_dbapi):
131+
dbapi.wrap_connect(self.tracer, mock_dbapi, "connect", "-")
132+
connection = mock_dbapi.connect()
133+
self.assertEqual(mock_dbapi.connect.call_count, 1)
134+
self.assertIsInstance(connection, dbapi.TracedConnectionProxy)
135+
136+
dbapi.unwrap_connect(mock_dbapi, "connect")
137+
connection = mock_dbapi.connect()
138+
self.assertEqual(mock_dbapi.connect.call_count, 2)
139+
self.assertIsInstance(connection, mock.Mock)
140+
113141

114142
# pylint: disable=unused-argument
115143
def mock_connect(*args, **kwargs):

ext/opentelemetry-ext-docker-tests/tests/pymysql/test_pymysql_functional.py

+7-19
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,12 @@
1313
# limitations under the License.
1414

1515
import os
16-
import time
17-
import unittest
1816

1917
import pymysql as pymy
2018

2119
from opentelemetry import trace as trace_api
22-
from opentelemetry.ext.pymysql import trace_integration
23-
from opentelemetry.sdk.trace import Tracer, TracerProvider
24-
from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor
25-
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
26-
InMemorySpanExporter,
27-
)
20+
from opentelemetry.ext.pymysql import PyMySQLInstrumentor
21+
from opentelemetry.test.test_base import TestBase
2822

2923
MYSQL_USER = os.getenv("MYSQL_USER ", "testuser")
3024
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD ", "testpassword")
@@ -33,17 +27,14 @@
3327
MYSQL_DB_NAME = os.getenv("MYSQL_DB_NAME ", "opentelemetry-tests")
3428

3529

36-
class TestFunctionalPyMysql(unittest.TestCase):
30+
class TestFunctionalPyMysql(TestBase):
3731
@classmethod
3832
def setUpClass(cls):
33+
super().setUpClass()
3934
cls._connection = None
4035
cls._cursor = None
41-
cls._tracer_provider = TracerProvider()
42-
cls._tracer = Tracer(cls._tracer_provider, None)
43-
cls._span_exporter = InMemorySpanExporter()
44-
cls._span_processor = SimpleExportSpanProcessor(cls._span_exporter)
45-
cls._tracer_provider.add_span_processor(cls._span_processor)
46-
trace_integration(cls._tracer_provider)
36+
cls._tracer = cls.tracer_provider.get_tracer(__name__)
37+
PyMySQLInstrumentor().instrument()
4738
cls._connection = pymy.connect(
4839
user=MYSQL_USER,
4940
password=MYSQL_PASSWORD,
@@ -58,11 +49,8 @@ def tearDownClass(cls):
5849
if cls._connection:
5950
cls._connection.close()
6051

61-
def setUp(self):
62-
self._span_exporter.clear()
63-
6452
def validate_spans(self):
65-
spans = self._span_exporter.get_finished_spans()
53+
spans = self.memory_exporter.get_finished_spans()
6654
self.assertEqual(len(spans), 2)
6755
for span in spans:
6856
if span.name == "rootSpan":
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# Changelog
22

33
## Unreleased
4+
5+
- Implement PyMySQL integration ([#504](https://github.com/open-telemetry/opentelemetry-python/pull/504))
6+
- Implement instrumentor interface ([#611](https://github.com/open-telemetry/opentelemetry-python/pull/611))

ext/opentelemetry-ext-pymysql/README.rst

-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ OpenTelemetry PyMySQL integration
66
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-pymysql.svg
77
:target: https://pypi.org/project/opentelemetry-ext-pymysql/
88

9-
Integration with PyMySQL that supports the PyMySQL library and is
10-
specified to trace_integration using 'PyMySQL'.
11-
12-
139
Installation
1410
------------
1511

ext/opentelemetry-ext-pymysql/setup.cfg

+9
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,16 @@ packages=find_namespace:
4242
install_requires =
4343
opentelemetry-api == 0.7.dev0
4444
opentelemetry-ext-dbapi == 0.7.dev0
45+
opentelemetry-auto-instrumentation == 0.7.dev0
4546
PyMySQL ~= 0.9.3
4647

48+
[options.extras_require]
49+
test =
50+
opentelemetry-test == 0.7.dev0
51+
4752
[options.packages.find]
4853
where = src
54+
55+
[options.entry_points]
56+
opentelemetry_instrumentor =
57+
pymysql = opentelemetry.ext.pymysql:PyMySQLInstrumentor

ext/opentelemetry-ext-pymysql/src/opentelemetry/ext/pymysql/__init__.py

+27-19
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515
"""
16-
The integration with PyMySQL supports the `PyMySQL`_ library and is specified
17-
to ``trace_integration`` using ``'PyMySQL'``.
16+
The integration with PyMySQL supports the `PyMySQL`_ library and can be enabled
17+
by using ``PyMySQLInstrumentor``.
1818
1919
.. _PyMySQL: https://pypi.org/project/PyMySQL/
2020
@@ -25,11 +25,13 @@
2525
2626
import pymysql
2727
from opentelemetry import trace
28-
from opentelemetry.ext.pymysql import trace_integration
28+
from opentelemetry.ext.pymysql import PyMySQLInstrumentor
2929
from opentelemetry.sdk.trace import TracerProvider
3030
3131
trace.set_tracer_provider(TracerProvider())
32-
trace_integration()
32+
33+
PyMySQLInstrumentor().instrument()
34+
3335
cnx = pymysql.connect(database="MySQL_Database")
3436
cursor = cnx.cursor()
3537
cursor.execute("INSERT INTO test (testField) VALUES (123)"
@@ -45,24 +47,30 @@
4547

4648
import pymysql
4749

48-
from opentelemetry.ext.dbapi import wrap_connect
50+
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
51+
from opentelemetry.ext.dbapi import unwrap_connect, wrap_connect
4952
from opentelemetry.ext.pymysql.version import __version__
5053
from opentelemetry.trace import TracerProvider, get_tracer
5154

5255

53-
def trace_integration(tracer_provider: typing.Optional[TracerProvider] = None):
54-
"""Integrate with the PyMySQL library.
55-
https://github.com/PyMySQL/PyMySQL/
56-
"""
56+
class PyMySQLInstrumentor(BaseInstrumentor):
57+
def _instrument(self, **kwargs):
58+
"""Integrate with the PyMySQL library.
59+
https://github.com/PyMySQL/PyMySQL/
60+
"""
61+
tracer_provider = kwargs.get("tracer_provider")
62+
63+
tracer = get_tracer(__name__, __version__, tracer_provider)
5764

58-
tracer = get_tracer(__name__, __version__, tracer_provider)
65+
connection_attributes = {
66+
"database": "db",
67+
"port": "port",
68+
"host": "host",
69+
"user": "user",
70+
}
71+
wrap_connect(
72+
tracer, pymysql, "connect", "mysql", "sql", connection_attributes
73+
)
5974

60-
connection_attributes = {
61-
"database": "db",
62-
"port": "port",
63-
"host": "host",
64-
"user": "user",
65-
}
66-
wrap_connect(
67-
tracer, pymysql, "connect", "mysql", "sql", connection_attributes
68-
)
75+
def _uninstrument(self, **kwargs):
76+
unwrap_connect(pymysql, "connect")

ext/opentelemetry-ext-pymysql/tests/test_pymysql_integration.py

+47-8
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,61 @@
1717
import pymysql
1818

1919
import opentelemetry.ext.pymysql
20-
from opentelemetry.ext.pymysql import trace_integration
20+
from opentelemetry.ext.pymysql import PyMySQLInstrumentor
21+
from opentelemetry.sdk import resources
2122
from opentelemetry.test.test_base import TestBase
2223

2324

2425
class TestPyMysqlIntegration(TestBase):
25-
def test_trace_integration(self):
26-
with mock.patch("pymysql.connect"):
27-
trace_integration()
28-
cnx = pymysql.connect(database="test")
29-
cursor = cnx.cursor()
30-
query = "SELECT * FROM test"
31-
cursor.execute(query)
26+
def tearDown(self):
27+
super().tearDown()
28+
with self.disable_logging():
29+
PyMySQLInstrumentor().uninstrument()
30+
31+
@mock.patch("pymysql.connect")
32+
# pylint: disable=unused-argument
33+
def test_instrumentor(self, mock_connect):
34+
PyMySQLInstrumentor().instrument()
35+
36+
cnx = pymysql.connect(database="test")
37+
cursor = cnx.cursor()
38+
query = "SELECT * FROM test"
39+
cursor.execute(query)
3240

3341
spans_list = self.memory_exporter.get_finished_spans()
3442
self.assertEqual(len(spans_list), 1)
3543
span = spans_list[0]
3644

3745
# Check version and name in span's instrumentation info
3846
self.check_span_instrumentation_info(span, opentelemetry.ext.pymysql)
47+
48+
# check that no spans are generated after uninstrument
49+
PyMySQLInstrumentor().uninstrument()
50+
51+
cnx = pymysql.connect(database="test")
52+
cursor = cnx.cursor()
53+
query = "SELECT * FROM test"
54+
cursor.execute(query)
55+
56+
spans_list = self.memory_exporter.get_finished_spans()
57+
self.assertEqual(len(spans_list), 1)
58+
59+
@mock.patch("pymysql.connect")
60+
# pylint: disable=unused-argument
61+
def test_custom_tracer_provider(self, mock_connect):
62+
resource = resources.Resource.create({})
63+
result = self.create_tracer_provider(resource=resource)
64+
tracer_provider, exporter = result
65+
66+
PyMySQLInstrumentor().instrument(tracer_provider=tracer_provider)
67+
68+
cnx = pymysql.connect(database="test")
69+
cursor = cnx.cursor()
70+
query = "SELECT * FROM test"
71+
cursor.execute(query)
72+
73+
spans_list = exporter.get_finished_spans()
74+
self.assertEqual(len(spans_list), 1)
75+
span = spans_list[0]
76+
77+
self.assertIs(span.resource, resource)

tests/util/src/opentelemetry/test/test_base.py

+12
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import logging
1516
import unittest
17+
from contextlib import contextmanager
1618

1719
from opentelemetry import trace as trace_api
1820
from opentelemetry.sdk.trace import TracerProvider, export
@@ -59,3 +61,13 @@ def create_tracer_provider(**kwargs):
5961
tracer_provider.add_span_processor(span_processor)
6062

6163
return tracer_provider, memory_exporter
64+
65+
@staticmethod
66+
@contextmanager
67+
def disable_logging(highest_level=logging.CRITICAL):
68+
logging.disable(highest_level)
69+
70+
try:
71+
yield
72+
finally:
73+
logging.disable(logging.NOTSET)

tox.ini

+2-1
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,9 @@ commands_pre =
188188
psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-dbapi
189189
psycopg2: pip install {toxinidir}/ext/opentelemetry-ext-psycopg2
190190

191+
pymysql: pip install {toxinidir}/opentelemetry-auto-instrumentation
191192
pymysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi
192-
pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql
193+
pymysql: pip install {toxinidir}/ext/opentelemetry-ext-pymysql[test]
193194

194195
redis: pip install {toxinidir}/opentelemetry-auto-instrumentation
195196
redis: pip install {toxinidir}/ext/opentelemetry-ext-redis[test]

0 commit comments

Comments
 (0)