@@ -191,9 +191,11 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
191
191
192
192
from __future__ import annotations
193
193
194
+ import os
194
195
import typing
195
196
import urllib
196
197
from collections import defaultdict
198
+ from distutils .util import strtobool
197
199
from functools import wraps
198
200
from timeit import default_timer
199
201
from typing import Any , Awaitable , Callable , DefaultDict , Tuple
@@ -226,6 +228,10 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
226
228
_set_http_user_agent ,
227
229
_set_status ,
228
230
)
231
+ from opentelemetry .instrumentation .asgi .environment_variables import (
232
+ OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN ,
233
+ OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN ,
234
+ )
229
235
from opentelemetry .instrumentation .asgi .types import (
230
236
ClientRequestHook ,
231
237
ClientResponseHook ,
@@ -527,6 +533,8 @@ class OpenTelemetryMiddleware:
527
533
the current globally configured one is used.
528
534
meter_provider: The optional meter provider to use. If omitted
529
535
the current globally configured one is used.
536
+ exclude_receive_span: Optional flag to exclude the http receive span from the trace.
537
+ exclude_send_span: Optional flag to exclude the http send span from the trace.
530
538
"""
531
539
532
540
# pylint: disable=too-many-branches
@@ -545,6 +553,8 @@ def __init__(
545
553
http_capture_headers_server_request : list [str ] | None = None ,
546
554
http_capture_headers_server_response : list [str ] | None = None ,
547
555
http_capture_headers_sanitize_fields : list [str ] | None = None ,
556
+ exclude_receive_span : bool = False ,
557
+ exclude_send_span : bool = False ,
548
558
):
549
559
# initialize semantic conventions opt-in if needed
550
560
_OpenTelemetrySemanticConventionStability ._initialize ()
@@ -651,6 +661,12 @@ def __init__(
651
661
)
652
662
or []
653
663
)
664
+ self .exclude_receive_span = exclude_receive_span or strtobool (
665
+ os .getenv (OTEL_PYTHON_ASGI_EXCLUDE_RECEIVE_SPAN , "false" )
666
+ )
667
+ self .exclude_send_span = exclude_send_span or strtobool (
668
+ os .getenv (OTEL_PYTHON_ASGI_EXCLUDE_SEND_SPAN , "false" )
669
+ )
654
670
655
671
# pylint: disable=too-many-statements
656
672
async def __call__ (
@@ -796,6 +812,8 @@ async def __call__(
796
812
# pylint: enable=too-many-branches
797
813
798
814
def _get_otel_receive (self , server_span_name , scope , receive ):
815
+ if self .exclude_receive_span :
816
+ return receive
799
817
@wraps (receive )
800
818
async def otel_receive ():
801
819
with self .tracer .start_as_current_span (
@@ -832,41 +850,48 @@ def _get_otel_send(
832
850
@wraps (send )
833
851
async def otel_send (message : dict [str , Any ]):
834
852
nonlocal expecting_trailers
835
- with self .tracer .start_as_current_span (
836
- " " .join ((server_span_name , scope ["type" ], "send" ))
837
- ) as send_span :
838
- if callable (self .client_response_hook ):
839
- self .client_response_hook (send_span , scope , message )
840
-
841
- status_code = None
842
- if message ["type" ] == "http.response.start" :
843
- status_code = message ["status" ]
844
- elif message ["type" ] == "websocket.send" :
845
- status_code = 200
846
-
847
- if send_span .is_recording ():
848
- if message ["type" ] == "http.response.start" :
849
- expecting_trailers = message .get ("trailers" , False )
850
- send_span .set_attribute ("asgi.event.type" , message ["type" ])
851
- if (
852
- server_span .is_recording ()
853
- and server_span .kind == trace .SpanKind .SERVER
854
- and "headers" in message
855
- ):
856
- custom_response_attributes = (
857
- collect_custom_headers_attributes (
858
- message ,
859
- self .http_capture_headers_sanitize_fields ,
860
- self .http_capture_headers_server_response ,
861
- normalise_response_header_name ,
853
+
854
+ status_code = None
855
+ if message ["type" ] == "http.response.start" :
856
+ status_code = message ["status" ]
857
+ expecting_trailers = message .get ("trailers" , False )
858
+ elif message ["type" ] == "websocket.send" :
859
+ status_code = 200
860
+
861
+ # Conditional send_span creation
862
+ if not self .exclude_send_span :
863
+ with self .tracer .start_as_current_span (
864
+ " " .join ((server_span_name , scope ["type" ], "send" ))
865
+ ) as send_span :
866
+ if callable (self .client_response_hook ):
867
+ self .client_response_hook (send_span , scope , message )
868
+
869
+ if send_span .is_recording ():
870
+ send_span .set_attribute ("asgi.event.type" , message ["type" ])
871
+ if status_code :
872
+ set_status_code (
873
+ send_span ,
874
+ status_code ,
875
+ None ,
876
+ self ._sem_conv_opt_in_mode ,
862
877
)
863
- if self .http_capture_headers_server_response
864
- else {}
878
+
879
+ # Server span logic always applied
880
+ if server_span .is_recording () and "headers" in message :
881
+ if server_span .kind == trace .SpanKind .SERVER :
882
+ custom_response_attributes = (
883
+ collect_custom_headers_attributes (
884
+ message ,
885
+ self .http_capture_headers_sanitize_fields ,
886
+ self .http_capture_headers_server_response ,
887
+ normalise_response_header_name ,
865
888
)
866
- if len (custom_response_attributes ) > 0 :
867
- server_span .set_attributes (
868
- custom_response_attributes
869
- )
889
+ if self .http_capture_headers_server_response
890
+ else {}
891
+ )
892
+ if len (custom_response_attributes ) > 0 :
893
+ server_span .set_attributes (custom_response_attributes )
894
+
870
895
if status_code :
871
896
# We record metrics only once
872
897
set_status_code (
@@ -875,12 +900,6 @@ async def otel_send(message: dict[str, Any]):
875
900
duration_attrs ,
876
901
self ._sem_conv_opt_in_mode ,
877
902
)
878
- set_status_code (
879
- send_span ,
880
- status_code ,
881
- None ,
882
- self ._sem_conv_opt_in_mode ,
883
- )
884
903
885
904
propagator = get_global_response_propagator ()
886
905
if propagator :
@@ -892,14 +911,15 @@ async def otel_send(message: dict[str, Any]):
892
911
setter = asgi_setter ,
893
912
)
894
913
895
- content_length = asgi_getter .get (message , "content-length" )
896
- if content_length :
897
- try :
898
- self .content_length_header = int (content_length [0 ])
899
- except ValueError :
900
- pass
914
+ content_length = asgi_getter .get (message , "content-length" )
915
+ if content_length :
916
+ try :
917
+ self .content_length_header = int (content_length [0 ])
918
+ except ValueError :
919
+ pass
920
+
921
+ await send (message )
901
922
902
- await send (message )
903
923
# pylint: disable=too-many-boolean-expressions
904
924
if (
905
925
not expecting_trailers
0 commit comments