Skip to content

Commit 95ca9a3

Browse files
mergify[bot]Kyle-Verhoogmajorgreys
authored
Store span in ASGI scope (#2725) (#2754)
* Store span in ASGI scope Motivation ---------- In the Django integration we'd like to be able to access the ASGI span from the scope of the request to modify it with additional tags which aren't accessible from the span modifier. * Add no span test case * Add pytest mark (cherry picked from commit c5b5bd2) Co-authored-by: Kyle Verhoog <[email protected]> Co-authored-by: Tahir H. Butt <[email protected]>
1 parent 643d6f4 commit 95ca9a3

File tree

4 files changed

+78
-1
lines changed

4 files changed

+78
-1
lines changed

ddtrace/contrib/asgi/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,6 @@ def custom_handle_exception_span(exc, span):
5858
with require_modules(required_modules) as missing_modules:
5959
if not missing_modules:
6060
from .middleware import TraceMiddleware
61+
from .middleware import span_from_scope
6162

62-
__all__ = ["TraceMiddleware"]
63+
__all__ = ["TraceMiddleware", "span_from_scope"]

ddtrace/contrib/asgi/middleware.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from typing import TYPE_CHECKING
23

34
import ddtrace
45
from ddtrace import config
@@ -12,6 +13,14 @@
1213
from .utils import guarantee_single_callable
1314

1415

16+
if TYPE_CHECKING:
17+
from typing import Any
18+
from typing import Mapping
19+
from typing import Optional
20+
21+
from ddtrace import Span
22+
23+
1524
log = get_logger(__name__)
1625

1726
config._add(
@@ -58,6 +67,12 @@ def _default_handle_exception_span(exc, span):
5867
span.set_tag(http.STATUS_CODE, 500)
5968

6069

70+
def span_from_scope(scope):
71+
# type: (Mapping[str, Any]) -> Optional[Span]
72+
"""Retrieve the top-level ASGI span from the scope."""
73+
return scope.get("datadog_span")
74+
75+
6176
class TraceMiddleware:
6277
"""
6378
ASGI application middleware that traces the requests.
@@ -104,6 +119,8 @@ async def __call__(self, scope, receive, send):
104119
span_type=SpanTypes.WEB,
105120
)
106121

122+
scope["datadog_span"] = span
123+
107124
if self.span_modifier:
108125
self.span_modifier(span, scope)
109126

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
ASGI: store the ASGI span in the scope. The span is stored under the
5+
``"datadog_span"`` key and can be retrieved using the
6+
``ddtrace.contrib.asgi.span_from_scope`` function.

tests/contrib/asgi/test_asgi.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99

1010
from ddtrace.contrib.asgi import TraceMiddleware
11+
from ddtrace.contrib.asgi import span_from_scope
1112
from ddtrace.propagation import http as http_propagation
1213
from tests.utils import DummyTracer
1314
from tests.utils import override_http_config
@@ -362,3 +363,55 @@ async def test_bad_headers(scope, tracer, test_spans):
362363
"type": "http.response.body",
363364
"body": b"*",
364365
}
366+
367+
368+
@pytest.mark.asyncio
369+
async def test_get_asgi_span(tracer, test_spans):
370+
async def test_app(scope, receive, send):
371+
message = await receive()
372+
if message.get("type") == "http.request":
373+
asgi_span = span_from_scope(scope)
374+
assert asgi_span is not None
375+
assert asgi_span.name == "asgi.request"
376+
await send({"type": "http.response.start", "status": 200, "headers": [[b"Content-Type", b"text/plain"]]})
377+
await send({"type": "http.response.body", "body": b""})
378+
379+
app = TraceMiddleware(test_app, tracer=tracer)
380+
async with httpx.AsyncClient(app=app) as client:
381+
response = await client.get("http://testserver/")
382+
assert response.status_code == 200
383+
384+
with override_http_config("asgi", dict(trace_query_string=True)):
385+
app = TraceMiddleware(test_app, tracer=tracer)
386+
async with httpx.AsyncClient(app=app) as client:
387+
response = await client.get("http://testserver/")
388+
assert response.status_code == 200
389+
390+
async def test_app(scope, receive, send):
391+
message = await receive()
392+
if message.get("type") == "http.request":
393+
root = tracer.current_root_span()
394+
assert root.name == "root"
395+
asgi_span = span_from_scope(scope)
396+
assert asgi_span is not None
397+
assert asgi_span.name == "asgi.request"
398+
await send({"type": "http.response.start", "status": 200, "headers": [[b"Content-Type", b"text/plain"]]})
399+
await send({"type": "http.response.body", "body": b""})
400+
401+
app = TraceMiddleware(test_app, tracer=tracer)
402+
async with httpx.AsyncClient(app=app) as client:
403+
with tracer.trace("root"):
404+
response = await client.get("http://testserver/")
405+
assert response.status_code == 200
406+
407+
async def test_app_no_middleware(scope, receive, send):
408+
message = await receive()
409+
if message.get("type") == "http.request":
410+
asgi_span = span_from_scope(scope)
411+
assert asgi_span is None
412+
await send({"type": "http.response.start", "status": 200, "headers": [[b"Content-Type", b"text/plain"]]})
413+
await send({"type": "http.response.body", "body": b""})
414+
415+
async with httpx.AsyncClient(app=test_app_no_middleware) as client:
416+
response = await client.get("http://testserver/")
417+
assert response.status_code == 200

0 commit comments

Comments
 (0)