44
55import sentry_sdk
66from sentry_sdk .api import continue_trace
7- from sentry_sdk .consts import OP
7+ from sentry_sdk .consts import OP , SPANDATA
88from sentry_sdk .integrations import DidNotEnable , Integration , _check_minimum_version
99from sentry_sdk .integrations ._wsgi_common import (
1010 RequestExtractor ,
1313)
1414from sentry_sdk .integrations .logging import ignore_logger
1515from sentry_sdk .scope import should_send_default_pii
16+ from sentry_sdk .traces import SegmentSource , StreamedSpan
1617from sentry_sdk .tracing import TransactionSource
18+ from sentry_sdk .tracing_utils import has_span_streaming_enabled
1719from sentry_sdk .utils import (
1820 CONTEXTVARS_ERROR_MESSAGE ,
1921 HAS_REAL_CONTEXTVARS ,
3335from typing import TYPE_CHECKING
3436
3537if 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
4144class 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
101107def _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