Skip to content

Commit 6295f25

Browse files
espain16TimPansinohannahramadanlrafeeiumaannamalai
committed
Aredis connection wrapper (#487)
* Initial aredis setup Co-authored-by: Hannah Ramadan <[email protected]> Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: CharlyCDeF <[email protected]> Co-authored-by: Elise <[email protected]> * Add connection wrappers * Update newrelic/hooks/datastore_aredis.py Co-authored-by: Timothy Pansino <[email protected]> * Update test_execute_command.py file Removed an unnecessary breakpoint Co-authored-by: Tim Pansino <[email protected]> Co-authored-by: Hannah Ramadan <[email protected]> Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: CharlyCDeF <[email protected]> Co-authored-by: Elise <[email protected]> Co-authored-by: Timothy Pansino <[email protected]>
1 parent 3d58d28 commit 6295f25

File tree

5 files changed

+382
-0
lines changed

5 files changed

+382
-0
lines changed

newrelic/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2547,6 +2547,12 @@ def _process_module_builtin_defaults():
25472547

25482548
_process_module_definition("solr", "newrelic.hooks.datastore_solrpy", "instrument_solrpy")
25492549

2550+
_process_module_definition(
2551+
"aredis.connection",
2552+
"newrelic.hooks.datastore_aredis",
2553+
"instrument_aredis_connection",
2554+
)
2555+
25502556
_process_module_definition(
25512557
"elasticsearch.client",
25522558
"newrelic.hooks.datastore_elasticsearch",

newrelic/hooks/datastore_aredis.py

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import re
16+
17+
from newrelic.api.datastore_trace import DatastoreTrace
18+
from newrelic.api.transaction import current_transaction
19+
from newrelic.common.object_wrapper import wrap_function_wrapper
20+
21+
_redis_client_methods = (
22+
"bgrewriteaof",
23+
"bgsave",
24+
"client_kill",
25+
"client_list",
26+
"client_getname",
27+
"client_setname",
28+
"config_get",
29+
"config_set",
30+
"config_resetstat",
31+
"config_rewrite",
32+
"dbsize",
33+
"debug_object",
34+
"echo",
35+
"flushall",
36+
"flushdb",
37+
"info",
38+
"lastsave",
39+
"object",
40+
"ping",
41+
"save",
42+
"sentinel",
43+
"sentinel_get_master_addr_by_name",
44+
"sentinel_master",
45+
"sentinel_masters",
46+
"sentinel_monitor",
47+
"sentinel_remove",
48+
"sentinel_sentinels",
49+
"sentinel_set",
50+
"sentinel_slaves",
51+
"shutdown",
52+
"slaveof",
53+
"slowlog_get",
54+
"slowlog_reset",
55+
"time",
56+
"append",
57+
"bitcount",
58+
"bitop",
59+
"bitpos",
60+
"decr",
61+
"delete",
62+
"dump",
63+
"exists",
64+
"expire",
65+
"expireat",
66+
"get",
67+
"getbit",
68+
"getrange",
69+
"getset",
70+
"incr",
71+
"incrby",
72+
"incrbyfloat",
73+
"keys",
74+
"mget",
75+
"mset",
76+
"msetnx",
77+
"move",
78+
"persist",
79+
"pexpire",
80+
"pexpireat",
81+
"psetex",
82+
"pttl",
83+
"randomkey",
84+
"rename",
85+
"renamenx",
86+
"restore",
87+
"set",
88+
"setbit",
89+
"setex",
90+
"setnx",
91+
"setrange",
92+
"strlen",
93+
"substr",
94+
"ttl",
95+
"type",
96+
"watch",
97+
"unwatch",
98+
"blpop",
99+
"brpop",
100+
"brpoplpush",
101+
"lindex",
102+
"linsert",
103+
"llen",
104+
"lpop",
105+
"lpush",
106+
"lpushx",
107+
"lrange",
108+
"lrem",
109+
"lset",
110+
"ltrim",
111+
"rpop",
112+
"rpoplpush",
113+
"rpush",
114+
"rpushx",
115+
"sort",
116+
"scan",
117+
"scan_iter",
118+
"sscan",
119+
"sscan_iter",
120+
"hscan",
121+
"hscan_inter",
122+
"zscan",
123+
"zscan_iter",
124+
"sadd",
125+
"scard",
126+
"sdiff",
127+
"sdiffstore",
128+
"sinter",
129+
"sinterstore",
130+
"sismember",
131+
"smembers",
132+
"smove",
133+
"spop",
134+
"srandmember",
135+
"srem",
136+
"sunion",
137+
"sunionstore",
138+
"zadd",
139+
"zcard",
140+
"zcount",
141+
"zincrby",
142+
"zinterstore",
143+
"zlexcount",
144+
"zrange",
145+
"zrangebylex",
146+
"zrangebyscore",
147+
"zrank",
148+
"zrem",
149+
"zremrangebylex",
150+
"zremrangebyrank",
151+
"zremrangebyscore",
152+
"zrevrange",
153+
"zrevrangebyscore",
154+
"zrevrank",
155+
"zscore",
156+
"zunionstore",
157+
"pfadd",
158+
"pfcount",
159+
"pfmerge",
160+
"hdel",
161+
"hexists",
162+
"hget",
163+
"hgetall",
164+
"hincrby",
165+
"hincrbyfloat",
166+
"hkeys",
167+
"hlen",
168+
"hset",
169+
"hsetnx",
170+
"hmset",
171+
"hmget",
172+
"hvals",
173+
"publish",
174+
"eval",
175+
"evalsha",
176+
"script_exists",
177+
"script_flush",
178+
"script_kill",
179+
"script_load",
180+
"setex",
181+
"lrem",
182+
"zadd",
183+
)
184+
185+
_redis_multipart_commands = set(["client", "cluster", "command", "config", "debug", "sentinel", "slowlog", "script"])
186+
187+
_redis_operation_re = re.compile(r"[-\s]+")
188+
189+
190+
def _conn_attrs_to_dict(connection):
191+
return {
192+
"host": getattr(connection, "host", None),
193+
"port": getattr(connection, "port", None),
194+
"path": getattr(connection, "path", None),
195+
"db": getattr(connection, "db", None),
196+
}
197+
198+
199+
def _instance_info(kwargs):
200+
host = kwargs.get("host") or "localhost"
201+
port_path_or_id = str(kwargs.get("port") or kwargs.get("path", "unknown"))
202+
db = str(kwargs.get("db") or 0)
203+
204+
return (host, port_path_or_id, db)
205+
206+
207+
async def _nr_Connection_send_command_wrapper_(wrapped, instance, args, kwargs):
208+
transaction = current_transaction()
209+
if not transaction:
210+
return await wrapped(*args, **kwargs)
211+
212+
host, port_path_or_id, db = (None, None, None)
213+
214+
try:
215+
dt = transaction.settings.datastore_tracer
216+
if dt.instance_reporting.enabled or dt.database_name_reporting.enabled:
217+
conn_kwargs = _conn_attrs_to_dict(instance)
218+
host, port_path_or_id, db = _instance_info(conn_kwargs)
219+
except Exception:
220+
pass
221+
222+
transaction._nr_datastore_instance_info = (host, port_path_or_id, db)
223+
224+
# Older Redis clients would when sending multi part commands pass
225+
# them in as separate arguments to send_command(). Need to therefore
226+
# detect those and grab the next argument from the set of arguments.
227+
228+
operation = args[0].strip().lower()
229+
230+
# If it's not a multi part command, there's no need to trace it, so
231+
# we can return early.
232+
233+
if operation.split()[0] not in _redis_multipart_commands:
234+
return await wrapped(*args, **kwargs)
235+
236+
# Convert multi args to single arg string
237+
238+
if operation in _redis_multipart_commands and len(args) > 1:
239+
operation = "%s %s" % (operation, args[1].strip().lower())
240+
241+
operation = _redis_operation_re.sub("_", operation)
242+
243+
with DatastoreTrace(
244+
product="Redis", target=None, operation=operation, host=host, port_path_or_id=port_path_or_id, database_name=db
245+
):
246+
return await wrapped(*args, **kwargs)
247+
248+
249+
def instrument_aredis_connection(module):
250+
wrap_function_wrapper(module.Connection, "send_command", _nr_Connection_send_command_wrapper_)

tests/datastore_aredis/conftest.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
17+
from testing_support.fixtures import (code_coverage_fixture,
18+
collector_agent_registration_fixture, collector_available_fixture)
19+
20+
_coverage_source = [
21+
'newrelic.hooks.datastore_aredis',
22+
]
23+
24+
code_coverage = code_coverage_fixture(source=_coverage_source)
25+
26+
_default_settings = {
27+
'transaction_tracer.explain_threshold': 0.0,
28+
'transaction_tracer.transaction_threshold': 0.0,
29+
'transaction_tracer.stack_trace_threshold': 0.0,
30+
'debug.log_data_collector_payloads': True,
31+
'debug.record_transaction_failure': True
32+
}
33+
34+
collector_agent_registration = collector_agent_registration_fixture(
35+
app_name='Python Agent Test (datastore_aredis)',
36+
default_settings=_default_settings,
37+
linked_applications=['Python Agent Test (datastore)'])
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
import aredis
17+
18+
from newrelic.api.background_task import background_task
19+
20+
from testing_support.fixtures import (validate_transaction_metrics,
21+
override_application_settings)
22+
from testing_support.db_settings import redis_settings
23+
from testing_support.util import instance_hostname
24+
25+
DB_SETTINGS = redis_settings()[0]
26+
REDIS_PY_VERSION = aredis.VERSION
27+
28+
# Settings
29+
30+
_enable_instance_settings = {
31+
'datastore_tracer.instance_reporting.enabled': True,
32+
}
33+
_disable_instance_settings = {
34+
'datastore_tracer.instance_reporting.enabled': False,
35+
}
36+
37+
# Metrics
38+
39+
_base_scoped_metrics = (
40+
('Datastore/operation/Redis/client_list', 1),
41+
)
42+
43+
_base_rollup_metrics = (
44+
('Datastore/all', 1),
45+
('Datastore/allOther', 1),
46+
('Datastore/Redis/all', 1),
47+
('Datastore/Redis/allOther', 1),
48+
('Datastore/operation/Redis/client_list', 1),
49+
)
50+
51+
_disable_scoped_metrics = list(_base_scoped_metrics)
52+
_disable_rollup_metrics = list(_base_rollup_metrics)
53+
54+
_enable_scoped_metrics = list(_base_scoped_metrics)
55+
_enable_rollup_metrics = list(_base_rollup_metrics)
56+
57+
_host = instance_hostname(DB_SETTINGS['host'])
58+
_port = DB_SETTINGS['port']
59+
60+
_instance_metric_name = 'Datastore/instance/Redis/%s/%s' % (_host, _port)
61+
62+
_enable_rollup_metrics.append(
63+
(_instance_metric_name, 1)
64+
)
65+
66+
_disable_rollup_metrics.append(
67+
(_instance_metric_name, None)
68+
)
69+
70+
def exercise_redis_multi_args(client):
71+
client.execute_command('CLIENT', 'LIST', parse='LIST')
72+
73+
def exercise_redis_single_arg(client):
74+
client.execute_command('CLIENT LIST')
75+
76+
@override_application_settings(_enable_instance_settings)
77+
@validate_transaction_metrics(
78+
'test_execute_command:test_strict_redis_execute_command_two_args_enable',
79+
scoped_metrics=_enable_scoped_metrics,
80+
rollup_metrics=_enable_rollup_metrics,
81+
background_task=True)
82+
@background_task()
83+
def test_strict_redis_execute_command_two_args_enable():
84+
r = aredis.StrictRedis(host=DB_SETTINGS['host'],
85+
port=DB_SETTINGS['port'], db=0)
86+
exercise_redis_multi_args(r)

0 commit comments

Comments
 (0)