@@ -483,7 +483,7 @@ def get_default_span_details(scope: dict) -> Tuple[str, dict]:
483483
484484
485485def _collect_target_attribute (
486- scope : typing .Dict [str , typing .Any ]
486+ scope : typing .Dict [str , typing .Any ],
487487) -> typing .Optional [str ]:
488488 """
489489 Returns the target path as defined by the Semantic Conventions.
@@ -529,6 +529,7 @@ class OpenTelemetryMiddleware:
529529 the current globally configured one is used.
530530 meter_provider: The optional meter provider to use. If omitted
531531 the current globally configured one is used.
532+ exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace.
532533 """
533534
534535 # pylint: disable=too-many-branches
@@ -547,6 +548,7 @@ def __init__(
547548 http_capture_headers_server_request : list [str ] | None = None ,
548549 http_capture_headers_server_response : list [str ] | None = None ,
549550 http_capture_headers_sanitize_fields : list [str ] | None = None ,
551+ exclude_spans : list [typing .Literal ["receive" , "send" ]] | None = None ,
550552 ):
551553 # initialize semantic conventions opt-in if needed
552554 _OpenTelemetrySemanticConventionStability ._initialize ()
@@ -653,6 +655,12 @@ def __init__(
653655 )
654656 or []
655657 )
658+ self .exclude_receive_span = (
659+ "receive" in exclude_spans if exclude_spans else False
660+ )
661+ self .exclude_send_span = (
662+ "send" in exclude_spans if exclude_spans else False
663+ )
656664
657665 # pylint: disable=too-many-statements
658666 async def __call__ (
@@ -796,8 +804,10 @@ async def __call__(
796804 span .end ()
797805
798806 # pylint: enable=too-many-branches
799-
800807 def _get_otel_receive (self , server_span_name , scope , receive ):
808+ if self .exclude_receive_span :
809+ return receive
810+
801811 @wraps (receive )
802812 async def otel_receive ():
803813 with self .tracer .start_as_current_span (
@@ -821,6 +831,66 @@ async def otel_receive():
821831
822832 return otel_receive
823833
834+ def _set_send_span (
835+ self ,
836+ server_span_name ,
837+ scope ,
838+ send ,
839+ message ,
840+ status_code ,
841+ expecting_trailers ,
842+ ):
843+ """Set send span attributes and status code."""
844+ with self .tracer .start_as_current_span (
845+ " " .join ((server_span_name , scope ["type" ], "send" ))
846+ ) as send_span :
847+ if callable (self .client_response_hook ):
848+ self .client_response_hook (send_span , scope , message )
849+
850+ if send_span .is_recording ():
851+ if message ["type" ] == "http.response.start" :
852+ expecting_trailers = message .get ("trailers" , False )
853+ send_span .set_attribute ("asgi.event.type" , message ["type" ])
854+
855+ if status_code :
856+ set_status_code (
857+ send_span ,
858+ status_code ,
859+ None ,
860+ self ._sem_conv_opt_in_mode ,
861+ )
862+ return expecting_trailers
863+
864+ def _set_server_span (
865+ self , server_span , message , status_code , duration_attrs
866+ ):
867+ """Set server span attributes and status code."""
868+ if (
869+ server_span .is_recording ()
870+ and server_span .kind == trace .SpanKind .SERVER
871+ and "headers" in message
872+ ):
873+ custom_response_attributes = (
874+ collect_custom_headers_attributes (
875+ message ,
876+ self .http_capture_headers_sanitize_fields ,
877+ self .http_capture_headers_server_response ,
878+ normalise_response_header_name ,
879+ )
880+ if self .http_capture_headers_server_response
881+ else {}
882+ )
883+ if len (custom_response_attributes ) > 0 :
884+ server_span .set_attributes (custom_response_attributes )
885+
886+ if status_code :
887+ set_status_code (
888+ server_span ,
889+ status_code ,
890+ duration_attrs ,
891+ self ._sem_conv_opt_in_mode ,
892+ )
893+
824894 def _get_otel_send (
825895 self ,
826896 server_span ,
@@ -834,74 +904,46 @@ def _get_otel_send(
834904 @wraps (send )
835905 async def otel_send (message : dict [str , Any ]):
836906 nonlocal expecting_trailers
837- with self .tracer .start_as_current_span (
838- " " .join ((server_span_name , scope ["type" ], "send" ))
839- ) as send_span :
840- if callable (self .client_response_hook ):
841- self .client_response_hook (send_span , scope , message )
842907
843- status_code = None
844- if message ["type" ] == "http.response.start" :
845- status_code = message ["status" ]
846- elif message ["type" ] == "websocket.send" :
847- status_code = 200
848-
849- if send_span .is_recording ():
850- if message ["type" ] == "http.response.start" :
851- expecting_trailers = message .get ("trailers" , False )
852- send_span .set_attribute ("asgi.event.type" , message ["type" ])
853- if (
854- server_span .is_recording ()
855- and server_span .kind == trace .SpanKind .SERVER
856- and "headers" in message
857- ):
858- custom_response_attributes = (
859- collect_custom_headers_attributes (
860- message ,
861- self .http_capture_headers_sanitize_fields ,
862- self .http_capture_headers_server_response ,
863- normalise_response_header_name ,
864- )
865- if self .http_capture_headers_server_response
866- else {}
867- )
868- if len (custom_response_attributes ) > 0 :
869- server_span .set_attributes (
870- custom_response_attributes
871- )
872- if status_code :
873- # We record metrics only once
874- set_status_code (
875- server_span ,
876- status_code ,
877- duration_attrs ,
878- self ._sem_conv_opt_in_mode ,
879- )
880- set_status_code (
881- send_span ,
882- status_code ,
883- None ,
884- self ._sem_conv_opt_in_mode ,
885- )
908+ status_code = None
909+ if message ["type" ] == "http.response.start" :
910+ status_code = message ["status" ]
911+ elif message ["type" ] == "websocket.send" :
912+ status_code = 200
886913
887- propagator = get_global_response_propagator ()
888- if propagator :
889- propagator . inject (
890- message ,
891- context = set_span_in_context (
892- server_span , trace . context_api . Context ()
893- ) ,
894- setter = asgi_setter ,
895- )
914+ if not self . exclude_send_span :
915+ expecting_trailers = self . _set_send_span (
916+ server_span_name ,
917+ scope ,
918+ send ,
919+ message ,
920+ status_code ,
921+ expecting_trailers ,
922+ )
896923
897- content_length = asgi_getter .get (message , "content-length" )
898- if content_length :
899- try :
900- self .content_length_header = int (content_length [0 ])
901- except ValueError :
902- pass
924+ self ._set_server_span (
925+ server_span , message , status_code , duration_attrs
926+ )
927+
928+ propagator = get_global_response_propagator ()
929+ if propagator :
930+ propagator .inject (
931+ message ,
932+ context = set_span_in_context (
933+ server_span , trace .context_api .Context ()
934+ ),
935+ setter = asgi_setter ,
936+ )
937+
938+ content_length = asgi_getter .get (message , "content-length" )
939+ if content_length :
940+ try :
941+ self .content_length_header = int (content_length [0 ])
942+ except ValueError :
943+ pass
944+
945+ await send (message )
903946
904- await send (message )
905947 # pylint: disable=too-many-boolean-expressions
906948 if (
907949 not expecting_trailers
0 commit comments