Skip to content

Commit a58538d

Browse files
sl0thentr0pyclaude
andcommitted
ref(tornado): Migrate integration to span-first
Add span-streaming support to the Tornado integration. When span streaming is enabled, the request handler emits a StreamedSpan with HTTP request attributes (method, headers, query, URL, client address) and sets the response status on completion. The legacy transaction path is preserved for non-streaming mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 106fe71 commit a58538d

2 files changed

Lines changed: 242 additions & 77 deletions

File tree

sentry_sdk/integrations/tornado.py

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import sentry_sdk
66
from sentry_sdk.api import continue_trace
7-
from sentry_sdk.consts import OP
7+
from sentry_sdk.consts import OP, SPANDATA
88
from sentry_sdk.scope import should_send_default_pii
9+
from sentry_sdk.traces import SegmentSource, StreamedSpan
910
from sentry_sdk.tracing import TransactionSource
11+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1012
from sentry_sdk.utils import (
1113
HAS_REAL_CONTEXTVARS,
1214
CONTEXTVARS_ERROR_MESSAGE,
@@ -34,10 +36,14 @@
3436

3537
if TYPE_CHECKING:
3638
from typing import Any
39+
from typing import ContextManager
3740
from typing import Optional
3841
from typing import Dict
3942
from typing import Callable
4043
from typing import Generator
44+
from typing import Union
45+
46+
from sentry_sdk.tracing import Span
4147

4248
from sentry_sdk._types import Event, EventProcessor
4349

@@ -101,6 +107,9 @@ def sentry_log_exception(
101107
RequestHandler.log_exception = sentry_log_exception
102108

103109

110+
_DEFAULT_TRANSACTION_NAME = "generic Tornado request"
111+
112+
104113
@contextlib.contextmanager
105114
def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]":
106115
integration = sentry_sdk.get_client().get_integration(TornadoIntegration)
@@ -110,6 +119,8 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]
110119
return
111120

112121
weak_handler = weakref.ref(self)
122+
client = sentry_sdk.get_client()
123+
span_streaming = has_span_streaming_enabled(client.options)
113124

114125
with sentry_sdk.isolation_scope() as scope:
115126
headers = self.request.headers
@@ -118,22 +129,90 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]
118129
processor = _make_event_processor(weak_handler)
119130
scope.add_event_processor(processor)
120131

121-
transaction = continue_trace(
122-
headers,
123-
op=OP.HTTP_SERVER,
124-
# Like with all other integrations, this is our
125-
# fallback transaction in case there is no route.
126-
# sentry_urldispatcher_resolve is responsible for
127-
# setting a transaction name later.
128-
name="generic Tornado request",
129-
source=TransactionSource.ROUTE,
130-
origin=TornadoIntegration.origin,
131-
)
132-
133-
with sentry_sdk.start_transaction(
134-
transaction, custom_sampling_context={"tornado_request": self.request}
135-
):
136-
yield
132+
span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]"
133+
134+
if span_streaming:
135+
sentry_sdk.traces.continue_trace(dict(headers))
136+
scope.set_custom_sampling_context({"tornado_request": self.request})
137+
138+
span_ctx = sentry_sdk.traces.start_span(
139+
name=_DEFAULT_TRANSACTION_NAME,
140+
attributes={
141+
"sentry.op": OP.HTTP_SERVER,
142+
"sentry.origin": TornadoIntegration.origin,
143+
"sentry.span.source": SegmentSource.ROUTE,
144+
},
145+
)
146+
else:
147+
transaction = continue_trace(
148+
headers,
149+
op=OP.HTTP_SERVER,
150+
# Like with all other integrations, this is our
151+
# fallback transaction in case there is no route.
152+
# sentry_urldispatcher_resolve is responsible for
153+
# setting a transaction name later.
154+
name=_DEFAULT_TRANSACTION_NAME,
155+
source=TransactionSource.ROUTE,
156+
origin=TornadoIntegration.origin,
157+
)
158+
span_ctx = sentry_sdk.start_transaction(
159+
transaction,
160+
custom_sampling_context={"tornado_request": self.request},
161+
)
162+
163+
with span_ctx as span:
164+
if isinstance(span, StreamedSpan):
165+
with capture_internal_exceptions():
166+
for attr, value in _get_request_attributes(self.request).items():
167+
span.set_attribute(attr, value)
168+
169+
method = getattr(self, self.request.method.lower(), None)
170+
if method is not None:
171+
tx_name = transaction_from_function(method) or ""
172+
if tx_name:
173+
span.name = tx_name
174+
span.set_attribute(
175+
"sentry.span.source",
176+
SegmentSource.COMPONENT.value,
177+
)
178+
179+
try:
180+
yield
181+
finally:
182+
if isinstance(span, StreamedSpan):
183+
with capture_internal_exceptions():
184+
status_int = self.get_status()
185+
span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int)
186+
span.status = "error" if status_int >= 400 else "ok"
187+
188+
189+
def _get_request_attributes(request: "Any") -> "Dict[str, Any]":
190+
attributes = {} # type: Dict[str, Any]
191+
192+
if request.method:
193+
attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper()
194+
195+
headers = _filter_headers(dict(request.headers), use_annotated_value=False)
196+
for header, value in headers.items():
197+
attributes[f"http.request.header.{header.lower()}"] = value
198+
199+
if request.query:
200+
attributes[SPANDATA.HTTP_QUERY] = request.query
201+
202+
attributes[SPANDATA.URL_FULL] = "%s://%s%s" % (
203+
request.protocol,
204+
request.host,
205+
request.path,
206+
)
207+
208+
if request.protocol:
209+
attributes["network.protocol.name"] = request.protocol
210+
211+
if should_send_default_pii() and request.remote_ip:
212+
attributes["client.address"] = request.remote_ip
213+
attributes["user.ip_address"] = request.remote_ip
214+
215+
return attributes
137216

138217

139218
@ensure_integration_enabled(TornadoIntegration)

0 commit comments

Comments
 (0)