Skip to content

Commit 9b71acf

Browse files
fix(asgi): fix websocket spans duration and close span [backport 3.12] (#14426)
Backport 358bfd5 from #14388 to 3.12. - Fix the duration of websocket.receive spans to close when the next websocket.receive span is started; previously the `cleanup_previous_receive` was in a handler method, which led to some delay - Fix the duration of the `fastapi.request` handshake span to close when the connection is upgraded, not after the connection is closed - Minor fix where websocket.close parent should be the request span if `DD_TRACE_WEBSOCKET_MESSAGES_SEPARATE_TRACES` is disabled Trace (when `DD_TRACE_WEBSOCKET_MESSAGES_SEPARATE_TRACES` is disabled): <img width="1062" height="487" alt="Screenshot 2025-08-21 at 1 00 39 PM" src="https://github.com/user-attachments/assets/42199884-a712-45fd-a888-d564b21cadf2" /> ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) Co-authored-by: Quinna Halim <[email protected]>
1 parent 5d557e8 commit 9b71acf

7 files changed

+512
-459
lines changed

ddtrace/contrib/internal/asgi/middleware.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -359,12 +359,14 @@ async def wrapped_receive():
359359
360360
Note:
361361
- Spans are linked to the handshake span
362-
- Receive spans remain open until the next receive operation or connection close
362+
- Receive spans are finished exactly when the next receive operation starts
363363
"""
364364

365365
if not self.integration_config.trace_asgi_websocket_messages:
366366
return await receive()
367367

368+
current_receive_span = scope.get("datadog", {}).get("current_receive_span")
369+
368370
with core.context_with_data(
369371
"asgi.websocket.receive_message",
370372
tracer=self.tracer,
@@ -377,6 +379,10 @@ async def wrapped_receive():
377379
call_trace=False,
378380
activate=True,
379381
) as ctx:
382+
if current_receive_span:
383+
current_receive_span.finish()
384+
scope["datadog"].pop("current_receive_span", None)
385+
380386
recv_span = ctx.span
381387
try:
382388
message = await receive()
@@ -421,6 +427,16 @@ async def wrapped_send(message: Mapping[str, Any]):
421427
"""
422428
try:
423429
if (
430+
scope["type"] == "websocket"
431+
and self.integration_config.trace_asgi_websocket_messages
432+
and message.get("type") == "websocket.accept"
433+
):
434+
# Close handshake span once connection is upgraded
435+
if span and span.error == 0:
436+
span.finish()
437+
return await send(message)
438+
439+
elif (
424440
scope["type"] == "websocket"
425441
and self.integration_config.trace_asgi_websocket_messages
426442
and message.get("type") == "websocket.send"
@@ -549,7 +565,7 @@ def _handle_websocket_close_message(self, scope: Mapping[str, Any], message: Map
549565
current_receive_span = scope.get("datadog", {}).get("current_receive_span")
550566

551567
# Use receive span as parent if it exists, otherwise use main span
552-
if current_receive_span:
568+
if self.integration_config.websocket_messages_separate_traces and current_receive_span:
553569
parent_span = current_receive_span
554570
else:
555571
parent_span = request_span
@@ -607,8 +623,6 @@ def _handle_http_response(
607623
def _handle_websocket_receive_message(
608624
self, scope: Mapping[str, Any], message: Mapping[str, Any], recv_span: Span, request_span: Span
609625
):
610-
_cleanup_previous_receive(scope)
611-
612626
recv_span.set_tag_str(COMPONENT, self.integration_config.integration_name)
613627
recv_span.set_tag_str(SPAN_KIND, SpanKind.CONSUMER)
614628
recv_span.set_tag_str(websocket.RECEIVE_DURATION_TYPE, "blocking")
@@ -660,8 +674,3 @@ def _handle_websocket_disconnect_message(
660674

661675
_copy_trace_level_tags(disconnect_span, request_span)
662676
_set_websocket_close_tags(disconnect_span, message)
663-
664-
# The context system automatically finishes the disconnect_span
665-
# Only finish the main span if needed
666-
if request_span and request_span.error == 0:
667-
request_span.finish()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
tracing: Fixes issue with ``websocket.receive`` span not closing exactly when another ``websocket.receive`` span was opened.
5+
- |
6+
tracing: Fixes duration of websocket handshake span such that the handshake span closes when the connection is upgraded.
7+
- |
8+
tracing: Fix where ``websocket.close`` parent should be the handshake span when configuration is disabled.

0 commit comments

Comments
 (0)