Repository: getsentry/sentry-python

Issue:
Add performance monitoring to Tornado integration
### Environment

How do you use Sentry?
Sentry SaaS (sentry.io) 

Which SDK and version?
sentry-sdk==1.0.0

### Steps to Reproduce

Create a python Tornado/Celery/Redis application and integrate sentry using:

    sentry_sdk.init(
        "https://xxxxxxxxxxxxxxxxx",
        integrations=[TornadoIntegration(), CeleryIntegration(), RedisIntegration()],
        traces_sample_rate=1.0
    )

### Expected Result

Capture results and monitor performance.

### Actual Result

Sentry is only capturing errors but the performance monitoring didn't work.



Discussion:
can you attach debugging output with debug=True kwarg passed to init()?

On Mon, Mar 15, 2021 at 4:55 PM alterEgo123 ***@***.***>
wrote:

> Environment
>
> How do you use Sentry?
> Sentry SaaS (sentry.io)
>
> Which SDK and version?
> sentry-sdk==1.0.0
> Steps to Reproduce
>
> Create a python Tornado/Celery/Redis application and integrate sentry
> using:
>
> sentry_sdk.init(
>     "https://xxxxxxxxxxxxxxxxx",
>     integrations=[TornadoIntegration(), CeleryIntegration(), RedisIntegration()],
>     traces_sample_rate=1.0
> )
>
> Expected Result
>
> Capture results and monitor performance.
> Actual Result
>
> Sentry is only capturing errors but the performance monitoring didn't work.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/getsentry/sentry-python/issues/1057>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAGMPRIRXZLWBO4AT7HLVRLTDYUWXANCNFSM4ZG2IN5Q>
> .
>

@untitaker This is Sentry's log if my request raises an exception.

```
 [sentry] DEBUG: Sending event, type:null level:error event_id:93f8b07eddf64757b81a71b71f707b85 project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:34 transport:229] Sending event, type:null level:error event_id:93f8b07eddf64757b81a71b71f707b85 project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:ad9ec973beee4ecb9c6033e9c631ef53 project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:ad9ec973beee4ecb9c6033e9c631ef53 project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:c54e6883514e43a5876ff3b5c04eb89b project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:c54e6883514e43a5876ff3b5c04eb89b project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:67f0c6cf197145d0843310d96db12339 project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:67f0c6cf197145d0843310d96db12339 project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:c3b9da3d5ec74cb993ad291a6e426651 project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:c3b9da3d5ec74cb993ad291a6e426651 project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:3d1b2296fd4d438289b75023bb3e3e73 project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:3d1b2296fd4d438289b75023bb3e3e73 project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:3e2b4a298e934c5ba950fa842fb7ec2b project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:3e2b4a298e934c5ba950fa842fb7ec2b project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:645e056203c646acac5a8071e9c4e13c project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:645e056203c646acac5a8071e9c4e13c project:5677226 host:o339462.ingest.sentry.io
 [sentry] DEBUG: Sending event, type:null level:error event_id:e032a9c57d324e1aa06daaa3a5c54a8e project:5677226 host:o339462.ingest.sentry.io
[D 210316 07:43:35 transport:229] Sending event, type:null level:error event_id:e032a9c57d324e1aa06daaa3a5c54a8e project:5677226 host:o339462.ingest.sentry.io
```


But nothing is shown when a request is successful. Am I not understanding performance monitoring correctly? 
Sorry, we don't actually have performance monitoring implemented for tornado atm. It'll take a week to add so I'll get to it soon. Do you see transaction events for your celery tasks though?
Yes it's working perfectly for Celery !

Resolving patch:
diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py
--- a/sentry_sdk/integrations/aiohttp.py
+++ b/sentry_sdk/integrations/aiohttp.py
@@ -92,7 +92,7 @@ async def sentry_app_handle(self, request, *args, **kwargs):
 
             weak_request = weakref.ref(request)
 
-            with Hub(Hub.current) as hub:
+            with Hub(hub) as hub:
                 # Scope data will not leak between requests because aiohttp
                 # create a task to wrap each request.
                 with hub.configure_scope() as scope:
diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py
--- a/sentry_sdk/integrations/tornado.py
+++ b/sentry_sdk/integrations/tornado.py
@@ -1,7 +1,9 @@
 import weakref
