From 4484a6e7045904fe804bdce8db3660313c4a5ec4 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Wed, 3 Jun 2026 19:21:05 +0000 Subject: [PATCH 1/2] exclude bg task duration from metrics --- .../instrumentation/asgi/__init__.py | 10 ++- .../tests/test_asgi_middleware.py | 72 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index efcc8ee05f..43d7693c9d 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -795,12 +795,14 @@ async def __call__( span_name, scope, receive ) + response_complete_time: list[float] = [] otel_send = self._get_otel_send( current_span, span_name, scope, send, attributes, + response_complete_time, ) await self.app(scope, otel_receive, otel_send) @@ -816,7 +818,11 @@ async def __call__( query, self._sem_conv_opt_in_mode, ) - duration_s = default_timer() - start + duration_s = ( + response_complete_time[0] - start + if response_complete_time + else default_timer() - start + ) duration_attrs_old = _parse_duration_attrs( attributes, _StabilityMode.DEFAULT ) @@ -974,6 +980,7 @@ def _get_otel_send( scope, send, duration_attrs, + response_complete_time: list[float], ): expecting_trailers = False @@ -1030,6 +1037,7 @@ async def otel_send(message: dict[str, Any]): and message["type"] == "http.response.trailers" and not message.get("more_trailers", False) ): + response_complete_time.append(default_timer()) server_span.end() return otel_send diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index 7295a15c30..6362f30a4e 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -702,6 +702,78 @@ def add_body_and_trailer_span(expected: list): _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 10**9, ) + async def test_background_execution_metrics_duration(self): + """Test that http.server.duration excludes background task time.""" + app = otel_asgi.OpenTelemetryMiddleware(background_execution_asgi) + self.seed_app(app) + await self.send_default_request() + await self.get_all_output() + + metrics = self.get_sorted_metrics(SCOPE) + duration_found = False + for metric in metrics: + if metric.name == "http.server.duration": + data_points = list(metric.data.data_points) + for point in data_points: + if isinstance(point, HistogramDataPoint): + duration_found = True + self.assertLess( + point.sum, + _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 1000, + ) + self.assertTrue( + duration_found, "http.server.duration metric not found" + ) + + async def test_background_execution_metrics_duration_new_semconv(self): + """Test that http.server.request.duration excludes background task time.""" + app = otel_asgi.OpenTelemetryMiddleware(background_execution_asgi) + self.seed_app(app) + await self.send_default_request() + await self.get_all_output() + + metrics = self.get_sorted_metrics(SCOPE) + duration_found = False + for metric in metrics: + if metric.name == "http.server.request.duration": + data_points = list(metric.data.data_points) + for point in data_points: + if isinstance(point, HistogramDataPoint): + duration_found = True + self.assertLess( + point.sum, + _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S, + ) + self.assertTrue( + duration_found, + "http.server.request.duration metric not found", + ) + + async def test_trailers_background_execution_metrics_duration(self): + """Test that http.server.duration excludes background task time for trailer responses.""" + app = otel_asgi.OpenTelemetryMiddleware( + background_execution_trailers_asgi + ) + self.seed_app(app) + await self.send_default_request() + await self.get_all_output() + + metrics = self.get_sorted_metrics(SCOPE) + duration_found = False + for metric in metrics: + if metric.name == "http.server.duration": + data_points = list(metric.data.data_points) + for point in data_points: + if isinstance(point, HistogramDataPoint): + duration_found = True + self.assertLess( + point.sum, + _SIMULATED_BACKGROUND_TASK_EXECUTION_TIME_S * 1000, + ) + self.assertTrue( + duration_found, "http.server.duration metric not found" + ) + async def test_override_span_name(self): """Test that default span_names can be overwritten by our callback function.""" span_name = "Dymaxion" From 81e6495527650675477cd42d4d45fc232643e34c Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Wed, 3 Jun 2026 19:28:27 +0000 Subject: [PATCH 2/2] add changelog --- .changelog/4656.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/4656.fixed diff --git a/.changelog/4656.fixed b/.changelog/4656.fixed new file mode 100644 index 0000000000..bf0d3888af --- /dev/null +++ b/.changelog/4656.fixed @@ -0,0 +1 @@ +`opentelemetry-instrumentation-asgi`: Fix HTTP server duration metrics to exclude background task execution time