Skip to content

Commit 822b244

Browse files
authored
feat(wsgi): Migrate to span first (#5988)
### Description Make the WSGI integration span first ready. Also, set an additional attribute (`user.ip_address`) in ASGI. Recommended to review with Ignore whitespace on. #### Issues * Closes #6072 * Closes https://linear.app/getsentry/issue/PY-2374/migrate-wsgi-to-span-first
1 parent 0dbc676 commit 822b244

File tree

4 files changed

+412
-105
lines changed

4 files changed

+412
-105
lines changed

sentry_sdk/integrations/_asgi_common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]":
132132

133133
client = asgi_scope.get("client")
134134
if client and should_send_default_pii():
135-
attributes["client.address"] = _get_ip(asgi_scope)
135+
ip = _get_ip(asgi_scope)
136+
attributes["client.address"] = ip
137+
attributes["user.ip_address"] = ip
136138

137139
return attributes

sentry_sdk/integrations/wsgi.py

Lines changed: 131 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
)
1414
from sentry_sdk.scope import should_send_default_pii, use_isolation_scope
1515
from sentry_sdk.sessions import track_session
16-
from sentry_sdk.tracing import Transaction, TransactionSource
16+
from sentry_sdk.traces import StreamedSpan, SegmentSource
17+
from sentry_sdk.tracing import Span, TransactionSource
18+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1719
from sentry_sdk.utils import (
1820
ContextVar,
1921
capture_internal_exceptions,
@@ -22,7 +24,18 @@
2224
)
2325

2426
if TYPE_CHECKING:
25-
from typing import Any, Callable, Dict, Iterator, Optional, Protocol, Tuple, TypeVar
27+
from typing import (
28+
Any,
29+
Callable,
30+
ContextManager,
31+
Dict,
32+
Iterator,
33+
Optional,
34+
Protocol,
35+
Tuple,
36+
TypeVar,
37+
Union,
38+
)
2639

