@@ -159,6 +159,51 @@ async def background_execution_asgi(scope, receive, send):
159
159
time .sleep (_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S )
160
160
161
161
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
+
162
207
async def error_asgi (scope , receive , send ):
163
208
assert isinstance (scope , dict )
164
209
assert scope ["type" ] == "http"
@@ -188,7 +233,12 @@ def validate_outputs(self, outputs, error=None, modifiers=None):
188
233
modifiers = modifiers or []
189
234
# Check for expected outputs
190
235
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
+
192
242
self .assertEqual (response_start ["type" ], "http.response.start" )
193
243
self .assertEqual (response_final_body ["type" ], "http.response.body" )
194
244
self .assertEqual (response_final_body .get ("more_body" , False ), False )
@@ -331,6 +381,41 @@ def test_background_execution(self):
331
381
_SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 10 ** 9 ,
332
382
)
333
383
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
+
334
419
def test_override_span_name (self ):
335
420
"""Test that default span_names can be overwritten by our callback function."""
336
421
span_name = "Dymaxion"
0 commit comments