Skip to content

Commit d191ded

Browse files
authored
fix(asgi): ensure error tags are set on request spans (backport #5818 to 1.13) (#5839)
Backports: #5818 ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines) are followed. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] OPTIONAL: PR description includes explicit acknowledgement of the performance implications of the change as reported in the benchmarks PR comment. ## Reviewer Checklist - [x] Title is accurate. - [x] No unnecessary changes are introduced. - [x] Description motivates each change. - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Testing strategy adequately addresses listed risk(s). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] Release note makes sense to a user of the library. - [x] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment.
1 parent 2b4a835 commit d191ded

File tree

6 files changed

+54
-46
lines changed

6 files changed

+54
-46
lines changed

ddtrace/contrib/asgi/middleware.py

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,7 @@ async def wrapped_send(message):
213213
trace_utils.set_http_meta(
214214
span, self.integration_config, status_code=status_code, response_headers=response_headers
215215
)
216-
217-
try:
218-
return await send(message)
219-
finally:
220-
# Per asgi spec, "more_body" is used if there is still data to send
221-
# Close the span if "http.response.body" has no more data left to send in the
222-
# response.
223-
if message.get("type") == "http.response.body" and not message.get("more_body", False):
224-
span.finish()
216+
return await send(message)
225217

226218
async def wrapped_blocked_send(message):
227219
accept_header = (
@@ -243,11 +235,7 @@ async def wrapped_blocked_send(message):
243235
message["body"] = bytes(appsec_utils._get_blocked_template(accept_header), "utf-8")
244236
message["more_body"] = False
245237

246-
try:
247-
return await send(message)
248-
finally:
249-
if message.get("type") == "http.response.body":
250-
span.finish()
238+
return await send(message)
251239

252240
try:
253241
if _request_blocked(span):
@@ -259,9 +247,6 @@ async def wrapped_blocked_send(message):
259247
self.handle_exception_span(exc, span)
260248
reraise(exc_type, exc_val, exc_tb)
261249
finally:
262-
try:
263-
del scope["datadog"]["request_span"]
264-
except KeyError:
265-
pass
266-
250+
if span in scope["datadog"]["request_spans"]:
251+
scope["datadog"]["request_spans"].remove(span)
267252
span.finish()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
asgi: Ensures ``error.message`` and ``error.stack`` tags are set when an exception is raised in a route.

tests/contrib/asgi/test_asgi.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,10 @@ async def tasks_app_with_more_body(scope, receive, send):
127127
await send({"type": "http.response.body", "body": b"*", "more_body": True})
128128
assert not request_span.finished
129129

130-
# assert that the span has finished after more_body is False
130+
# assert that the span has not finished after more_body. Span should be finished after
131+
# the ddtrace trace middleware is exited.
131132
await send({"type": "http.response.body", "body": b"*", "more_body": False})
132-
assert request_span.finished
133+
assert not request_span.finished
133134
await asyncio.sleep(1)
134135

135136

@@ -524,7 +525,7 @@ async def test_tasks_asgi_without_more_body(scope, tracer, test_spans):
524525
assert request_span.span_type == "web"
525526
# typical duration without background task should be in less than 10 ms
526527
# duration with background task will take approximately 1.1s
527-
assert request_span.duration < 1
528+
assert request_span.duration < 1.1
528529

529530

530531
@pytest.mark.asyncio
@@ -547,7 +548,7 @@ async def test_tasks_asgi_with_more_body(scope, tracer, test_spans):
547548
assert request_span.span_type == "web"
548549
# typical duration without background task should be in less than 10 ms
549550
# duration with background task will take approximately 1.1s
550-
assert request_span.duration < 1
551+
assert request_span.duration < 1.1
551552

552553

553554
@pytest.mark.asyncio

tests/contrib/fastapi/test_fastapi.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import ddtrace
99
from ddtrace import config
10-
from ddtrace.constants import ERROR_MSG
1110
from ddtrace.contrib.fastapi import patch as fastapi_patch
1211
from ddtrace.contrib.fastapi import unpatch as fastapi_unpatch
1312
from ddtrace.contrib.starlette.patch import patch as patch_starlette
@@ -341,27 +340,10 @@ def test_invalid_path(client, tracer, test_spans):
341340
assert request_span.get_tag("span.kind") == "server"
342341

343342

344-
def test_500_error_raised(client, tracer, test_spans):
343+
@snapshot(ignores=["meta.error.stack"])
344+
def test_500_error_raised(snapshot_client):
345345
with pytest.raises(RuntimeError):
346-
client.get("/500", headers={"X-Token": "DataDog"})
347-
spans = test_spans.pop_traces()
348-
assert len(spans) == 1
349-
assert len(spans[0]) == 1
350-
351-
request_span = spans[0][0]
352-
assert request_span.service == "fastapi"
353-
assert request_span.name == "fastapi.request"
354-
assert request_span.resource == "GET /500"
355-
assert request_span.get_tag("http.route") == "/500"
356-
assert request_span.error == 1
357-
assert request_span.get_tag("http.method") == "GET"
358-
assert request_span.get_tag("http.url") == "http://testserver/500"
359-
assert request_span.get_tag("http.status_code") == "500"
360-
assert request_span.get_tag(ERROR_MSG) == "Server error"
361-
assert request_span.get_tag("error.type") == "builtins.RuntimeError"
362-
assert request_span.get_tag("component") == "fastapi"
363-
assert request_span.get_tag("span.kind") == "server"
364-
assert 'raise RuntimeError("Server error")' in request_span.get_tag("error.stack")
346+
snapshot_client.get("/500")
365347

366348

367349
def test_streaming_response(client, tracer, test_spans):
@@ -647,7 +629,7 @@ def test_background_task(client, tracer, test_spans):
647629
assert request_span.resource == "GET /asynctask"
648630
# typical duration without background task should be in less than 10 ms
649631
# duration with background task will take approximately 1.1s
650-
assert request_span.duration < 1
632+
assert request_span.duration < 1.1
651633

652634

653635
@pytest.mark.parametrize("host", ["hostserver", "hostserver:5454"])

tests/contrib/starlette/test_starlette.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,4 +534,4 @@ def test_background_task(client, tracer, test_spans):
534534
assert request_span.resource == "GET /backgroundtask"
535535
# typical duration without background task should be in less than 10ms
536536
# duration with background task will take approximately 1.1s
537-
assert request_span.duration < 1
537+
assert request_span.duration < 1.1
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[[
2+
{
3+
"name": "fastapi.request",
4+
"service": "fastapi",
5+
"resource": "GET /500",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "web",
10+
"error": 1,
11+
"meta": {
12+
"_dd.p.dm": "-0",
13+
"component": "fastapi",
14+
"error.message": "Server error",
15+
"error.stack": "Traceback (most recent call last):\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/ddtrace/contrib/asgi/middleware.py\", line 243, in __call__\n return await self.app(scope, receive, wrapped_send)\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/starlette/middleware/errors.py\", line 181, in __call__\n raise exc from None\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/starlette/middleware/errors.py\", line 159, in __call__\n await self.app(scope, receive, _send)\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/starlette/exceptions.py\", line 82, in __call__\n raise exc from None\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/starlette/exceptions.py\", line 71, in __call__\n await self.app(scope, receive, sender)\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/starlette/routing.py\", line 566, in __call__\n await route.handle(scope, receive, send)\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/starlette/routing.py\", line 227, in handle\n await self.app(scope, receive, send)\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/starlette/routing.py\", line 41, in app\n response = await func(request)\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/fastapi/routing.py\", line 201, in app\n raw_response = await run_endpoint_function(\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/.riot/venv_py3913_fastapi~0640/lib/python3.9/site-packages/fastapi/routing.py\", line 148, in run_endpoint_function\n return await dependant.call(**values)\n File \"/Users/munirabdinur/go/src/github.com/DataDog/dd-trace-py/tests/contrib/fastapi/app.py\", line 91, in error\n raise RuntimeError(\"Server error\")\nRuntimeError: Server error\n",
16+
"error.type": "builtins.RuntimeError",
17+
"http.method": "GET",
18+
"http.route": "/500",
19+
"http.status_code": "500",
20+
"http.url": "http://testserver/500",
21+
"http.useragent": "testclient",
22+
"http.version": "1.1",
23+
"language": "python",
24+
"runtime-id": "64d5226c7c7c4f79a067f32ae2c114ef",
25+
"span.kind": "server"
26+
},
27+
"metrics": {
28+
"_dd.agent_psr": 1.0,
29+
"_dd.top_level": 1,
30+
"_dd.tracer_kr": 1.0,
31+
"_sampling_priority_v1": 1,
32+
"process_id": 24153
33+
},
34+
"duration": 1430000,
35+
"start": 1683658745856878000
36+
}]]

0 commit comments

Comments
 (0)