Skip to content

Commit e103926

Browse files
authored
feat: Make ASGI support span first (#5680)
This PR makes the ASGI integration work both in legacy and span streaming mode. Some features and attributes will be missing in span streaming mode for now (see the Out of scope section below). Best reviewed with whitespace ignored: https://github.com/getsentry/sentry-python/pull/5680/changes?w=1 --- A bit of a background on migrating integrations to span first. In order to support both legacy spans and span streaming, most integrations will follow the same patterns: ### API We need to use the `start_span` API from `sentry_sdk.traces` if we're in span streaming mode (`traces_lifecycle="stream"`). There are no transactions anymore. Top-level spans will also be started via the `start_span` API in span streaming mode. ### Setting data on spans If an integration sets data on a span (via `span.set_data`, `span.set_tag` etc.), it should use `span.set_attribute` when span streaming is enabled. The attributes that we set need to be in Sentry conventions. This is deliberately not the case for most quick ports of integrations like this one and will follow in [a future step](#5152). ### Trace propagation If an integration sits at a service boundary and is capable of propagating incoming trace information (like WSGI/ASGI or Celery), in span first mode we need to switch from the old style `with continue_trace(...) as transaction:` to the [new style `continue_trace()` and `new_trace()`](https://sentry-docs-git-ivana-span-first-migration-guide.sentry.dev/platforms/python/migration/span-first/#trace-propagation) (not context managers). ### `start_span` arguments You can pass things like `op`, `origin`, `source` to the old `start_span` API. With the new API, this is no longer possible, and the individual properties need to be set as attributes directly. ### Out of scope For now, the following is out of scope and will follow in the future: - Making sure all attributes are correct and that they're set in Sentry conventions: #5152 - Migrating event processors (as streaming spans are not events, event processors are not run on them, meaning some data will not be set yet): #5152
1 parent dc65e13 commit e103926

File tree

1 file changed

+92
-27
lines changed

1 file changed

+92
-27
lines changed

sentry_sdk/integrations/asgi.py

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@
2222
nullcontext,
2323
)
2424
from sentry_sdk.sessions import track_session
25+
from sentry_sdk.traces import StreamedSpan
2526
from sentry_sdk.tracing import (
2627
SOURCE_FOR_STYLE,
28+
Transaction,
2729
TransactionSource,
2830
)
31+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
2932
from sentry_sdk.utils import (
3033
ContextVar,
3134
event_from_exception,
@@ -35,17 +38,19 @@
3538
transaction_from_function,
3639
_get_installed_modules,
3740
)
38-
from sentry_sdk.tracing import Transaction
3941

4042
from typing import TYPE_CHECKING
4143

4244
if TYPE_CHECKING:
4345
from typing import Any
46+
from typing import ContextManager
4447
from typing import Dict
4548
from typing import Optional
4649
from typing import Tuple
50+
from typing import Union
4751

48-
from sentry_sdk._types import Event, Hint
52+
from sentry_sdk._types import Attributes, Event, Hint
53+
from sentry_sdk.tracing import Span
4954

5055

5156
_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied")
@@ -185,6 +190,9 @@ async def _run_app(
185190
self._capture_lifespan_exception(exc)
186191
raise exc from None
187192

193+
client = sentry_sdk.get_client()
194+
span_streaming = has_span_streaming_enabled(client.options)
195+
188196
_asgi_middleware_applied.set(True)
189197
try:
190198
with sentry_sdk.isolation_scope() as sentry_scope:
@@ -204,48 +212,105 @@ async def _run_app(
204212
)
205213

206214
method = scope.get("method", "").upper()
207-
transaction = None
208-
if ty in ("http", "websocket"):
209-
if ty == "websocket" or method in self.http_methods_to_capture:
210-
transaction = continue_trace(
211-
_get_headers(scope),
212-
op="{}.server".format(ty),
215+
216+
span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]"
217+
if span_streaming:
218+
segment: "Optional[StreamedSpan]" = None
219+
attributes: "Attributes" = {
220+
"sentry.span.source": getattr(
221+
transaction_source, "value", transaction_source
222+
),
223+
"sentry.origin": self.span_origin,
224+
"asgi.type": ty,
225+
}
226+
227+
if ty in ("http", "websocket"):
228+
if (
229+
ty == "websocket"
230+
or method in self.http_methods_to_capture
231+
):
232+
sentry_sdk.traces.continue_trace(_get_headers(scope))
233+
234+
sentry_scope.set_custom_sampling_context(
235+
{"asgi_scope": scope}
236+
)
237+
238+
attributes["sentry.op"] = f"{ty}.server"
239+
segment = sentry_sdk.traces.start_span(
240+
name=transaction_name, attributes=attributes
241+
)
242+
else:
243+
sentry_sdk.traces.new_trace()
244+
245+
sentry_scope.set_custom_sampling_context(
246+
{"asgi_scope": scope}
247+
)
248+
249+
attributes["sentry.op"] = OP.HTTP_SERVER
250+
segment = sentry_sdk.traces.start_span(
251+
name=transaction_name, attributes=attributes
252+
)
253+
254+
span_ctx = segment or nullcontext()
255+
256+
else:
257+
transaction = None
258+
if ty in ("http", "websocket"):
259+
if (
260+
ty == "websocket"
261+
or method in self.http_methods_to_capture
262+
):
263+
transaction = continue_trace(
264+
_get_headers(scope),
265+
op="{}.server".format(ty),
266+
name=transaction_name,
267+
source=transaction_source,
268+
origin=self.span_origin,
269+
)
270+
else:
271+
transaction = Transaction(
272+
op=OP.HTTP_SERVER,
213273
name=transaction_name,
214274
source=transaction_source,
215275
origin=self.span_origin,
216276
)
217-
else:
218-
transaction = Transaction(
219-
op=OP.HTTP_SERVER,
220-
name=transaction_name,
221-
source=transaction_source,
222-
origin=self.span_origin,
223-
)
224277

225-
if transaction:
226-
transaction.set_tag("asgi.type", ty)
278+
if transaction:
279+
transaction.set_tag("asgi.type", ty)
227280

228-
transaction_context = (
229-
sentry_sdk.start_transaction(
230-
transaction,
231-
custom_sampling_context={"asgi_scope": scope},
281+
span_ctx = (
282+
sentry_sdk.start_transaction(
283+
transaction,
284+
custom_sampling_context={"asgi_scope": scope},
285+
)
286+
if transaction is not None
287+
else nullcontext()
232288
)
233-
if transaction is not None
234-
else nullcontext()
235-
)
236-
with transaction_context:
289+
290+
with span_ctx as span:
237291
try:
238292

239293
async def _sentry_wrapped_send(
240294
event: "Dict[str, Any]",
241295
) -> "Any":
242-
if transaction is not None:
296+
if span is not None:
243297
is_http_response = (
244298
event.get("type") == "http.response.start"
245299
and "status" in event
246300
)
247301
if is_http_response:
248-
transaction.set_http_status(event["status"])
302+
if isinstance(span, StreamedSpan):
303+
span.status = (
304+
"error"
305+
if event["status"] >= 400
306+
else "ok"
307+
)
308+
span.set_attribute(
309+
"http.response.status_code",
310+
event["status"],
311+
)
312+
else:
313+
span.set_http_status(event["status"])
249314

250315
return await send(event)
251316

0 commit comments

Comments
 (0)