2740
from sentry_sdk._types import Event, EventProcessor
2841
from sentry_sdk.utils import ExcInfo
@@ -42,6 +55,7 @@ def __call__(
4255

4356

4457
_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
58+
_DEFAULT_TRANSACTION_NAME = "generic WSGI request"
4559

4660

4761
def wsgi_decoding_dance(s: str, charset: str = "utf-8", errors: str = "replace") -> str:
@@ -94,6 +108,9 @@ def __call__(
94108
if _wsgi_middleware_applied.get(False):
95109
return self.app(environ, start_response)
96110

111+
client = sentry_sdk.get_client()
112+
span_streaming = has_span_streaming_enabled(client.options)
113+
97114
_wsgi_middleware_applied.set(True)
98115
try:
99116
with sentry_sdk.isolation_scope() as scope:
@@ -108,34 +125,72 @@ def __call__(
108125
)
109126

110127
method = environ.get("REQUEST_METHOD", "").upper()
111-
transaction = None
128+
129+
span_ctx: "Optional[ContextManager[Union[Span, StreamedSpan, None]]]" = None
112130
if method in self.http_methods_to_capture:
113-
transaction = continue_trace(
114-
environ,
115-
op=OP.HTTP_SERVER,
116-
name="generic WSGI request",
117-
source=TransactionSource.ROUTE,
118-
origin=self.span_origin,
119-
)
131+
if span_streaming:
132+
sentry_sdk.traces.continue_trace(
133+
dict(_get_headers(environ))
134+
)
135+
scope.set_custom_sampling_context({"wsgi_environ": environ})
136+
137+
span_ctx = sentry_sdk.traces.start_span(
138+
name=_DEFAULT_TRANSACTION_NAME,
139+
attributes={
140+
"sentry.span.source": SegmentSource.ROUTE,
141+
"sentry.origin": self.span_origin,
142+
"sentry.op": OP.HTTP_SERVER,
143+
},
144+
)
145+
else:
146+
transaction = continue_trace(
147+
environ,
148+
op=OP.HTTP_SERVER,
149+
name=_DEFAULT_TRANSACTION_NAME,
150+
source=TransactionSource.ROUTE,
151+
origin=self.span_origin,
152+
)
153+
154+
span_ctx = sentry_sdk.start_transaction(
155+
transaction,
156+
custom_sampling_context={"wsgi_environ": environ},
157+
)
158+
159+
span_ctx = span_ctx or nullcontext()
160+
161+
with span_ctx as span:
162+
if isinstance(span, StreamedSpan):
163+
with capture_internal_exceptions():
164+
for attr, value in _get_request_attributes(
165+
environ, self.use_x_forwarded_for
166+
).items():
167+
span.set_attribute(attr, value)
120168

121-
transaction_context = (
122-
sentry_sdk.start_transaction(
123-
transaction,
124-
custom_sampling_context={"wsgi_environ": environ},
125-
)
126-
if transaction is not None
127-
else nullcontext()
128-
)
129-
with transaction_context:
130169
try:
131170
response = self.app(
132171
environ,
133-
partial(
134-
_sentry_start_response, start_response, transaction
135-
),
172+
partial(_sentry_start_response, start_response, span),
136173
)
137174
except BaseException:
138175
reraise(*_capture_exception())
176+
finally:
177+
if isinstance(span, StreamedSpan):
178+
already_set = (
179+
span.name != _DEFAULT_TRANSACTION_NAME
180+
and span.get_attributes().get("sentry.span.source")
181+
in [
182+
SegmentSource.COMPONENT.value,
183+
SegmentSource.ROUTE.value,
184+
SegmentSource.CUSTOM.value,
185+
]
186+
)
187+
if not already_set:
188+
with capture_internal_exceptions():
189+
span.name = _DEFAULT_TRANSACTION_NAME
190+
span.set_attribute(
191+
"sentry.span.source",
192+
SegmentSource.ROUTE.value,
193+
)
139194
finally:
140195
_wsgi_middleware_applied.set(False)
141196

@@ -167,15 +222,19 @@ def __call__(
167222

168223
def _sentry_start_response(
169224
old_start_response: "StartResponse",
170-
transaction: "Optional[Transaction]",
225+
span: "Optional[Union[Span, StreamedSpan]]",
171226
status: str,
172227
response_headers: "WsgiResponseHeaders",
173228
exc_info: "Optional[WsgiExcInfo]" = None,
174229
) -> "WsgiResponseIter": # type: ignore[type-var]
175230
with capture_internal_exceptions():
176231
status_int = int(status.split(" ", 1)[0])
177-
if transaction is not None:
178-
transaction.set_http_status(status_int)
232+
if span is not None:
233+
if isinstance(span, StreamedSpan):
234+
span.status = "error" if status_int >= 400 else "ok"
235+
span.set_attribute("http.response.status_code", status_int)
236+
else:
237+
span.set_http_status(status_int)
179238

180239
if exc_info is None:
181240
# The Django Rest Framework WSGI test client, and likely other
@@ -326,3 +385,50 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
326385
return event
327386

328387
return event_processor
388+
389+
390+
def _get_request_attributes(
391+
environ: "Dict[str, str]",
392+
use_x_forwarded_for: bool = False,
393+
) -> "Dict[str, Any]":
394+
"""
395+
Return span attributes related to the HTTP request from the WSGI environ.
396+
"""
397+
attributes: "dict[str, Any]" = {}
398+
399+
method = environ.get("REQUEST_METHOD")
400+
if method:
401+
attributes["http.request.method"] = method.upper()
402+
403+
headers = _filter_headers(dict(_get_headers(environ)), use_annotated_value=False)
404+
for header, value in headers.items():
405+
attributes[f"http.request.header.{header.lower()}"] = value
406+
407+
query_string = environ.get("QUERY_STRING")
408+
if query_string:
409+
attributes["http.query"] = query_string
410+
411+
attributes["url.full"] = get_request_url(environ, use_x_forwarded_for)
412+
413+
url_scheme = environ.get("wsgi.url_scheme")
414+
if url_scheme:
415+
attributes["network.protocol.name"] = url_scheme
416+
417+
server_name = environ.get("SERVER_NAME")
418+
if server_name:
419+
attributes["server.address"] = server_name
420+
421+
server_port = environ.get("SERVER_PORT")
422+
if server_port:
423+
try:
424+
attributes["server.port"] = int(server_port)
425+
except ValueError:
426+
pass
427+
428+
if should_send_default_pii():
429+
client_ip = get_client_ip(environ)
430+
if client_ip:
431+
attributes["client.address"] = client_ip
432+
attributes["user.ip_address"] = client_ip
433+
434+
return attributes

sentry_sdk/scope.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ def get_active_propagation_context(self) -> "PropagationContext":
714714
def set_custom_sampling_context(
715715
self, custom_sampling_context: "dict[str, Any]"
716716
) -> None:
717-
self.get_active_propagation_context()._set_custom_sampling_context(
717+
self.get_current_scope().get_active_propagation_context()._set_custom_sampling_context(
718718
custom_sampling_context
719719
)
720720

0 commit comments

Comments
 (0)