diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6f969aa1..9aeb2e2cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- `opentelemetry-instrumentation-asgi` Add `http.server.request.size` metric + ([#1867](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1867)) ### Fixed 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 1ee25ae7d9..a1fa1f8e31 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -511,6 +511,11 @@ def __init__( unit="By", description="measures the size of HTTP response messages (compressed).", ) + self.server_request_size_histogram = self.meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, + unit="By", + description="Measures the size of HTTP request messages (compressed).", + ) self.active_requests_counter = self.meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, unit="requests", @@ -603,6 +608,16 @@ async def __call__(self, scope, receive, send): self.server_response_size_histogram.record( self.content_length_header, duration_attrs ) + request_size = asgi_getter.get(scope, "content-length") + if request_size: + try: + request_size_amount = int(request_size[0]) + except ValueError: + pass + else: + self.server_request_size_histogram.record( + request_size_amount, duration_attrs + ) if token: context.detach(token) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index 0a0c2c301f..8ca82d0226 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -47,16 +47,19 @@ "http.server.active_requests", "http.server.duration", "http.server.response.size", + "http.server.request.size", ] _recommended_attrs = { "http.server.active_requests": _active_requests_count_attrs, "http.server.duration": _duration_attrs, "http.server.response.size": _duration_attrs, + "http.server.request.size": _duration_attrs, } async def http_app(scope, receive, send): message = await receive() + scope["headers"] = [(b"content-length", b"128")] assert scope["type"] == "http" if message.get("type") == "http.request": await send( @@ -99,6 +102,7 @@ async def error_asgi(scope, receive, send): assert isinstance(scope, dict) assert scope["type"] == "http" message = await receive() + scope["headers"] = [(b"content-length", b"128")] if message.get("type") == "http.request": try: raise ValueError @@ -592,6 +596,8 @@ def test_basic_metric_success(self): ) elif metric.name == "http.server.response.size": self.assertEqual(1024, point.sum) + elif metric.name == "http.server.request.size": + self.assertEqual(128, point.sum) elif isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes, @@ -630,7 +636,7 @@ async def target_asgi(scope, receive, send): expected_target, ) assertions += 1 - self.assertEqual(assertions, 2) + self.assertEqual(assertions, 3) def test_no_metric_for_websockets(self): self.scope = { diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 7f12d6e3f3..4269dfa2e4 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -45,6 +45,7 @@ "http.server.active_requests", "http.server.duration", "http.server.response.size", + "http.server.request.size", ] _recommended_attrs = { "http.server.active_requests": _active_requests_count_attrs, @@ -53,6 +54,10 @@ *_duration_attrs, SpanAttributes.HTTP_TARGET, }, + "http.server.request.size": { + *_duration_attrs, + SpanAttributes.HTTP_TARGET, + }, } @@ -251,8 +256,13 @@ def test_basic_metric_success(self): def test_basic_post_request_metric_success(self): start = default_timer() - self._client.post("/foobar") + response = self._client.post( + "/foobar", + json={"foo": "bar"}, + ) duration = max(round((default_timer() - start) * 1000), 0) + response_size = int(response.headers.get("content-length")) + request_size = int(response.request.headers.get("content-length")) metrics_list = self.memory_metrics_reader.get_metrics_data() for metric in ( metrics_list.resource_metrics[0].scope_metrics[0].metrics @@ -260,7 +270,12 @@ def test_basic_post_request_metric_success(self): for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) - self.assertAlmostEqual(duration, point.sum, delta=30) + if metric.name == "http.server.duration": + self.assertAlmostEqual(duration, point.sum, delta=30) + elif metric.name == "http.server.response.size": + self.assertEqual(response_size, point.sum) + elif metric.name == "http.server.request.size": + self.assertEqual(request_size, point.sum) if isinstance(point, NumberDataPoint): self.assertEqual(point.value, 0) diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py index 2a53bdffb7..e1c77312a4 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -50,11 +50,13 @@ "http.server.active_requests", "http.server.duration", "http.server.response.size", + "http.server.request.size", ] _recommended_attrs = { "http.server.active_requests": _active_requests_count_attrs, "http.server.duration": _duration_attrs, "http.server.response.size": _duration_attrs, + "http.server.request.size": _duration_attrs, } @@ -165,8 +167,13 @@ def test_basic_post_request_metric_success(self): "http.scheme": "http", "http.server_name": "testserver", } - self._client.post("/foobar") + response = self._client.post( + "/foobar", + json={"foo": "bar"}, + ) duration = max(round((default_timer() - start) * 1000), 0) + response_size = int(response.headers.get("content-length")) + request_size = int(response.request.headers.get("content-length")) metrics_list = self.memory_metrics_reader.get_metrics_data() for metric in ( metrics_list.resource_metrics[0].scope_metrics[0].metrics @@ -174,10 +181,15 @@ def test_basic_post_request_metric_success(self): for point in list(metric.data.data_points): if isinstance(point, HistogramDataPoint): self.assertEqual(point.count, 1) - self.assertAlmostEqual(duration, point.sum, delta=30) self.assertDictEqual( dict(point.attributes), expected_duration_attributes ) + if metric.name == "http.server.duration": + self.assertAlmostEqual(duration, point.sum, delta=30) + elif metric.name == "http.server.response.size": + self.assertEqual(response_size, point.sum) + elif metric.name == "http.server.request.size": + self.assertEqual(request_size, point.sum) if isinstance(point, NumberDataPoint): self.assertDictEqual( expected_requests_count_attributes,