Skip to content

Commit c9bb751

Browse files
authored
added instrumentation for mysql-connector and pymysql (#603)
* added instrumentation for mysql-connector-python * added support for instrumenting pymysql
1 parent 26c62b2 commit c9bb751

15 files changed

+357
-4
lines changed

.ci/.jenkins_framework.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ FRAMEWORK:
3030
- eventlet-newest
3131
- gevent-newest
3232
- zerorpc-0.4
33+
- mysql_connector-newest
34+
- pymysql-newest

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repos:
1414
- id: black
1515
language_version: python3
1616
- repo: https://github.com/pre-commit/pre-commit-hooks
17-
rev: v1.3.0
17+
rev: v2.3.0
1818
hooks:
1919
- id: flake8
2020
exclude: elasticapm\/utils\/wrapt|build|src|tests|dist|conftest.py|setup.py

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55

66
### New Features
77

8+
* added instrumentation for mysql-connector and pymysql (#603)
89
* implemented stack_trace_limit configuration option (#623)
910
* autoinsert tracing middleware in django settings (#625)
1011

11-
###
12-
1312
## v5.2.3
1413
[Check the diff](https://github.com/elastic/apm-agent-python/compare/v5.2.2...v5.2.3)
1514

docs/supported-technologies.asciidoc

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Collected trace data:
9191

9292
[float]
9393
[[automatic-instrumentation-db-mysql]]
94-
==== MySQL
94+
==== MySQLdb
9595

9696
Library: `MySQLdb`
9797

@@ -101,6 +101,38 @@ Instrumented methods:
101101

102102
The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls.
103103

104+
Collected trace data:
105+
106+
* parametrized SQL query
107+
108+
[float]
109+
[[automatic-instrumentation-db-mysql-connector]]
110+
==== mysql-connector
111+
112+
Library: `mysql-connector-python`
113+
114+
Instrumented methods:
115+
116+
* `mysql.connector.connect`
117+
118+
The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls.
119+
120+
Collected trace data:
121+
122+
* parametrized SQL query
123+
124+
[float]
125+
[[automatic-instrumentation-db-pymysql]]
126+
==== pymysql
127+
128+
Library: `pymysql`
129+
130+
Instrumented methods:
131+
132+
* `pymysql.connect`
133+
134+
The instrumented `connect` method returns a wrapped connection/cursor which instruments the actual `Cursor.execute` calls.
135+
104136
Collected trace data:
105137

106138
* parametrized SQL query
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
32+
from elasticapm.instrumentation.packages.dbapi2 import (
33+
ConnectionProxy,
34+
CursorProxy,
35+
DbApi2Instrumentation,
36+
extract_signature,
37+
)
38+
39+
40+
class MySQLCursorProxy(CursorProxy):
41+
provider_name = "mysql"
42+
43+
def extract_signature(self, sql):
44+
return extract_signature(sql)
45+
46+
47+
class MySQLConnectionProxy(ConnectionProxy):
48+
cursor_proxy = MySQLCursorProxy
49+
50+
51+
class MySQLConnectorInstrumentation(DbApi2Instrumentation):
52+
name = "mysql_connector"
53+
54+
instrument_list = [("mysql.connector", "connect")]
55+
56+
def call(self, module, method, wrapped, instance, args, kwargs):
57+
return MySQLConnectionProxy(wrapped(*args, **kwargs))
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
from elasticapm.instrumentation.packages.dbapi2 import (
32+
ConnectionProxy,
33+
CursorProxy,
34+
DbApi2Instrumentation,
35+
extract_signature,
36+
)
37+
38+
39+
class PyMySQLCursorProxy(CursorProxy):
40+
provider_name = "mysql"
41+
42+
def extract_signature(self, sql):
43+
return extract_signature(sql)
44+
45+
46+
class PyMySQLConnectionProxy(ConnectionProxy):
47+
cursor_proxy = PyMySQLCursorProxy
48+
49+
50+
class PyMySQLConnectorInstrumentation(DbApi2Instrumentation):
51+
name = "pymysql"
52+
53+
instrument_list = [("pymysql", "connect")]
54+
55+
def call(self, module, method, wrapped, instance, args, kwargs):
56+
return PyMySQLConnectionProxy(wrapped(*args, **kwargs))

elasticapm/instrumentation/register.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"elasticapm.instrumentation.packages.psycopg2.Psycopg2Instrumentation",
3838
"elasticapm.instrumentation.packages.psycopg2.Psycopg2ExtensionsInstrumentation",
3939
"elasticapm.instrumentation.packages.mysql.MySQLInstrumentation",
40+
"elasticapm.instrumentation.packages.mysql_connector.MySQLConnectorInstrumentation",
41+
"elasticapm.instrumentation.packages.pymysql.PyMySQLConnectorInstrumentation",
4042
"elasticapm.instrumentation.packages.pylibmc.PyLibMcInstrumentation",
4143
"elasticapm.instrumentation.packages.pymongo.PyMongoInstrumentation",
4244
"elasticapm.instrumentation.packages.pymongo.PyMongoBulkInstrumentation",

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ markers =
2828
memcached
2929
redis
3030
psutil
31+
mysql_connector
32+
pymysql
33+
mysqldb
3134

3235
[isort]
3336
line_length=120

tests/docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ services:
124124
volumes:
125125
- pymssqldata:/var/opt/mssql
126126

127+
mysql:
128+
image: mysql
129+
command: --default-authentication-plugin=mysql_native_password --log_error_verbosity=3
130+
environment:
131+
- MYSQL_DATABASE=eapm_tests
132+
- MYSQL_USER=eapm
133+
- MYSQL_PASSWORD=Very(!)Secure
134+
- MYSQL_RANDOM_ROOT_PASSWORD=yes
135+
volumes:
136+
- mysqldata:/var/lib/mysql
137+
127138
run_tests:
128139
image: apm-agent-python:${PYTHON_VERSION}
129140
environment:
@@ -156,3 +167,5 @@ volumes:
156167
driver: local
157168
pymssqldata:
158169
driver: local
170+
mysqldata:
171+
driver: local
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
32+
import os
33+
34+
import pytest
35+
36+
from elasticapm.conf.constants import TRANSACTION
37+
38+
connector = pytest.importorskip("mysql.connector")
39+
40+
pytestmark = [pytest.mark.mysql_connector]
41+
42+
43+
if "MYSQL_HOST" not in os.environ:
44+
pytestmark.append(pytest.mark.skip("Skipping mysql-connector tests, no MYSQL_HOST environment variable set"))
45+
46+
47+
@pytest.yield_fixture(scope="function")
48+
def mysql_connector_connection(request):
49+
conn = connector.connect(
50+
host=os.environ.get("MYSQL_HOST", "localhost"),
51+
user=os.environ.get("MYSQL_USER", "eapm"),
52+
password=os.environ.get("MYSQL_PASSWORD", ""),
53+
database=os.environ.get("MYSQL_DATABASE", "eapm_tests"),
54+
)
55+
cursor = conn.cursor()
56+
cursor.execute("CREATE TABLE `test` (`id` INT, `name` VARCHAR(5))")
57+
cursor.execute("INSERT INTO `test` (`id`, `name`) VALUES (1, 'one'), (2, 'two'), (3, 'three')")
58+
row = cursor.fetchone()
59+
print(row)
60+
61+
yield conn
62+
63+
cursor.execute("DROP TABLE `test`")
64+
65+
66+
@pytest.mark.integrationtest
67+
def test_mysql_connector_select(instrument, mysql_connector_connection, elasticapm_client):
68+
cursor = mysql_connector_connection.cursor()
69+
query = "SELECT * FROM test WHERE name LIKE 't%' ORDER BY id"
70+
71+
try:
72+
elasticapm_client.begin_transaction("web.django")
73+
cursor.execute(query)
74+
assert cursor.fetchall() == [(2, "two"), (3, "three")]
75+
elasticapm_client.end_transaction(None, "test-transaction")
76+
finally:
77+
transactions = elasticapm_client.events[TRANSACTION]
78+
spans = elasticapm_client.spans_for_transaction(transactions[0])
79+
span = spans[0]
80+
assert span["name"] == "SELECT FROM test"
81+
assert span["type"] == "db"
82+
assert span["subtype"] == "mysql"
83+
assert span["action"] == "query"
84+
assert "db" in span["context"]
85+
assert span["context"]["db"]["type"] == "sql"
86+
assert span["context"]["db"]["statement"] == query

0 commit comments

Comments
 (0)