Skip to content

Commit 62f5e8a

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) <[email protected]>
1 parent 015ff31 commit 62f5e8a

2 files changed

Lines changed: 240 additions & 78 deletions

File tree

sentry_sdk/integrations/tornado.py

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
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.integrations import DidNotEnable, Integration, _check_minimum_version
99
from sentry_sdk.integrations._wsgi_common import (
1010
RequestExtractor,
@@ -13,7 +13,9 @@
1313
)
1414
from sentry_sdk.integrations.logging import ignore_logger
1515
from sentry_sdk.scope import should_send_default_pii
16+
from sentry_sdk.traces import SegmentSource, StreamedSpan
1617
from sentry_sdk.tracing import TransactionSource
18+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1719
from sentry_sdk.utils import (
1820
CONTEXTVARS_ERROR_MESSAGE,
1921
HAS_REAL_CONTEXTVARS,
@@ -33,9 +35,10 @@
3335
from typing import TYPE_CHECKING
3436

3537
if TYPE_CHECKING:
36-
from typing import Any, Callable, Dict, Generator, Optional
38+
from typing import Any, Callable, ContextManager, Dict, Generator, Optional, Union
3739

3840
from sentry_sdk._types import Event, EventProcessor
41+
from sentry_sdk.tracing import Span
3942

4043

4144
class TornadoIntegration(Integration):
@@ -97,6 +100,9 @@ def sentry_log_exception(
97100
RequestHandler.log_exception = sentry_log_exception
98101

99102

103+
_DEFAULT_TRANSACTION_NAME = "generic Tornado request"
104+
105+
100106
@contextlib.contextmanager
101107
def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]":
102108
integration = sentry_sdk.get_client().get_integration(TornadoIntegration)
@@ -106,6 +112,8 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]
106112
return
107113

108114
weak_handler = weakref.ref(self)
115+
client = sentry_sdk.get_client()
116+
span_streaming = has_span_streaming_enabled(client.options)
109117

110118
with sentry_sdk.isolation_scope() as scope:
111119
headers = self.request.headers
@@ -114,22 +122,90 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]
114122
processor = _make_event_processor(weak_handler)
115123
scope.add_event_processor(processor)
116124

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

134210

135211
@ensure_integration_enabled(TornadoIntegration)

0 commit comments

Comments
 (0)