Skip to content

Commit e371b02

Browse files
authored
Add redis.asyncio.Connection instrumentation (#919)
* Add async connection instrumentation * Remove unsupported flask tests * Remove old instrumentation from coverage analysis * [Mega-Linter] Apply linters fixes * Trigger tests --------- Co-authored-by: lrafeei <[email protected]>
1 parent b1be563 commit e371b02

File tree

5 files changed

+140
-30
lines changed

5 files changed

+140
-30
lines changed

newrelic/config.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2760,20 +2760,6 @@ def _process_module_builtin_defaults():
27602760
"aioredis.connection", "newrelic.hooks.datastore_aioredis", "instrument_aioredis_connection"
27612761
)
27622762

2763-
# Redis v4.2+
2764-
_process_module_definition(
2765-
"redis.asyncio.client", "newrelic.hooks.datastore_redis", "instrument_asyncio_redis_client"
2766-
)
2767-
2768-
# Redis v4.2+
2769-
_process_module_definition(
2770-
"redis.asyncio.commands", "newrelic.hooks.datastore_redis", "instrument_asyncio_redis_client"
2771-
)
2772-
2773-
_process_module_definition(
2774-
"redis.asyncio.connection", "newrelic.hooks.datastore_aioredis", "instrument_aioredis_connection"
2775-
)
2776-
27772763
# v7 and below
27782764
_process_module_definition(
27792765
"elasticsearch.client",
@@ -2930,6 +2916,21 @@ def _process_module_builtin_defaults():
29302916
"instrument_pymongo_collection",
29312917
)
29322918