+import contextlib
 from inspect import iscoroutinefunction
 
 from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.tracing import Transaction
 from sentry_sdk.utils import (
     HAS_REAL_CONTEXTVARS,
     CONTEXTVARS_ERROR_MESSAGE,
@@ -32,6 +34,7 @@
     from typing import Optional
     from typing import Dict
     from typing import Callable
+    from typing import Generator
 
     from sentry_sdk._types import EventProcessor
 
@@ -63,19 +66,8 @@ def setup_once():
             # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
             # In that case our method should be a coroutine function too
             async def sentry_execute_request_handler(self, *args, **kwargs):
-                # type: (Any, *Any, **Any) -> Any
-                hub = Hub.current
-                integration = hub.get_integration(TornadoIntegration)
-                if integration is None:
-                    return await old_execute(self, *args, **kwargs)
-
-                weak_handler = weakref.ref(self)
-
-                with Hub(hub) as hub:
-                    with hub.configure_scope() as scope:
-                        scope.clear_breadcrumbs()
-                        processor = _make_event_processor(weak_handler)  # type: ignore
-                        scope.add_event_processor(processor)
+                # type: (RequestHandler, *Any, **Any) -> Any
+                with _handle_request_impl(self):
                     return await old_execute(self, *args, **kwargs)
 
         else:
@@ -83,18 +75,7 @@ async def sentry_execute_request_handler(self, *args, **kwargs):
             @coroutine  # type: ignore
             def sentry_execute_request_handler(self, *args, **kwargs):
                 # type: (RequestHandler, *Any, **Any) -> Any
-                hub = Hub.current
-                integration = hub.get_integration(TornadoIntegration)
-                if integration is None:
-                    return old_execute(self, *args, **kwargs)
-
-                weak_handler = weakref.ref(self)
-
-                with Hub(hub) as hub:
-                    with hub.configure_scope() as scope:
-                        scope.clear_breadcrumbs()
-                        processor = _make_event_processor(weak_handler)  # type: ignore
-                        scope.add_event_processor(processor)
+                with _handle_request_impl(self):
                     result = yield from old_execute(self, *args, **kwargs)
                     return result
 
@@ -110,6 +91,39 @@ def sentry_log_exception(self, ty, value, tb, *args, **kwargs):
         RequestHandler.log_exception = sentry_log_exception  # type: ignore
 
 
+@contextlib.contextmanager
+def _handle_request_impl(self):
+    # type: (RequestHandler) -> Generator[None, None, None]
+    hub = Hub.current
+    integration = hub.get_integration(TornadoIntegration)
+
+    if integration is None:
+        yield
+
+    weak_handler = weakref.ref(self)
+
+    with Hub(hub) as hub:
+        with hub.configure_scope() as scope:
+            scope.clear_breadcrumbs()
+            processor = _make_event_processor(weak_handler)  # type: ignore
+            scope.add_event_processor(processor)
+
+        transaction = Transaction.continue_from_headers(
+            self.request.headers,
+            op="http.server",
+            # Like with all other integrations, this is our
+            # fallback transaction in case there is no route.
+            # sentry_urldispatcher_resolve is responsible for
+            # setting a transaction name later.
+            name="generic Tornado request",
+        )
+
+        with hub.start_transaction(
+            transaction, custom_sampling_context={"tornado_request": self.request}
+        ):
+            yield
+
+
 def _capture_exception(ty, value, tb):
     # type: (type, BaseException, Any) -> None
     hub = Hub.current


Test patch:
diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py
--- a/tests/integrations/tornado/test_tornado.py
+++ b/tests/integrations/tornado/test_tornado.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from sentry_sdk import configure_scope
+from sentry_sdk import configure_scope, start_transaction
 from sentry_sdk.integrations.tornado import TornadoIntegration
 
 from tornado.web import RequestHandler, Application, HTTPError
@@ -40,6 +40,25 @@ def get(self):
             scope.set_tag("foo", "42")
         1 / 0
 
+    def post(self):
+        with configure_scope() as scope:
+            scope.set_tag("foo", "43")
+        1 / 0
+
+
+class HelloHandler(RequestHandler):
+    async def get(self):
+        with configure_scope() as scope:
+            scope.set_tag("foo", "42")
+
+        return b"hello"
+
+    async def post(self):
+        with configure_scope() as scope:
+            scope.set_tag("foo", "43")
+
+        return b"hello"
+
 
 def test_basic(tornado_testcase, sentry_init, capture_events):
     sentry_init(integrations=[TornadoIntegration()], send_default_pii=True)
@@ -82,6 +101,82 @@ def test_basic(tornado_testcase, sentry_init, capture_events):
         assert not scope._tags
 
 
+@pytest.mark.parametrize(
+    "handler,code",
+    [
+        (CrashingHandler, 500),
+        (HelloHandler, 200),
+    ],
+)
+def test_transactions(tornado_testcase, sentry_init, capture_events, handler, code):
+    sentry_init(integrations=[TornadoIntegration()], traces_sample_rate=1.0, debug=True)
+    events = capture_events()
+    client = tornado_testcase(Application([(r"/hi", handler)]))
+
+    with start_transaction(name="client") as span:
+        pass
+
+    response = client.fetch(
+        "/hi", method="POST", body=b"heyoo", headers=dict(span.iter_headers())
+    )
+    assert response.code == code
+
+    if code == 200:
+        client_tx, server_tx = events
+        server_error = None
+    else:
+        client_tx, server_error, server_tx = events
+
+    assert client_tx["type"] == "transaction"
+    assert client_tx["transaction"] == "client"
+
+    if server_error is not None:
+        assert server_error["exception"]["values"][0]["type"] == "ZeroDivisionError"
+        assert (
+            server_error["transaction"]
+            == "tests.integrations.tornado.test_tornado.CrashingHandler.post"
+        )
+
+    if code == 200:
+        assert (
+            server_tx["transaction"]
+            == "tests.integrations.tornado.test_tornado.HelloHandler.post"
+        )
+    else:
+        assert (
+            server_tx["transaction"]
+            == "tests.integrations.tornado.test_tornado.CrashingHandler.post"
+        )
+
+    assert server_tx["type"] == "transaction"
+
+    request = server_tx["request"]
+    host = request["headers"]["Host"]
+    assert server_tx["request"] == {
+        "env": {"REMOTE_ADDR": "127.0.0.1"},
+        "headers": {
+            "Accept-Encoding": "gzip",
+            "Connection": "close",
+            **request["headers"],
+        },
+        "method": "POST",
+        "query_string": "",
+        "data": {"heyoo": [""]},
+        "url": "http://{host}/hi".format(host=host),
+    }
+
+    assert (
+        client_tx["contexts"]["trace"]["trace_id"]
+        == server_tx["contexts"]["trace"]["trace_id"]
+    )
+
+    if server_error is not None:
+        assert (
+            server_error["contexts"]["trace"]["trace_id"]
+            == server_tx["contexts"]["trace"]["trace_id"]
+        )
+
+
 def test_400_not_logged(tornado_testcase, sentry_init, capture_events):
     sentry_init(integrations=[TornadoIntegration()])
     events = capture_events()