Skip to content

Commit 4c09f32

Browse files
untitakersentry-bot
andauthored
feat: Support tracing on Tornado (#1060)
* feat: Support tracing on Tornado * add extra assertion about request body * parametrize transaction test * fix: Formatting Co-authored-by: sentry-bot <[email protected]>
1 parent 4a37642 commit 4c09f32

File tree

3 files changed

+136
-27
lines changed

3 files changed

+136
-27
lines changed

sentry_sdk/integrations/aiohttp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ async def sentry_app_handle(self, request, *args, **kwargs):
9292

9393
weak_request = weakref.ref(request)
9494

95-
with Hub(Hub.current) as hub:
95+
with Hub(hub) as hub:
9696
# Scope data will not leak between requests because aiohttp
9797
# create a task to wrap each request.
9898
with hub.configure_scope() as scope:

sentry_sdk/integrations/tornado.py

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import weakref
2+
import contextlib
23
from inspect import iscoroutinefunction
34

45
from sentry_sdk.hub import Hub, _should_send_default_pii
6+
from sentry_sdk.tracing import Transaction
57
from sentry_sdk.utils import (
68
HAS_REAL_CONTEXTVARS,
79
CONTEXTVARS_ERROR_MESSAGE,
@@ -32,6 +34,7 @@
3234
from typing import Optional
3335
from typing import Dict
3436
from typing import Callable
37+
from typing import Generator
3538

3639
from sentry_sdk._types import EventProcessor
3740

@@ -63,38 +66,16 @@ def setup_once():
6366
# Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
6467
# In that case our method should be a coroutine function too
6568
async def sentry_execute_request_handler(self, *args, **kwargs):
66-
# type: (Any, *Any, **Any) -> Any
67-
hub = Hub.current
68-
integration = hub.get_integration(TornadoIntegration)
69-
if integration is None:
70-
return await old_execute(self, *args, **kwargs)
71-
72-
weak_handler = weakref.ref(self)
73-
74-
with Hub(hub) as hub:
75-
with hub.configure_scope() as scope:
76-
scope.clear_breadcrumbs()
77-
processor = _make_event_processor(weak_handler) # type: ignore
78-
scope.add_event_processor(processor)
69+
# type: (RequestHandler, *Any, **Any) -> Any
70+
with _handle_request_impl(self):
7971
return await old_execute(self, *args, **kwargs)
8072

8173
else:
8274

8375
@coroutine # type: ignore
8476
def sentry_execute_request_handler(self, *args, **kwargs):
8577
# type: (RequestHandler, *Any, **Any) -> Any
86-
hub = Hub.current
87-
integration = hub.get_integration(TornadoIntegration)
88-
if integration is None:
89-
return old_execute(self, *args, **kwargs)
90-
91-
weak_handler = weakref.ref(self)
92-
93-
with Hub(hub) as hub:
94-
with hub.configure_scope() as scope:
95-
scope.clear_breadcrumbs()
96-
processor = _make_event_processor(weak_handler) # type: ignore
97-
scope.add_event_processor(processor)
78+
with _handle_request_impl(self):
9879
result = yield from old_execute(self, *args, **kwargs)
9980
return result
10081

@@ -110,6 +91,39 @@ def sentry_log_exception(self, ty, value, tb, *args, **kwargs):
11091
RequestHandler.log_exception = sentry_log_exception # type: ignore
11192

11293

94+
@contextlib.contextmanager
95+
def _handle_request_impl(self):
96+
# type: (RequestHandler) -> Generator[None, None, None]
97+
hub = Hub.current
98+
integration = hub.get_integration(TornadoIntegration)
99+
100+
if integration is None:
101+
yield
102+
103+
weak_handler = weakref.ref(self)
104+
105+
with Hub(hub) as hub:
106+
with hub.configure_scope() as scope:
107+
scope.clear_breadcrumbs()
108+
processor = _make_event_processor(weak_handler) # type: ignore
109+
scope.add_event_processor(processor)
110+
111+
transaction = Transaction.continue_from_headers(
112+
self.request.headers,
113+
op="http.server",
114+
# Like with all other integrations, this is our
115+
# fallback transaction in case there is no route.
116+
# sentry_urldispatcher_resolve is responsible for
117+
# setting a transaction name later.
118+
name="generic Tornado request",
119+
)
120+
121+
with hub.start_transaction(
122+
transaction, custom_sampling_context={"tornado_request": self.request}
123+
):
124+
yield
125+
126+
113127
def _capture_exception(ty, value, tb):
114128
# type: (type, BaseException, Any) -> None
115129
hub = Hub.current

tests/integrations/tornado/test_tornado.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from sentry_sdk import configure_scope
5+
from sentry_sdk import configure_scope, start_transaction
66
from sentry_sdk.integrations.tornado import TornadoIntegration
77

88
from tornado.web import RequestHandler, Application, HTTPError
@@ -40,6 +40,25 @@ def get(self):
4040
scope.set_tag("foo", "42")
4141
1 / 0
4242

43+
def post(self):
44+
with configure_scope() as scope:
45+
scope.set_tag("foo", "43")
46+
1 / 0
47+
48+
49+
class HelloHandler(RequestHandler):
50+
async def get(self):
51+
with configure_scope() as scope:
52+
scope.set_tag("foo", "42")
53+
54+
return b"hello"
55+
56+
async def post(self):
57+
with configure_scope() as scope:
58+
scope.set_tag("foo", "43")
59+
60+
return b"hello"
61+
4362

4463
def test_basic(tornado_testcase, sentry_init, capture_events):
4564
sentry_init(integrations=[TornadoIntegration()], send_default_pii=True)
@@ -82,6 +101,82 @@ def test_basic(tornado_testcase, sentry_init, capture_events):
82101
assert not scope._tags
83102

84103

104+
@pytest.mark.parametrize(
105+
"handler,code",
106+
[
107+
(CrashingHandler, 500),
108+
(HelloHandler, 200),
109+
],
110+
)
111+
def test_transactions(tornado_testcase, sentry_init, capture_events, handler, code):
112+
sentry_init(integrations=[TornadoIntegration()], traces_sample_rate=1.0, debug=True)
113+
events = capture_events()
114+
client = tornado_testcase(Application([(r"/hi", handler)]))
115+
116+
with start_transaction(name="client") as span:
117+
pass
118+
119+
response = client.fetch(
120+
"/hi", method="POST", body=b"heyoo", headers=dict(span.iter_headers())
121+
)
122+
assert response.code == code
123+
124+
if code == 200:
125+
client_tx, server_tx = events
126+
server_error = None
127+
else:
128+
client_tx, server_error, server_tx = events
129+
130+
assert client_tx["type"] == "transaction"
131+
assert client_tx["transaction"] == "client"
132+
133+
if server_error is not None:
134+
assert server_error["exception"]["values"][0]["type"] == "ZeroDivisionError"
135+
assert (
136+
server_error["transaction"]
137+
== "tests.integrations.tornado.test_tornado.CrashingHandler.post"
138+
)
139+
140+
if code == 200:
141+
assert (
142+
server_tx["transaction"]
143+
== "tests.integrations.tornado.test_tornado.HelloHandler.post"
144+
)
145+
else:
146+
assert (
147+
server_tx["transaction"]
148+
== "tests.integrations.tornado.test_tornado.CrashingHandler.post"
149+
)
150+
151+
assert server_tx["type"] == "transaction"
152+
153+
request = server_tx["request"]
154+
host = request["headers"]["Host"]
155+
assert server_tx["request"] == {
156+
"env": {"REMOTE_ADDR": "127.0.0.1"},
157+
"headers": {
158+
"Accept-Encoding": "gzip",
159+
"Connection": "close",
160+
**request["headers"],
161+
},
162+
"method": "POST",
163+
"query_string": "",
164+
"data": {"heyoo": [""]},
165+
"url": "http://{host}/hi".format(host=host),
166+
}
167+
168+
assert (
169+
client_tx["contexts"]["trace"]["trace_id"]
170+
== server_tx["contexts"]["trace"]["trace_id"]
171+
)
172+
173+
if server_error is not None:
174+
assert (
175+
server_error["contexts"]["trace"]["trace_id"]
176+
== server_tx["contexts"]["trace"]["trace_id"]
177+
)
178+
179+
85180
def test_400_not_logged(tornado_testcase, sentry_init, capture_events):
86181
sentry_init(integrations=[TornadoIntegration()])
87182
events = capture_events()

0 commit comments

Comments
 (0)