2919+
# Redis v4.2+
2920+
_process_module_definition(
2921+
"redis.asyncio.client", "newrelic.hooks.datastore_redis", "instrument_asyncio_redis_client"
2922+
)
2923+
2924+
# Redis v4.2+
2925+
_process_module_definition(
2926+
"redis.asyncio.commands", "newrelic.hooks.datastore_redis", "instrument_asyncio_redis_client"
2927+
)
2928+
2929+
# Redis v4.2+
2930+
_process_module_definition(
2931+
"redis.asyncio.connection", "newrelic.hooks.datastore_redis", "instrument_asyncio_redis_connection"
2932+
)
2933+
29332934
_process_module_definition(
29342935
"redis.connection",
29352936
"newrelic.hooks.datastore_redis",

newrelic/hooks/datastore_redis.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
import re
1616

1717
from newrelic.api.datastore_trace import DatastoreTrace
18+
from newrelic.api.time_trace import current_trace
1819
from newrelic.api.transaction import current_transaction
1920
from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper
2021

21-
2222
_redis_client_sync_methods = {
2323
"acl_dryrun",
2424
"auth",
@@ -545,6 +545,59 @@ def _nr_wrapper_asyncio_Redis_method_(wrapped, instance, args, kwargs):
545545
wrap_function_wrapper(module, name, _nr_wrapper_asyncio_Redis_method_)
546546

547547

548+
async def wrap_async_Connection_send_command(wrapped, instance, args, kwargs):
549+
transaction = current_transaction()
550+
if not transaction:
551+
return await wrapped(*args, **kwargs)
552+
553+
host, port_path_or_id, db = (None, None, None)
554+
555+
try:
556+
dt = transaction.settings.datastore_tracer
557+
if dt.instance_reporting.enabled or dt.database_name_reporting.enabled:
558+
conn_kwargs = _conn_attrs_to_dict(instance)
559+
host, port_path_or_id, db = _instance_info(conn_kwargs)
560+
except Exception:
561+
pass
562+
563+
# Older Redis clients would when sending multi part commands pass
564+
# them in as separate arguments to send_command(). Need to therefore
565+
# detect those and grab the next argument from the set of arguments.
566+
567+
operation = args[0].strip().lower()
568+
569+
# If it's not a multi part command, there's no need to trace it, so
570+
# we can return early.
571+
572+
if (
573+
operation.split()[0] not in _redis_multipart_commands
574+
): # Set the datastore info on the DatastoreTrace containing this function call.
575+
trace = current_trace()
576+
577+
# Find DatastoreTrace no matter how many other traces are inbetween
578+
while trace is not None and not isinstance(trace, DatastoreTrace):
579+
trace = getattr(trace, "parent", None)
580+
581+
if trace is not None:
582+
trace.host = host
583+
trace.port_path_or_id = port_path_or_id
584+
trace.database_name = db
585+
586+
return await wrapped(*args, **kwargs)
587+
588+
# Convert multi args to single arg string
589+
590+
if operation in _redis_multipart_commands and len(args) > 1:
591+
operation = "%s %s" % (operation, args[1].strip().lower())
592+
593+
operation = _redis_operation_re.sub("_", operation)
594+
595+
with DatastoreTrace(
596+
product="Redis", target=None, operation=operation, host=host, port_path_or_id=port_path_or_id, database_name=db
597+
):
598+
return await wrapped(*args, **kwargs)
599+
600+
548601
def _nr_Connection_send_command_wrapper_(wrapped, instance, args, kwargs):
549602
transaction = current_transaction()
550603

@@ -613,6 +666,7 @@ def instrument_asyncio_redis_client(module):
613666
if hasattr(class_, operation):
614667
_wrap_asyncio_Redis_method_wrapper(module, "Redis", operation)
615668

669+
616670
def instrument_redis_commands_core(module):
617671
_instrument_redis_commands_module(module, "CoreCommands")
618672

@@ -658,4 +712,12 @@ def _instrument_redis_commands_module(module, class_name):
658712

659713

660714
def instrument_redis_connection(module):
661-
wrap_function_wrapper(module, "Connection.send_command", _nr_Connection_send_command_wrapper_)
715+
if hasattr(module, "Connection"):
716+
if hasattr(module.Connection, "send_command"):
717+
wrap_function_wrapper(module, "Connection.send_command", _nr_Connection_send_command_wrapper_)
718+
719+
720+
def instrument_asyncio_redis_connection(module):
721+
if hasattr(module, "Connection"):
722+
if hasattr(module.Connection, "send_command"):
723+
wrap_function_wrapper(module, "Connection.send_command", wrap_async_Connection_send_command)

newrelic/hooks/framework_flask.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def _nr_wrapper_error_handler_(wrapped, instance, args, kwargs):
166166
return FunctionTraceWrapper(wrapped, name=name)(*args, **kwargs)
167167

168168

169-
def _nr_wrapper_Flask__register_error_handler_(wrapped, instance, args, kwargs):
169+
def _nr_wrapper_Flask__register_error_handler_(wrapped, instance, args, kwargs): # pragma: no cover
170170
def _bind_params(key, code_or_exception, f):
171171
return key, code_or_exception, f
172172

@@ -189,7 +189,6 @@ def _bind_params(code_or_exception, f):
189189

190190

191191
def _nr_wrapper_Flask_try_trigger_before_first_request_functions_(wrapped, instance, args, kwargs):
192-
193192
transaction = current_transaction()
194193

195194
if transaction is None:
@@ -355,7 +354,6 @@ def _nr_wrapper_Blueprint_endpoint_(wrapped, instance, args, kwargs):
355354

356355
@function_wrapper
357356
def _nr_wrapper_Blueprint_before_request_wrapped_(wrapped, instance, args, kwargs):
358-
359357
transaction = current_transaction()
360358

361359
if transaction is None:

tests/datastore_redis/test_asyncio.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,53 @@
3030
DB_SETTINGS = redis_settings()[0]
3131
REDIS_PY_VERSION = get_package_version_tuple("redis")
3232

33-
# Metrics
33+
# Metrics for publish test
34+
35+
datastore_all_metric_count = 5 if REDIS_PY_VERSION >= (5, 0) else 3
3436

3537
_base_scoped_metrics = [("Datastore/operation/Redis/publish", 3)]
3638

3739
if REDIS_PY_VERSION >= (5, 0):
38-
_base_scoped_metrics.append(('Datastore/operation/Redis/client_setinfo', 2),)
39-
40-
datastore_all_metric_count = 5 if REDIS_PY_VERSION >= (5, 0) else 3
40+
_base_scoped_metrics.append(
41+
("Datastore/operation/Redis/client_setinfo", 2),
42+
)
4143

4244
_base_rollup_metrics = [
4345
("Datastore/all", datastore_all_metric_count),
4446
("Datastore/allOther", datastore_all_metric_count),
4547
("Datastore/Redis/all", datastore_all_metric_count),
4648
("Datastore/Redis/allOther", datastore_all_metric_count),
4749
("Datastore/operation/Redis/publish", 3),
48-
("Datastore/instance/Redis/%s/%s" % (instance_hostname(DB_SETTINGS["host"]), DB_SETTINGS["port"]), datastore_all_metric_count),
50+
(
51+
"Datastore/instance/Redis/%s/%s" % (instance_hostname(DB_SETTINGS["host"]), DB_SETTINGS["port"]),
52+
datastore_all_metric_count,
53+
),
4954
]
5055
if REDIS_PY_VERSION >= (5, 0):
51-
_base_rollup_metrics.append(('Datastore/operation/Redis/client_setinfo', 2),)
56+
_base_rollup_metrics.append(
57+
("Datastore/operation/Redis/client_setinfo", 2),
58+
)
59+
60+
61+
# Metrics for connection pool test
62+
63+
_base_pool_scoped_metrics = [
64+
("Datastore/operation/Redis/get", 1),
65+
("Datastore/operation/Redis/set", 1),
66+
("Datastore/operation/Redis/client_list", 1),
67+
]
68+
69+
_base_pool_rollup_metrics = [
70+
("Datastore/all", 3),
71+
("Datastore/allOther", 3),
72+
("Datastore/Redis/all", 3),
73+
("Datastore/Redis/allOther", 3),
74+
("Datastore/operation/Redis/get", 1),
75+
("Datastore/operation/Redis/set", 1),
76+
("Datastore/operation/Redis/client_list", 1),
77+
("Datastore/instance/Redis/%s/%s" % (instance_hostname(DB_SETTINGS["host"]), DB_SETTINGS["port"]), 3),
78+
]
79+
5280

5381
# Tests
5482

@@ -60,6 +88,31 @@ def client(loop): # noqa
6088
return loop.run_until_complete(redis.asyncio.Redis(host=DB_SETTINGS["host"], port=DB_SETTINGS["port"], db=0))
6189

6290

91+
@pytest.fixture()
92+
def client_pool(loop): # noqa
93+
import redis.asyncio
94+
95+
connection_pool = redis.asyncio.ConnectionPool(host=DB_SETTINGS["host"], port=DB_SETTINGS["port"], db=0)
96+
return loop.run_until_complete(redis.asyncio.Redis(connection_pool=connection_pool))
97+
98+
99+
@pytest.mark.skipif(REDIS_PY_VERSION < (4, 2), reason="This functionality exists in Redis 4.2+")
100+
@validate_transaction_metrics(
101+
"test_asyncio:test_async_connection_pool",
102+
scoped_metrics=_base_pool_scoped_metrics,
103+
rollup_metrics=_base_pool_rollup_metrics,
104+
background_task=True,
105+
)
106+
@background_task()
107+
def test_async_connection_pool(client_pool, loop): # noqa
108+
async def _test_async_pool(client_pool):
109+
await client_pool.set("key1", "value1")
110+
await client_pool.get("key1")
111+
await client_pool.execute_command("CLIENT", "LIST")
112+
113+
loop.run_until_complete(_test_async_pool(client_pool))
114+
115+
63116
@pytest.mark.skipif(REDIS_PY_VERSION < (4, 2), reason="This functionality exists in Redis 4.2+")
64117
@validate_transaction_metrics("test_asyncio:test_async_pipeline", background_task=True)
65118
@background_task()
@@ -104,4 +157,4 @@ async def _test_pubsub():
104157
await future
105158

106159
loop.run_until_complete(_test_pubsub())
107-
assert messages_received == ["Hello", "World", "NOPE"]
160+
assert messages_received == ["Hello", "World", "NOPE"]

tox.ini

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,6 @@ envlist =
128128
# Falcon master branch failing on 3.11 currently.
129129
python-framework_falcon-py311-falcon0200,
130130
python-framework_fastapi-{py37,py38,py39,py310,py311},
131-
python-framework_flask-{pypy27,py27}-flask0012,
132-
python-framework_flask-{pypy27,py27,py37,py38,py39,py310,py311,pypy38}-flask0101,
133131
; temporarily disabling flaskmaster tests
134132
python-framework_flask-{py37,py38,py39,py310,py311,pypy38}-flasklatest,
135133
python-framework_graphene-{py37,py38,py39,py310,py311}-graphenelatest,
@@ -302,8 +300,6 @@ deps =
302300
framework_flask: markupsafe<2.1
303301
framework_flask: jinja2<3.1
304302
framework_flask: Flask-Compress
305-
framework_flask-flask0012: flask<0.13
306-
framework_flask-flask0101: flask<1.2
307303
framework_flask-flasklatest: flask[async]
308304
framework_flask-flaskmaster: https://github.com/pallets/werkzeug/archive/main.zip
309305
framework_flask-flaskmaster: https://github.com/pallets/flask/archive/main.zip#egg=flask[async]

0 commit comments

Comments
 (0)