Skip to content

Commit 6c6ac09

Browse files
authored
Use semantic attributes in traces sampler for ASGI spans (#3774)
1 parent 13ec94f commit 6c6ac09

File tree

5 files changed

+58
-20
lines changed

5 files changed

+58
-20
lines changed

MIGRATION_GUIDE.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
2020
- clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`).
2121
- `sentry_sdk.init` now returns `None` instead of a context manager.
2222
- The `sampling_context` argument of `traces_sampler` now additionally contains all span attributes known at span start.
23-
- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties, if available, are accessible as `asgi_scope.endpoint`, `asgi_scope.path`, `asgi_scope.root_path`, `asgi_scope.route`, `asgi_scope.scheme`, `asgi_scope.server` and `asgi_scope.type`.
23+
- The `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore for ASGI frameworks. Instead, the individual properties on the scope, if available, are accessible as follows:
24+
25+
| Scope property | Sampling context key(s) |
26+
| -------------- | ------------------------------- |
27+
| `type` | `network.protocol.name` |
28+
| `scheme` | `url.scheme` |
29+
| `path` | `url.path` |
30+
| `http_version` | `network.protocol.version` |
31+
| `method` | `http.request.method` |
32+
| `server` | `server.address`, `server.port` |
33+
| `client` | `client.address`, `client.port` |
34+
| full URL | `url.full` |
2435

2536
### Removed
2637

sentry_sdk/integrations/_asgi_common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def _get_headers(asgi_scope):
3232
return headers
3333

3434

35-
def _get_url(asgi_scope, default_scheme, host):
36-
# type: (Dict[str, Any], Literal["ws", "http"], Optional[Union[AnnotatedValue, str]]) -> str
35+
def _get_url(asgi_scope, default_scheme=None, host=None):
36+
# type: (Dict[str, Any], Optional[Literal["ws", "http"]], Optional[Union[AnnotatedValue, str]]) -> str
3737
"""
3838
Extract URL from the ASGI scope, without also including the querystring.
3939
"""

sentry_sdk/integrations/asgi.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from sentry_sdk.integrations._asgi_common import (
1717
_get_headers,
18+
_get_query,
1819
_get_request_data,
1920
_get_url,
2021
)
@@ -57,6 +58,14 @@
5758

5859
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
5960

61+
ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE = {
62+
"http_version": "network.protocol.version",
63+
"method": "http.request.method",
64+
"path": "url.path",
65+
"scheme": "url.scheme",
66+
"type": "network.protocol.name",
67+
}
68+
6069

6170
def _capture_exception(exc, mechanism_type="asgi"):
6271
# type: (Any, str) -> None
@@ -213,23 +222,21 @@ async def _run_app(self, scope, receive, send, asgi_version):
213222
)
214223
if should_trace
215224
else nullcontext()
216-
) as transaction:
217-
if transaction is not None:
218-
logger.debug(
219-
"[ASGI] Started transaction: %s", transaction
220-
)
221-
transaction.set_tag("asgi.type", ty)
225+
) as span:
226+
if span is not None:
227+
logger.debug("[ASGI] Started transaction: %s", span)
228+
span.set_tag("asgi.type", ty)
222229
try:
223230

224231
async def _sentry_wrapped_send(event):
225232
# type: (Dict[str, Any]) -> Any
226233
is_http_response = (
227234
event.get("type") == "http.response.start"
228-
and transaction is not None
235+
and span is not None
229236
and "status" in event
230237
)
231238
if is_http_response:
232-
transaction.set_http_status(event["status"])
239+
span.set_http_status(event["status"])
233240

234241
return await send(event)
235242

@@ -328,12 +335,31 @@ def _get_transaction_name_and_source(self, transaction_style, asgi_scope):
328335

329336
def _prepopulate_attributes(scope):
330337
# type: (Any) -> dict[str, Any]
331-
"""Unpack asgi_scope into serializable attributes."""
338+
"""Unpack ASGI scope into serializable OTel attributes."""
332339
scope = scope or {}
333340

334341
attributes = {}
335-
for attr in ("endpoint", "path", "root_path", "route", "scheme", "server", "type"):
342+
for attr, key in ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE.items():
336343
if scope.get(attr):
337-
attributes[f"asgi_scope.{attr}"] = scope[attr]
344+
attributes[key] = scope[attr]
345+
346+
for attr in ("client", "server"):
347+
if scope.get(attr):
348+
try:
349+
host, port = scope[attr]
350+
attributes[f"{attr}.address"] = host
351+
attributes[f"{attr}.port"] = port
352+
except Exception:
353+
pass
354+
355+
try:
356+
full_url = _get_url(scope)
357+
query = _get_query(scope)
358+
if query:
359+
full_url = f"{full_url}?{query}"
360+
361+
attributes["url.full"] = full_url
362+
except Exception:
363+
pass
338364

339365
return attributes

tests/integrations/asgi/test_asgi.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,12 @@ async def test_custom_transaction_name(
726726
@pytest.mark.asyncio
727727
async def test_asgi_scope_in_traces_sampler(sentry_init, asgi3_app):
728728
def dummy_traces_sampler(sampling_context):
729-
assert sampling_context["asgi_scope.path"] == "/test"
730-
assert sampling_context["asgi_scope.scheme"] == "http"
729+
assert sampling_context["url.path"] == "/test"
730+
assert sampling_context["url.scheme"] == "http"
731+
assert sampling_context["url.full"] == "/test?hello=there"
732+
assert sampling_context["http.request.method"] == "GET"
733+
assert sampling_context["network.protocol.version"] == "1.1"
734+
assert sampling_context["network.protocol.name"] == "http"
731735

732736
sentry_init(
733737
traces_sampler=dummy_traces_sampler,
@@ -737,4 +741,4 @@ def dummy_traces_sampler(sampling_context):
737741
app = SentryAsgiMiddleware(asgi3_app)
738742

739743
async with TestClient(app) as client:
740-
await client.get("/test")
744+
await client.get("/test?hello=there")

tests/integrations/fastapi/test_fastapi.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,6 @@ async def _error(request: Request):
246246
assert event["request"]["headers"]["authorization"] == "[Filtered]"
247247

248248

249-
@pytest.mark.asyncio
250249
def test_response_status_code_ok_in_transaction_context(sentry_init, capture_envelopes):
251250
"""
252251
Tests that the response status code is added to the transaction "response" context.
@@ -275,7 +274,6 @@ def test_response_status_code_ok_in_transaction_context(sentry_init, capture_env
275274
assert transaction["contexts"]["response"]["status_code"] == 200
276275

277276

278-
@pytest.mark.asyncio
279277
def test_response_status_code_error_in_transaction_context(
280278
sentry_init,
281279
capture_envelopes,
@@ -312,7 +310,6 @@ def test_response_status_code_error_in_transaction_context(
312310
assert transaction["contexts"]["response"]["status_code"] == 500
313311

314312

315-
@pytest.mark.asyncio
316313
def test_response_status_code_not_found_in_transaction_context(
317314
sentry_init,
318315
capture_envelopes,

0 commit comments

Comments
 (0)