Skip to content

Commit 7205269

Browse files
Sparkyczbeniwohli
andauthored
Add aioredis instrumentation (#1082)
* Add aioredis instrumentation * fix code formating by black Co-authored-by: Benjamin Wohlwend <[email protected]> * add boilerplate for CI test matrix * Use async_capture_span instead of capture_span in aioredis instrumentation * Blacking.. Co-authored-by: Benjamin Wohlwend <[email protected]>
1 parent 4d125e6 commit 7205269

File tree

10 files changed

+255
-0
lines changed

10 files changed

+255
-0
lines changed

.ci/.jenkins_exclude.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,11 @@ exclude:
220220
FRAMEWORK: asyncpg-newest
221221
- PYTHON_VERSION: python-3.10-rc # https://github.com/MagicStack/asyncpg/issues/699
222222
FRAMEWORK: asyncpg-newest
223+
- PYTHON_VERSION: pypy-3
224+
FRAMEWORK: aioredis-newest
225+
- PYTHON_VERSION: python-3.6
226+
FRAMEWORK: aioredis-newest
227+
- PYTHON_VERSION: python-3.10-rc # getting "loop argument must agree with lock" error
228+
FRAMEWORK: aioredis-newest
223229
- PYTHON_VERSION: python-3.10-rc
224230
FRAMEWORK: httpx-0.12

.ci/.jenkins_framework.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ FRAMEWORK:
1818
- boto3-newest
1919
- pymongo-newest
2020
- redis-newest
21+
- aioredis-newest
22+
#- aioredis-2 # not supported yet
2123
- psycopg2-newest
2224
- pymssql-newest
2325
- pyodbc-newest

docs/supported-technologies.asciidoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,22 @@ Collected trace data:
338338
* Redis command name
339339

340340

341+
[float]
342+
[[automatic-instrumentation-db-aioredis]]
343+
==== aioredis
344+
345+
Library: `aioredis` (`<2.0`)
346+
347+
Instrumented methods:
348+
349+
* `aioredis.pool.ConnectionsPool.execute`
350+
* `aioredis.commands.transaction.Pipeline.execute`
351+
* `aioredis.connection.RedisConnection.execute`
352+
353+
Collected trace data:
354+
355+
* Redis command name
356+
341357
[float]
342358
[[automatic-instrumentation-db-cassandra]]
343359
==== Cassandra
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 __future__ import absolute_import
32+
33+
from elasticapm.instrumentation.packages.base import AbstractInstrumentedModule
34+
from elasticapm.traces import execution_context
35+
from elasticapm.contrib.asyncio.traces import async_capture_span
36+
37+
38+
class RedisConnectionPoolInstrumentation(AbstractInstrumentedModule):
39+
name = "aioredis"
40+
41+
instrument_list = [("aioredis.pool", "ConnectionsPool.execute")]
42+
43+
def call(self, module, method, wrapped, instance, args, kwargs):
44+
if len(args) > 0:
45+
wrapped_name = args[0].decode()
46+
else:
47+
wrapped_name = self.get_wrapped_name(wrapped, instance, method)
48+
49+
with async_capture_span(
50+
wrapped_name, span_type="db", span_subtype="redis", span_action="query", leaf=True
51+
) as span:
52+
span.context["destination"] = _get_destination_info(instance)
53+
54+
return wrapped(*args, **kwargs)
55+
56+
57+
class RedisPipelineInstrumentation(AbstractInstrumentedModule):
58+
name = "aioredis"
59+
60+
instrument_list = [("aioredis.commands.transaction", "Pipeline.execute")]
61+
62+
def call(self, module, method, wrapped, instance, args, kwargs):
63+
wrapped_name = self.get_wrapped_name(wrapped, instance, method)
64+
65+
with async_capture_span(
66+
wrapped_name, span_type="db", span_subtype="redis", span_action="query", leaf=True
67+
) as span:
68+
span.context["destination"] = _get_destination_info(instance)
69+
70+
return wrapped(*args, **kwargs)
71+
72+
73+
class RedisConnectionInstrumentation(AbstractInstrumentedModule):
74+
name = "aioredis"
75+
76+
instrument_list = (("aioredis.connection", "RedisConnection.execute"),)
77+
78+
def call(self, module, method, wrapped, instance, args, kwargs):
79+
span = execution_context.get_span()
80+
if span and span.subtype == "aioredis":
81+
span.context["destination"] = _get_destination_info(instance)
82+
return wrapped(*args, **kwargs)
83+
84+
85+
def _get_destination_info(connection):
86+
destination_info = {"service": {"name": "aioredis", "resource": "redis", "type": "db"}}
87+
88+
if hasattr(connection, "_pool_or_conn"):
89+
destination_info["port"] = connection._pool_or_conn.address[1]
90+
destination_info["address"] = connection._pool_or_conn.address[0]
91+
else:
92+
destination_info["port"] = connection.address[1]
93+
destination_info["address"] = connection.address[0]
94+
95+
return destination_info

elasticapm/instrumentation/register.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
"elasticapm.instrumentation.packages.tornado.TornadoHandleRequestExceptionInstrumentation",
8080
"elasticapm.instrumentation.packages.tornado.TornadoRenderInstrumentation",
8181
"elasticapm.instrumentation.packages.asyncio.httpcore.HTTPCoreAsyncInstrumentation",
82+
"elasticapm.instrumentation.packages.asyncio.aioredis.RedisConnectionPoolInstrumentation",
83+
"elasticapm.instrumentation.packages.asyncio.aioredis.RedisPipelineInstrumentation",
84+
"elasticapm.instrumentation.packages.asyncio.aioredis.RedisConnectionInstrumentation",
8285
]
8386
)
8487

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ markers =
135135
mongodb
136136
memcached
137137
redis
138+
aioredis
138139
psutil
139140
mysql_connector
140141
pymysql
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 pytest # isort:skip
32+
33+
aioredis = pytest.importorskip("aioredis") # isort:skip
34+
35+
import os
36+
37+
from elasticapm.conf.constants import TRANSACTION
38+
from elasticapm.traces import capture_span
39+
40+
pytestmark = [pytest.mark.asyncio, pytest.mark.aioredis]
41+
42+
if "REDIS_HOST" not in os.environ:
43+
pytestmark.append(pytest.mark.skip("Skipping redis tests, no REDIS_HOST environment variable set"))
44+
45+
46+
@pytest.fixture()
47+
async def redis_conn():
48+
_host = os.environ["REDIS_HOST"]
49+
_port = os.environ.get("REDIS_PORT", 6379)
50+
conn = await aioredis.create_redis_pool(f"redis://{_host}:{_port}")
51+
52+
yield conn
53+
54+
conn.close()
55+
await conn.wait_closed()
56+
57+
58+
@pytest.mark.integrationtest
59+
async def test_pipeline(instrument, elasticapm_client, redis_conn):
60+
elasticapm_client.begin_transaction("transaction.test")
61+
with capture_span("test_pipeline", "test"):
62+
pipeline = redis_conn.pipeline()
63+
pipeline.rpush("mykey", "a", "b")
64+
pipeline.expire("mykey", 1000)
65+
await pipeline.execute()
66+
elasticapm_client.end_transaction("MyView")
67+
68+
transactions = elasticapm_client.events[TRANSACTION]
69+
spans = elasticapm_client.spans_for_transaction(transactions[0])
70+
71+
assert spans[0]["name"] in ("StrictPipeline.execute", "Pipeline.execute")
72+
assert spans[0]["type"] == "db"
73+
assert spans[0]["subtype"] == "redis"
74+
assert spans[0]["action"] == "query"
75+
assert spans[0]["context"]["destination"] == {
76+
"address": os.environ.get("REDIS_HOST", "localhost"),
77+
"port": int(os.environ.get("REDIS_PORT", 6379)),
78+
"service": {"name": "aioredis", "resource": "redis", "type": "db"},
79+
}
80+
81+
assert spans[1]["name"] == "test_pipeline"
82+
assert spans[1]["type"] == "test"
83+
84+
assert len(spans) == 2
85+
86+
87+
@pytest.mark.integrationtest
88+
async def test_redis_client(instrument, elasticapm_client, redis_conn):
89+
elasticapm_client.begin_transaction("transaction.test")
90+
with capture_span("test_redis_client", "test"):
91+
await redis_conn.rpush("mykey", "a", "b")
92+
await redis_conn.expire("mykey", 1000)
93+
elasticapm_client.end_transaction("MyView")
94+
95+
transactions = elasticapm_client.events[TRANSACTION]
96+
spans = elasticapm_client.spans_for_transaction(transactions[0])
97+
98+
spans = sorted(spans, key=lambda x: x["name"])
99+
100+
assert {t["name"] for t in spans} == {"test_redis_client", "RPUSH", "EXPIRE"}
101+
102+
assert spans[0]["name"] == "EXPIRE"
103+
assert spans[0]["type"] == "db"
104+
assert spans[0]["subtype"] == "redis"
105+
assert spans[0]["action"] == "query"
106+
assert spans[0]["context"]["destination"] == {
107+
"address": os.environ.get("REDIS_HOST", "localhost"),
108+
"port": int(os.environ.get("REDIS_PORT", 6379)),
109+
"service": {"name": "aioredis", "resource": "redis", "type": "db"},
110+
}
111+
112+
assert spans[1]["name"] == "RPUSH"
113+
assert spans[1]["type"] == "db"
114+
assert spans[1]["subtype"] == "redis"
115+
assert spans[1]["action"] == "query"
116+
assert spans[1]["context"]["destination"] == {
117+
"address": os.environ.get("REDIS_HOST", "localhost"),
118+
"port": int(os.environ.get("REDIS_PORT", 6379)),
119+
"service": {"name": "aioredis", "resource": "redis", "type": "db"},
120+
}
121+
122+
assert spans[2]["name"] == "test_redis_client"
123+
assert spans[2]["type"] == "test"
124+
125+
assert len(spans) == 3
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
aioredis>=2.0.0a1
2+
-r reqs-base.txt
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
aioredis<2
2+
-r reqs-base.txt

tests/scripts/envs/aioredis.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export PYTEST_MARKER="-m aioredis"
2+
export DOCKER_DEPS="redis"
3+
export REDIS_HOST="redis"

0 commit comments

Comments
 (0)