Skip to content

Commit a8a3814

Browse files
Sparkyczbeniwohli
andauthored
Add aiomysql instrumentation (#1107)
* Add aiomysql instrumentation * Add destination info to aiomysql instumentation * fix some isort linting issues and deprecation warnings Co-authored-by: Benjamin Wohlwend <[email protected]>
1 parent 8e246ac commit a8a3814

File tree

10 files changed

+202
-1
lines changed

10 files changed

+202
-1
lines changed

.ci/.jenkins_exclude.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,10 @@ exclude:
228228
FRAMEWORK: aioredis-newest
229229
- PYTHON_VERSION: python-3.10-rc
230230
FRAMEWORK: httpx-0.12
231+
# aiomysql
232+
- PYTHON_VERSION: pypy-3
233+
FRAMEWORK: aiomysql-newest
234+
- PYTHON_VERSION: python-3.6
235+
FRAMEWORK: aiomysql-newest
236+
- PYTHON_VERSION: python-3.10-rc # getting "loop argument must agree with lock" error
237+
FRAMEWORK: aiomysql-newest

.ci/.jenkins_framework.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ FRAMEWORK:
4646
- httpx-newest
4747
- httplib2-newest
4848
- prometheus_client-newest
49+
- aiomysql-newest

docs/supported-technologies.asciidoc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,20 @@ Collected trace data:
197197

198198
* parametrized SQL query
199199

200+
[float]
201+
[[automatic-instrumentation-db-aiomysql]]
202+
==== aiomysql
203+
204+
Library: `aiomysql`
205+
206+
Instrumented methods:
207+
208+
* `aiomysql.cursors.Cursor.execute`
209+
210+
Collected trace data:
211+
212+
* parametrized SQL query
213+
200214
[float]
201215
[[automatic-instrumentation-db-postgres]]
202216
==== PostgreSQL
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.contrib.asyncio.traces import async_capture_span
32+
from elasticapm.instrumentation.packages.asyncio.base import AsyncAbstractInstrumentedModule
33+
from elasticapm.instrumentation.packages.dbapi2 import extract_signature
34+
from elasticapm.utils.encoding import shorten
35+
36+
37+
class AioMySQLInstrumentation(AsyncAbstractInstrumentedModule):
38+
name = "aiomysql"
39+
40+
instrument_list = [("aiomysql.cursors", "Cursor.execute")]
41+
42+
async def call(self, module, method, wrapped, instance, args, kwargs):
43+
if method == "Cursor.execute":
44+
query = args[0]
45+
name = extract_signature(query)
46+
47+
# Truncate sql_string to 10000 characters to prevent large queries from
48+
# causing an error to APM server.
49+
query = shorten(query, string_length=10000)
50+
51+
context = {
52+
"db": {"type": "sql", "statement": query},
53+
"destination": {
54+
"address": instance.connection.host,
55+
"port": instance.connection.port,
56+
"service": {"name": "mysql", "resource": "mysql", "type": "db"},
57+
},
58+
}
59+
action = "query"
60+
else:
61+
raise AssertionError("call from uninstrumented method")
62+
63+
async with async_capture_span(
64+
name, leaf=True, span_type="db", span_subtype="mysql", span_action=action, extra=context
65+
):
66+
return await wrapped(*args, **kwargs)

elasticapm/instrumentation/packages/asyncio/aioredis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030

3131
from __future__ import absolute_import
3232

33+
from elasticapm.contrib.asyncio.traces import async_capture_span
3334
from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
3435
from elasticapm.traces import execution_context
35-
from elasticapm.contrib.asyncio.traces import async_capture_span
3636

3737

3838
class RedisConnectionPoolInstrumentation(AbstractInstrumentedModule):

elasticapm/instrumentation/register.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"elasticapm.instrumentation.packages.asyncio.aioredis.RedisConnectionPoolInstrumentation",
8383
"elasticapm.instrumentation.packages.asyncio.aioredis.RedisPipelineInstrumentation",
8484
"elasticapm.instrumentation.packages.asyncio.aioredis.RedisConnectionInstrumentation",
85+
"elasticapm.instrumentation.packages.asyncio.aiomysql.AioMySQLInstrumentation",
8586
]
8687
)
8788

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ markers =
145145
pyodbc
146146
aiohttp
147147
aiopg
148+
aiomysql
148149
asyncpg
149150
tornado
150151
starlette
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
import os
32+
33+
import pytest
34+
35+
from elasticapm.conf.constants import TRANSACTION
36+
from elasticapm.utils import default_ports
37+
38+
aiomysql = pytest.importorskip("aiomysql")
39+
40+
pytestmark = [pytest.mark.asyncio, pytest.mark.aiomysql]
41+
42+
43+
if "MYSQL_HOST" not in os.environ:
44+
pytestmark.append(pytest.mark.skip("Skipping aiomysql tests, no MYSQL_HOST environment variable set"))
45+
46+
47+
@pytest.fixture(scope="function")
48+
async def aiomysql_connection(request, event_loop):
49+
assert event_loop.is_running()
50+
pool = await aiomysql.create_pool(
51+
host=os.environ.get("MYSQL_HOST", "localhost"),
52+
user=os.environ.get("MYSQL_USER", "eapm"),
53+
password=os.environ.get("MYSQL_PASSWORD", ""),
54+
db=os.environ.get("MYSQL_DATABASE", "eapm_tests"),
55+
loop=event_loop,
56+
)
57+
58+
try:
59+
async with pool.acquire() as conn:
60+
async with conn.cursor() as cursor:
61+
await cursor.execute("CREATE TABLE `test` (`id` INT, `name` VARCHAR(5))")
62+
await cursor.execute("INSERT INTO `test` (`id`, `name`) VALUES (1, 'one'), (2, 'two'), (3, 'three')")
63+
64+
yield conn
65+
66+
finally:
67+
# Drop the testing table and close the connection pool after testcase
68+
async with pool.acquire() as conn:
69+
async with conn.cursor() as cursor:
70+
await cursor.execute("DROP TABLE `test`")
71+
72+
pool.close()
73+
await pool.wait_closed()
74+
75+
76+
@pytest.mark.integrationtest
77+
async def test_aiomysql_select(instrument, aiomysql_connection, elasticapm_client):
78+
try:
79+
elasticapm_client.begin_transaction("web.django")
80+
81+
async with aiomysql_connection.cursor() as cursor:
82+
query = "SELECT * FROM test WHERE `name` LIKE 't%' ORDER BY id"
83+
await cursor.execute(query)
84+
assert await cursor.fetchall() == ((2, "two"), (3, "three"))
85+
86+
elasticapm_client.end_transaction(None, "test-transaction")
87+
finally:
88+
transactions = elasticapm_client.events[TRANSACTION]
89+
spans = elasticapm_client.spans_for_transaction(transactions[0])
90+
span = spans[0]
91+
assert span["name"] == "SELECT FROM test"
92+
assert span["type"] == "db"
93+
assert span["subtype"] == "mysql"
94+
assert span["action"] == "query"
95+
assert "db" in span["context"]
96+
assert span["context"]["db"]["type"] == "sql"
97+
assert span["context"]["db"]["statement"] == query
98+
assert span["context"]["destination"] == {
99+
"address": os.environ.get("MYSQL_HOST", "localhost"),
100+
"port": default_ports.get("mysql"),
101+
"service": {"name": "mysql", "resource": "mysql", "type": "db"},
102+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
aiomysql
2+
-r reqs-base.txt

tests/scripts/envs/aiomysql.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export PYTEST_MARKER="-m aiomysql"
2+
export DOCKER_DEPS="mysql"
3+
export MYSQL_HOST="mysql"
4+
export MYSQL_USER="eapm"
5+
export MYSQL_PASSWORD="Very(!)Secure"
6+
export WAIT_FOR_HOST="mysql"
7+
export WAIT_FOR_PORT=3306

0 commit comments

Comments
 (0)