1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15- from newrelic .api .datastore_trace import DatastoreTrace
15+ from newrelic .api .datastore_trace import DatastoreTrace , DatastoreTraceWrapper
1616from newrelic .api .time_trace import current_trace
1717from newrelic .api .transaction import current_transaction
18- from newrelic .common .object_wrapper import wrap_function_wrapper
18+ from newrelic .common .object_wrapper import wrap_function_wrapper , function_wrapper , FunctionWrapper
1919from newrelic .hooks .datastore_redis import (
2020 _redis_client_methods ,
2121 _redis_multipart_commands ,
2222 _redis_operation_re ,
2323)
2424
25+ from newrelic .common .async_wrapper import async_wrapper
26+
27+ import aioredis
28+
29+ try :
30+ AIOREDIS_VERSION = tuple (int (x ) for x in getattr (aioredis , "__version__" ).split ("." ))
31+ except Exception :
32+ AIOREDIS_VERSION = (0 , 0 , 0 )
33+
2534
2635def _conn_attrs_to_dict (connection ):
2736 host = getattr (connection , "host" , None )
@@ -45,13 +54,36 @@ def _instance_info(kwargs):
4554
4655
4756def _wrap_AioRedis_method_wrapper (module , instance_class_name , operation ):
48- async def _nr_wrapper_AioRedis_method_ (wrapped , instance , args , kwargs ):
57+
58+ @function_wrapper
59+ async def _nr_wrapper_AioRedis_async_method_ (wrapped , instance , args , kwargs ):
4960 transaction = current_transaction ()
5061 if transaction is None :
5162 return await wrapped (* args , ** kwargs )
5263
5364 with DatastoreTrace (product = "Redis" , target = None , operation = operation ):
5465 return await wrapped (* args , ** kwargs )
66+
67+ def _nr_wrapper_AioRedis_method_ (wrapped , instance , args , kwargs ):
68+ # Check for transaction and return early if found.
69+ # Method will return synchronously without executing,
70+ # it will be added to the command stack and run later.
71+ if AIOREDIS_VERSION < (2 ,):
72+ # AioRedis v1 uses a RedisBuffer instead of a real connection for queueing up pipeline commands
73+ from aioredis .commands .transaction import _RedisBuffer
74+ if isinstance (instance ._pool_or_conn , _RedisBuffer ):
75+ # Method will return synchronously without executing,
76+ # it will be added to the command stack and run later.
77+ return wrapped (* args , ** kwargs )
78+ else :
79+ # AioRedis v2 uses a Pipeline object for a client and internally queues up pipeline commands
80+ from aioredis .client import Pipeline
81+ if isinstance (instance , Pipeline ):
82+ return wrapped (* args , ** kwargs )
83+
84+ # Method should be run when awaited, therefore we wrap in an async wrapper.
85+ return _nr_wrapper_AioRedis_async_method_ (wrapped )(* args , ** kwargs )
86+
5587
5688 name = "%s.%s" % (instance_class_name , operation )
5789 wrap_function_wrapper (module , name , _nr_wrapper_AioRedis_method_ )
@@ -108,6 +140,58 @@ async def wrap_Connection_send_command(wrapped, instance, args, kwargs):
108140 return await wrapped (* args , ** kwargs )
109141
110142
143+ def wrap_RedisConnection_execute (wrapped , instance , args , kwargs ):
144+ # RedisConnection in aioredis v1 returns a future instead of using coroutines
145+ transaction = current_transaction ()
146+ if not transaction :
147+ return wrapped (* args , ** kwargs )
148+
149+ host , port_path_or_id , db = (None , None , None )
150+
151+ try :
152+ dt = transaction .settings .datastore_tracer
153+ if dt .instance_reporting .enabled or dt .database_name_reporting .enabled :
154+ conn_kwargs = _conn_attrs_to_dict (instance )
155+ host , port_path_or_id , db = _instance_info (conn_kwargs )
156+ except Exception :
157+ pass
158+
159+ # Older Redis clients would when sending multi part commands pass
160+ # them in as separate arguments to send_command(). Need to therefore
161+ # detect those and grab the next argument from the set of arguments.
162+
163+ operation = args [0 ].strip ().lower ()
164+
165+ # If it's not a multi part command, there's no need to trace it, so
166+ # we can return early.
167+
168+ if operation .split ()[0 ] not in _redis_multipart_commands : # Set the datastore info on the DatastoreTrace containing this function call.
169+ trace = current_trace ()
170+
171+ # Find DatastoreTrace no matter how many other traces are inbetween
172+ while trace is not None and not isinstance (trace , DatastoreTrace ):
173+ trace = getattr (trace , "parent" , None )
174+
175+ if trace is not None :
176+ trace .host = host
177+ trace .port_path_or_id = port_path_or_id
178+ trace .database_name = db
179+
180+ return wrapped (* args , ** kwargs )
181+
182+ # Convert multi args to single arg string
183+
184+ if operation in _redis_multipart_commands and len (args ) > 1 :
185+ operation = "%s %s" % (operation , args [1 ].strip ().lower ())
186+
187+ operation = _redis_operation_re .sub ("_" , operation )
188+
189+ with DatastoreTrace (
190+ product = "Redis" , target = None , operation = operation , host = host , port_path_or_id = port_path_or_id , database_name = db
191+ ):
192+ return wrapped (* args , ** kwargs )
193+
194+
111195def instrument_aioredis_client (module ):
112196 # StrictRedis is just an alias of Redis, no need to wrap it as well.
113197 if hasattr (module , "Redis" ):
@@ -124,4 +208,4 @@ def instrument_aioredis_connection(module):
124208
125209 if hasattr (module , "RedisConnection" ):
126210 if hasattr (module .RedisConnection , "execute" ):
127- wrap_function_wrapper (module , "RedisConnection.execute" , wrap_Connection_send_command )
211+ wrap_function_wrapper (module , "RedisConnection.execute" , wrap_RedisConnection_execute )
0 commit comments