@@ -159,6 +159,51 @@ async def background_execution_asgi(scope, receive, send):
159159 time .sleep (_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S )
160160
161161
162+ async def background_execution_trailers_asgi (scope , receive , send ):
163+ assert isinstance (scope , dict )
164+ assert scope ["type" ] == "http"
165+ message = await receive ()
166+ scope ["headers" ] = [(b"content-length" , b"128" )]
167+ assert scope ["type" ] == "http"
168+ if message .get ("type" ) == "http.request" :
169+ await send (
170+ {
171+ "type" : "http.response.start" ,
172+ "status" : 200 ,
173+ "headers" : [
174+ [b"Content-Type" , b"text/plain" ],
175+ [b"content-length" , b"1024" ],
176+ ],
177+ "trailers" : True ,
178+ }
179+ )
180+ await send (
181+ {"type" : "http.response.body" , "body" : b"*" , "more_body" : True }
182+ )
183+ await send (
184+ {"type" : "http.response.body" , "body" : b"*" , "more_body" : False }
185+ )
186+ await send (
187+ {
188+ "type" : "http.response.trailers" ,
189+ "headers" : [
190+ [b"trailer" , b"test-trailer" ],
191+ ],
192+ "more_trailers" : True ,
193+ }
194+ )
195+ await send (
196+ {
197+ "type" : "http.response.trailers" ,
198+ "headers" : [
199+ [b"trailer" , b"second-test-trailer" ],
200+ ],
201+ "more_trailers" : False ,
202+ }
203+ )
204+ time .sleep (_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S )
205+
206+
162207async def error_asgi (scope , receive , send ):
163208 assert isinstance (scope , dict )
164209 assert scope ["type" ] == "http"
@@ -188,7 +233,12 @@ def validate_outputs(self, outputs, error=None, modifiers=None):
188233 modifiers = modifiers or []
189234 # Check for expected outputs
190235 response_start = outputs [0 ]
191- response_final_body = outputs [- 1 ]
236+ response_final_body = [
237+ output
238+ for output in outputs
239+ if output ["type" ] == "http.response.body"
240+ ][- 1 ]
241+
192242 self .assertEqual (response_start ["type" ], "http.response.start" )
193243 self .assertEqual (response_final_body ["type" ], "http.response.body" )
194244 self .assertEqual (response_final_body .get ("more_body" , False ), False )
@@ -331,6 +381,41 @@ def test_background_execution(self):
331381 _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 10 ** 9 ,
332382 )
333383
384+ def test_trailers (self ):
385+ """Test that trailers are emitted as expected and that the server span is ended
386+ BEFORE the background task is finished."""
387+ app = otel_asgi .OpenTelemetryMiddleware (
388+ background_execution_trailers_asgi
389+ )
390+ self .seed_app (app )
391+ self .send_default_request ()
392+ outputs = self .get_all_output ()
393+
394+ def add_body_and_trailer_span (expected : list ):
395+ body_span = {
396+ "name" : "GET / http send" ,
397+ "kind" : trace_api .SpanKind .INTERNAL ,
398+ "attributes" : {"type" : "http.response.body" },
399+ }
400+ trailer_span = {
401+ "name" : "GET / http send" ,
402+ "kind" : trace_api .SpanKind .INTERNAL ,
403+ "attributes" : {"type" : "http.response.trailers" },
404+ }
405+ expected [2 :2 ] = [body_span ]
406+ expected [4 :4 ] = [trailer_span ] * 2
407+ return expected
408+
409+ self .validate_outputs (outputs , modifiers = [add_body_and_trailer_span ])
410+ span_list = self .memory_exporter .get_finished_spans ()
411+ server_span = span_list [- 1 ]
412+ assert server_span .kind == SpanKind .SERVER
413+ span_duration_nanos = server_span .end_time - server_span .start_time
414+ self .assertLessEqual (
415+ span_duration_nanos ,
416+ _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 10 ** 9 ,
417+ )
418+
334419 def test_override_span_name (self ):
335420 """Test that default span_names can be overwritten by our callback function."""
336421 span_name = "Dymaxion"
0 commit comments