Repository: elastic/apm-agent-python

Issue:
aioredis instrumentation: AttributeError: 'str' object has no attribute 'decode'
**Describe the bug**
In some cases `aioredis` passes `str` commands to its `aioredis.Redis.execute()` method rather than `bytes` commands, like e.g. when executing `aioredis.Redis.ping()`, which is unacceptable by `elastic-apm`.  

**Current workaround**
Disable `aioredis` instrumenting (set environment variable `SKIP_INSTRUMENT_AIOREDIS="true"`) prior `elasticapm.instrument()` is called.

**To Reproduce**
```python3
import asyncio

import aioredis
import elasticapm

REDIS_URL = "redis://localhost:6379"


async def main():
    elasticapm.instrument()

    client = await aioredis.create_redis_pool(REDIS_URL)
    elastic_apm_client = elasticapm.Client()

    elastic_apm_client.begin_transaction("redis-ping")
    try:
        assert await client.ping() == b"PONG"
    finally:
        client.close()
        await client.wait_closed()
        elastic_apm_client.end_transaction("redis-ping")

if __name__ == "__main__":
    asyncio.run(main())
```

**Expected result**
No exception is raised.

**Actual result**
```python3
Traceback (most recent call last):
  File "<...>/test.py", line 24, in <module>
    asyncio.run(main())
  File "/usr/local/lib/python3.8/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "<...>/test.py", line 17, in main
    assert await client.ping() == b"PONG"
  File "<...>/venv/lib/python3.8/site-packages/aioredis/commands/__init__.py", line 114, in ping
    return self.execute('PING', *args, encoding=encoding)
  File "<...>/venv/lib/python3.8/site-packages/aioredis/commands/__init__.py", line 51, in execute
    return self._pool_or_conn.execute(command, *args, **kwargs)
  File "<...>/venv/lib/python3.8/site-packages/elasticapm/instrumentation/packages/base.py", line 210, in call_if_sampling
    return self.call(module, method, wrapped, instance, args, kwargs)
  File "<...>/venv/lib/python3.8/site-packages/elasticapm/instrumentation/packages/asyncio/aioredis.py", line 46, in call
    wrapped_name = args[0].decode()
AttributeError: 'str' object has no attribute 'decode'
```

**Environment**
- OS: Ubuntu 18.04.5 LTS
- Python version: 3.8.9
- Framework and version: `aioredis==1.3.1`
- APM Server version: --
- Agent version: `elasticapm==6.3.3`



Discussion:

Resolving patch:
diff --git a/elasticapm/instrumentation/packages/asyncio/aioredis.py b/elasticapm/instrumentation/packages/asyncio/aioredis.py
--- a/elasticapm/instrumentation/packages/asyncio/aioredis.py
+++ b/elasticapm/instrumentation/packages/asyncio/aioredis.py
@@ -38,12 +38,16 @@
 class RedisConnectionPoolInstrumentation(AbstractInstrumentedModule):
     name = "aioredis"
 
-    instrument_list = [("aioredis.pool", "ConnectionsPool.execute"),
-                       ("aioredis.pool", "ConnectionsPool.execute_pubsub")]
+    instrument_list = [
+        ("aioredis.pool", "ConnectionsPool.execute"),
+        ("aioredis.pool", "ConnectionsPool.execute_pubsub"),
+    ]
 
     def call(self, module, method, wrapped, instance, args, kwargs):
         if len(args) > 0:
-            wrapped_name = args[0].decode()
+            wrapped_name = args[0]
+            if isinstance(wrapped_name, bytes):
+                wrapped_name = wrapped_name.decode()
         else:
             wrapped_name = self.get_wrapped_name(wrapped, instance, method)
 
@@ -74,8 +78,10 @@ def call(self, module, method, wrapped, instance, args, kwargs):
 class RedisConnectionInstrumentation(AbstractInstrumentedModule):
     name = "aioredis"
 
-    instrument_list = (("aioredis.connection", "RedisConnection.execute"),
-                       ("aioredis.pool", "ConnectionsPool.execute_pubsub"))
+    instrument_list = (
+        ("aioredis.connection", "RedisConnection.execute"),
+        ("aioredis.pool", "ConnectionsPool.execute_pubsub"),
+    )
 
     def call(self, module, method, wrapped, instance, args, kwargs):
         span = execution_context.get_span()


Test patch:
diff --git a/tests/instrumentation/asyncio_tests/aioredis_tests.py b/tests/instrumentation/asyncio_tests/aioredis_tests.py
--- a/tests/instrumentation/asyncio_tests/aioredis_tests.py
+++ b/tests/instrumentation/asyncio_tests/aioredis_tests.py
@@ -55,6 +55,18 @@ async def redis_conn():
     await conn.wait_closed()
 
 
+@pytest.mark.integrationtest
+async def test_ping(instrument, elasticapm_client, redis_conn):
+    # The PING command is sent as a byte string, so this tests if we can handle
+    # the command both as a str and as a bytes. See #1307
+    elasticapm_client.begin_transaction("transaction.test")
+    redis_conn.ping()
+    elasticapm_client.end_transaction("test")
+    transaction = elasticapm_client.events[TRANSACTION][0]
+    span = elasticapm_client.spans_for_transaction(transaction)[0]
+    assert span["name"] == "PING"
+
+
 @pytest.mark.integrationtest
 async def test_pipeline(instrument, elasticapm_client, redis_conn):
     elasticapm_client.begin_transaction("transaction.test")