Repository: elastic/apm-agent-python

Issue:
Flower hangs from version 5.4.0.
**Describe the bug**: 
Flower hangs (no answer from http connection to browser) when a version >= 5.4.0 is installed

**To Reproduce**

1. pip install elastic-apm==5.4.0
2. restart flower and try to access

**Environment (please complete the following information)**
- OS: Ubuntu 18.04
- Python version: 3.6
- Framework and version: Django 2.2
- APM Server version: NA
- Agent version: 5.4.0+ 



Discussion:
To clarify, you were previously on a version <5.4.0 and had it working? I want to make sure this is a regression as opposed to a long-standing bug.
The last version we had in production was 5.3.2 and it worked fine. From 5.4.0 it hangs.

Please let me know what extra info I can provide to help.
Thanks for that update. I'm going to spend some time tomorrow looking into this.
Aha! I have this reproduced and think I'm on the road to a fix. Basically, `flower` uses `tornado`, for which we recently added instrumentation. With the way `flower` integrates with Django and Celery, apparently it does get instrumented but not as a `tornado` app, which causes this error:

```
[E 200429 20:19:11 base_events:1296] Exception in callback _HandlerDelegate.execute.<locals>.<lambda>(<Task finishe...pm_client'",)>) at /Users/basepi/.pyenv/versions/3.6.10/envs/djangotest/lib/python3.6/site-packages/tornado/web.py:2333
    handle: <Handle _HandlerDelegate.execute.<locals>.<lambda>(<Task finishe...pm_client'",)>) at /Users/basepi/.pyenv/versions/3.6.10/envs/djangotest/lib/python3.6/site-packages/tornado/web.py:2333>
    Traceback (most recent call last):
      File "/Users/basepi/.pyenv/versions/3.6.10/lib/python3.6/asyncio/events.py", line 145, in _run
        self._callback(*self._args)
      File "/Users/basepi/.pyenv/versions/3.6.10/envs/djangotest/lib/python3.6/site-packages/tornado/web.py", line 2333, in <lambda>
        fut.add_done_callback(lambda f: f.result())
      File "/Users/basepi/.pyenv/versions/3.6.10/envs/djangotest/lib/python3.6/site-packages/elasticapm/instrumentation/packages/tornado.py", line 50, in call
        client = instance.application.elasticapm_client
    AttributeError: 'Flower' object has no attribute 'elasticapm_client'
```

Working on a fix.

Resolving patch:
diff --git a/elasticapm/instrumentation/packages/tornado.py b/elasticapm/instrumentation/packages/tornado.py
--- a/elasticapm/instrumentation/packages/tornado.py
+++ b/elasticapm/instrumentation/packages/tornado.py
@@ -43,6 +43,11 @@ class TornadoRequestExecuteInstrumentation(AsyncAbstractInstrumentedModule):
     instrument_list = [("tornado.web", "RequestHandler._execute")]
 
     async def call(self, module, method, wrapped, instance, args, kwargs):
+        if not hasattr(instance.application, "elasticapm_client"):
+            # If tornado was instrumented but not as the main framework
+            # (i.e. in Flower), we should skip it.
+            return await wrapped(*args, **kwargs)
+
         # Late import to avoid ImportErrors
         from elasticapm.contrib.tornado.utils import get_data_from_request, get_data_from_response
 
@@ -74,6 +79,10 @@ class TornadoHandleRequestExceptionInstrumentation(AbstractInstrumentedModule):
     instrument_list = [("tornado.web", "RequestHandler._handle_request_exception")]
 
     def call(self, module, method, wrapped, instance, args, kwargs):
+        if not hasattr(instance.application, "elasticapm_client"):
+            # If tornado was instrumented but not as the main framework
+            # (i.e. in Flower), we should skip it.
+            return wrapped(*args, **kwargs)
 
         # Late import to avoid ImportErrors
         from tornado.web import Finish, HTTPError


Test patch:
diff --git a/tests/contrib/asyncio/tornado/tornado_tests.py b/tests/contrib/asyncio/tornado/tornado_tests.py
--- a/tests/contrib/asyncio/tornado/tornado_tests.py
+++ b/tests/contrib/asyncio/tornado/tornado_tests.py
@@ -75,6 +75,36 @@ def get(self):
     return app
 
 
+@pytest.fixture
+def app_no_client():
+    class HelloHandler(tornado.web.RequestHandler):
+        def get(self):
+            with async_capture_span("test"):
+                pass
+            return self.write("Hello, world")
+
+        post = get
+
+    class RenderHandler(tornado.web.RequestHandler):
+        def get(self):
+            with async_capture_span("test"):
+                pass
+            items = ["Item 1", "Item 2", "Item 3"]
+            return self.render("test.html", title="Testing so hard", items=items)
+
+    class BoomHandler(tornado.web.RequestHandler):
+        def get(self):
+            raise tornado.web.HTTPError()
+
+        post = get
+
+    app = tornado.web.Application(
+        [(r"/", HelloHandler), (r"/boom", BoomHandler), (r"/render", RenderHandler)],
+        template_path=os.path.join(os.path.dirname(__file__), "templates"),
+    )
+    return app
+
+
 @pytest.mark.gen_test
 async def test_get(app, base_url, http_client):
     elasticapm_client = app.elasticapm_client
@@ -196,3 +226,15 @@ async def test_capture_headers_body_is_dynamic(app, base_url, http_client):
     assert transactions[2]["context"]["request"]["body"] == "[REDACTED]"
     assert "headers" not in errors[1]["context"]["request"]
     assert errors[1]["context"]["request"]["body"] == "[REDACTED]"
+
+
+@pytest.mark.gen_test
+async def test_no_elasticapm_client(app_no_client, base_url, http_client, elasticapm_client):
+    """
+    Need to make sure instrumentation works even when tornado is not
+    explicitly using the agent
+    """
+    elasticapm_client.begin_transaction("test")
+    response = await http_client.fetch(base_url)
+    assert response.code == 200
+    elasticapm_client.end_transaction("test")