Skip to content

Commit bc9e29a

Browse files
yoav-orcatim-schilling
authored andcommitted
Instrument aioredis
Rebuild of #552 with tracking in an aioredis-specific instrument submodule.
1 parent 32d0df1 commit bc9e29a

File tree

5 files changed

+285
-0
lines changed

5 files changed

+285
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# coding=utf-8
2+
from __future__ import absolute_import, division, print_function, unicode_literals
3+
4+
import wrapt
5+
6+
from scout_apm.core.tracked_request import TrackedRequest
7+
8+
9+
@wrapt.decorator
10+
async def wrapped_redis_execute(wrapped, instance, args, kwargs):
11+
try:
12+
op = args[0]
13+
if isinstance(op, bytes):
14+
op = op.decode()
15+
except (IndexError, TypeError):
16+
op = "Unknown"
17+
18+
tracked_request = TrackedRequest.instance()
19+
tracked_request.start_span(operation="Redis/{}".format(op))
20+
21+
try:
22+
return await wrapped(*args, **kwargs)
23+
finally:
24+
tracked_request.stop_span()
25+
26+
27+
@wrapt.decorator
28+
async def wrapped_pipeline_execute(wrapped, instance, args, kwargs):
29+
tracked_request = TrackedRequest.instance()
30+
tracked_request.start_span(operation="Redis/MULTI")
31+
32+
try:
33+
return await wrapped(*args, **kwargs)
34+
finally:
35+
tracked_request.stop_span()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# coding=utf-8
2+
from __future__ import absolute_import, division, print_function, unicode_literals
3+
4+
import logging
5+
6+
try:
7+
from aioredis import Redis
8+
except ImportError: # pragma: no cover
9+
Redis = None
10+
11+
try:
12+
from aioredis.commands import Pipeline
13+
except ImportError:
14+
Pipeline = None
15+
16+
# The async_ module can only be shipped on Python 3.6+
17+
try:
18+
from scout_apm.async_.instruments.aioredis import (
19+
wrapped_redis_execute,
20+
wrapped_pipeline_execute,
21+
)
22+
except ImportError:
23+
wrapped_redis_execute = None
24+
wrapped_pipeline_execute = None
25+
26+
logger = logging.getLogger(__name__)
27+
28+
have_patched_redis_execute = False
29+
have_patched_pipeline_execute = False
30+
31+
32+
def ensure_installed():
33+
global have_patched_redis_execute, have_patched_pipeline_execute
34+
35+
logger.debug("Instrumenting aioredis.")
36+
37+
if Redis is None:
38+
logger.debug("Couldn't import aioredis.Redis - probably not installed.")
39+
elif wrapped_redis_execute is None:
40+
logger.debug(
41+
"Couldn't import scout_apm.async_.instruments.aioredis - probably using Python < 3.6."
42+
)
43+
elif not have_patched_redis_execute:
44+
try:
45+
Redis.execute = wrapped_redis_execute(Redis.execute)
46+
except Exception as exc:
47+
logger.warning(
48+
"Failed to instrument aioredis.Redis.execute: %r",
49+
exc,
50+
exc_info=exc,
51+
)
52+
else:
53+
have_patched_redis_execute = True
54+
55+
if Pipeline is None:
56+
logger.debug(
57+
"Couldn't import aioredis.commands.Pipeline - probably not installed."
58+
)
59+
elif wrapped_pipeline_execute is None:
60+
logger.debug(
61+
"Couldn't import scout_apm.async_.instruments.aioredis - probably using Python < 3.6."
62+
)
63+
elif not have_patched_pipeline_execute:
64+
try:
65+
Pipeline.execute = wrapped_pipeline_execute(Pipeline.execute)
66+
except Exception as exc:
67+
logger.warning(
68+
"Failed to instrument aioredis.commands.Pipeline.execute: %r",
69+
exc,
70+
exc_info=exc,
71+
)
72+
else:
73+
have_patched_pipeline_execute = True

src/scout_apm/instruments/redis.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
from redis import StrictRedis as Redis
2020
from redis.client import BasePipeline as Pipeline
2121

22+
try:
23+
from scout_apm.async_.instruments.redis import ensure_async_installed
24+
except ImportError:
25+
ensure_async_installed = None
26+
2227
logger = logging.getLogger(__name__)
2328

2429

@@ -56,6 +61,9 @@ def ensure_installed():
5661
else:
5762
have_patched_pipeline_execute = True
5863

64+
if ensure_async_installed is not None:
65+
ensure_async_installed()
66+
5967
return True
6068

6169

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# coding=utf-8
2+
from __future__ import absolute_import, division, print_function, unicode_literals
3+
4+
import logging
5+
import os
6+
7+
import aioredis
8+
import pytest
9+
10+
from scout_apm.instruments.aioredis import ensure_installed
11+
from tests.compat import mock
12+
from tests.tools import async_test
13+
14+
15+
async def get_redis_conn():
16+
ensure_installed()
17+
# e.g. export REDIS_URL="redis://localhost:6379/0"
18+
if "REDIS_URL" not in os.environ:
19+
raise pytest.skip("Redis isn't available")
20+
conn = await aioredis.create_connection(os.environ["REDIS_URL"])
21+
return aioredis.Redis(conn)
22+
23+
24+
def test_ensure_installed_twice(caplog):
25+
ensure_installed()
26+
ensure_installed()
27+
28+
assert caplog.record_tuples == 2 * [
29+
(
30+
"scout_apm.instruments.aioredis",
31+
logging.DEBUG,
32+
"Instrumenting aioredis.",
33+
)
34+
]
35+
36+
37+
def test_ensure_installed_fail_no_redis_execute(caplog):
38+
mock_not_patched = mock.patch(
39+
"scout_apm.instruments.aioredis.have_patched_redis_execute", new=False
40+
)
41+
mock_redis = mock.patch("scout_apm.instruments.aioredis.Redis")
42+
with mock_not_patched, mock_redis as mocked_redis:
43+
del mocked_redis.execute
44+
45+
ensure_installed()
46+
47+
assert len(caplog.record_tuples) == 2
48+
assert caplog.record_tuples[0] == (
49+
"scout_apm.instruments.aioredis",
50+
logging.DEBUG,
51+
"Instrumenting aioredis.",
52+
)
53+
logger, level, message = caplog.record_tuples[1]
54+
assert logger == "scout_apm.instruments.aioredis"
55+
assert level == logging.WARNING
56+
assert message.startswith(
57+
"Failed to instrument aioredis.Redis.execute: AttributeError"
58+
)
59+
60+
61+
def test_ensure_installed_fail_no_wrapped_redis_execute(caplog):
62+
mock_not_patched = mock.patch(
63+
"scout_apm.instruments.aioredis.have_patched_redis_execute", new=False
64+
)
65+
mock_wrapped_redis_execute = mock.patch(
66+
"scout_apm.instruments.aioredis.wrapped_redis_execute", new=None
67+
)
68+
with mock_not_patched, mock_wrapped_redis_execute:
69+
ensure_installed()
70+
71+
assert len(caplog.record_tuples) == 2
72+
assert caplog.record_tuples[0] == (
73+
"scout_apm.instruments.aioredis",
74+
logging.DEBUG,
75+
"Instrumenting aioredis.",
76+
)
77+
assert caplog.record_tuples[1] == (
78+
"scout_apm.instruments.aioredis",
79+
logging.DEBUG,
80+
"Couldn't import scout_apm.async_.instruments.aioredis - probably using Python < 3.6.",
81+
)
82+
83+
84+
def test_ensure_installed_fail_no_pipeline_execute(caplog):
85+
mock_not_patched = mock.patch(
86+
"scout_apm.instruments.aioredis.have_patched_pipeline_execute", new=False
87+
)
88+
mock_pipeline = mock.patch("scout_apm.instruments.aioredis.Pipeline")
89+
with mock_not_patched, mock_pipeline as mocked_pipeline:
90+
del mocked_pipeline.execute
91+
92+
ensure_installed()
93+
94+
assert len(caplog.record_tuples) == 2
95+
assert caplog.record_tuples[0] == (
96+
"scout_apm.instruments.aioredis",
97+
logging.DEBUG,
98+
"Instrumenting aioredis.",
99+
)
100+
logger, level, message = caplog.record_tuples[1]
101+
assert logger == "scout_apm.instruments.aioredis"
102+
assert level == logging.WARNING
103+
assert message.startswith(
104+
"Failed to instrument aioredis.commands.Pipeline.execute: AttributeError"
105+
)
106+
107+
108+
def test_ensure_installed_fail_no_wrapped_pipeline_execute(caplog):
109+
mock_not_patched = mock.patch(
110+
"scout_apm.instruments.aioredis.have_patched_pipeline_execute", new=False
111+
)
112+
mock_wrapped_pipeline_execute = mock.patch(
113+
"scout_apm.instruments.aioredis.wrapped_pipeline_execute", new=None
114+
)
115+
with mock_not_patched, mock_wrapped_pipeline_execute:
116+
ensure_installed()
117+
118+
assert len(caplog.record_tuples) == 2
119+
assert caplog.record_tuples[0] == (
120+
"scout_apm.instruments.aioredis",
121+
logging.DEBUG,
122+
"Instrumenting aioredis.",
123+
)
124+
assert caplog.record_tuples[1] == (
125+
"scout_apm.instruments.aioredis",
126+
logging.DEBUG,
127+
"Couldn't import scout_apm.async_.instruments.aioredis - probably using Python < 3.6.",
128+
)
129+
130+
131+
@async_test
132+
async def test_echo(tracked_request):
133+
redis_conn = await get_redis_conn()
134+
135+
await redis_conn.echo("Hello World!")
136+
137+
assert len(tracked_request.complete_spans) == 1
138+
assert tracked_request.complete_spans[0].operation == "Redis/ECHO"
139+
140+
141+
# def test_pipeline_echo(redis_conn, tracked_request):
142+
# with redis_conn.pipeline() as p:
143+
# p.echo("Hello World!")
144+
# p.execute()
145+
146+
# assert len(tracked_request.complete_spans) == 1
147+
# assert tracked_request.complete_spans[0].operation == "Redis/MULTI"
148+
149+
150+
# def test_execute_command_missing_argument(redis_conn, tracked_request):
151+
# # Redis instrumentation doesn't crash if op is missing.
152+
# # This raises a TypeError (Python 3) or IndexError (Python 2)
153+
# # when calling the original method.
154+
# with pytest.raises(IndexError):
155+
# redis_conn.execute_command()
156+
157+
# assert len(tracked_request.complete_spans) == 1
158+
# assert tracked_request.complete_spans[0].operation == "Redis/Unknown"
159+
160+
161+
# def test_perform_request_bad_url(redis_conn, tracked_request):
162+
# with pytest.raises(TypeError):
163+
# # Redis instrumentation doesn't crash if op has the wrong type.
164+
# # This raises a TypeError when calling the original method.
165+
# redis_conn.execute_command(None)
166+
167+
# assert len(tracked_request.complete_spans) == 1
168+
# assert tracked_request.complete_spans[0].operation == "Redis/None"

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ passenv =
1616
MONGODB_URL
1717
REDIS_URL
1818
deps =
19+
aioredis ; python_version >= "3.6"
1920
bottle
2021
cherrypy
2122
celery!=4.4.4 # https://github.com/celery/celery/issues/6153

0 commit comments

Comments
 (0)