Skip to content

Commit

Permalink
Add http.server.response.size metric to ASGI implementation. (#1789)
Browse files Browse the repository at this point in the history
* Add http.server.response.size metric to ASGI implementation.
Add new unit tests.

* Update changelog.

* Fix linting by disabling too-many-nested-blocks

* Put new logic in a new method

* Refactor the placement of new logic.

* Fixed the unit tests in FastAPI and Starlette

* Update changelog.

* FIx lint errors.

* Refactor getting content-length header

* Refactor getting content-length header

---------

Co-authored-by: Shalev Roda <[email protected]>
Co-authored-by: Diego Hurtado <[email protected]>
  • Loading branch information
3 people authored Jun 19, 2023
1 parent 8cc10a0 commit 60753e2
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#1780](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1780))
- Add metric instrumentation for celery
([#1679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1679))
- `opentelemetry-instrumentation-asgi` Add `http.server.response.size` metric
([#1789](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1789))

## Version 1.18.0/0.39b0 (2023-05-10)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,11 @@ def __init__(
unit="ms",
description="measures the duration of the inbound HTTP request",
)
self.server_response_size_histogram = self.meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE,
unit="By",
description="measures the size of HTTP response messages (compressed).",
)
self.active_requests_counter = self.meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
Expand All @@ -518,6 +523,7 @@ def __init__(
self.server_request_hook = server_request_hook
self.client_request_hook = client_request_hook
self.client_response_hook = client_response_hook
self.content_length_header = None

async def __call__(self, scope, receive, send):
"""The ASGI application
Expand Down Expand Up @@ -593,6 +599,10 @@ async def __call__(self, scope, receive, send):
self.active_requests_counter.add(
-1, active_requests_count_attrs
)
if self.content_length_header:
self.server_response_size_histogram.record(
self.content_length_header, duration_attrs
)
if token:
context.detach(token)

Expand Down Expand Up @@ -660,6 +670,13 @@ async def otel_send(message):
setter=asgi_setter,
)

content_length = asgi_getter.get(message, "content-length")
if content_length:
try:
self.content_length_header = int(content_length[0])
except ValueError:
pass

await send(message)

return otel_send
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@
_expected_metric_names = [
"http.server.active_requests",
"http.server.duration",
"http.server.response.size",
]
_recommended_attrs = {
"http.server.active_requests": _active_requests_count_attrs,
"http.server.duration": _duration_attrs,
"http.server.response.size": _duration_attrs,
}


Expand All @@ -61,7 +63,10 @@ async def http_app(scope, receive, send):
{
"type": "http.response.start",
"status": 200,
"headers": [[b"Content-Type", b"text/plain"]],
"headers": [
[b"Content-Type", b"text/plain"],
[b"content-length", b"1024"],
],
}
)
await send({"type": "http.response.body", "body": b"*"})
Expand Down Expand Up @@ -103,7 +108,10 @@ async def error_asgi(scope, receive, send):
{
"type": "http.response.start",
"status": 200,
"headers": [[b"Content-Type", b"text/plain"]],
"headers": [
[b"Content-Type", b"text/plain"],
[b"content-length", b"1024"],
],
}
)
await send({"type": "http.response.body", "body": b"*"})
Expand All @@ -126,7 +134,8 @@ def validate_outputs(self, outputs, error=None, modifiers=None):
# Check http response start
self.assertEqual(response_start["status"], 200)
self.assertEqual(
response_start["headers"], [[b"Content-Type", b"text/plain"]]
response_start["headers"],
[[b"Content-Type", b"text/plain"], [b"content-length", b"1024"]],
)

exc_info = self.scope.get("hack_exc_info")
Expand Down Expand Up @@ -352,6 +361,7 @@ def test_traceresponse_header(self):
response_start["headers"],
[
[b"Content-Type", b"text/plain"],
[b"content-length", b"1024"],
[b"traceresponse", f"{traceresponse}".encode()],
[b"access-control-expose-headers", b"traceresponse"],
],
Expand Down Expand Up @@ -565,6 +575,7 @@ def test_basic_metric_success(self):
"http.flavor": "1.0",
}
metrics_list = self.memory_metrics_reader.get_metrics_data()
# pylint: disable=too-many-nested-blocks
for resource_metric in metrics_list.resource_metrics:
for scope_metrics in resource_metric.scope_metrics:
for metric in scope_metrics.metrics:
Expand All @@ -575,9 +586,12 @@ def test_basic_metric_success(self):
dict(point.attributes),
)
self.assertEqual(point.count, 1)
self.assertAlmostEqual(
duration, point.sum, delta=5
)
if metric.name == "http.server.duration":
self.assertAlmostEqual(
duration, point.sum, delta=5
)
elif metric.name == "http.server.response.size":
self.assertEqual(1024, point.sum)
elif isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
Expand All @@ -602,13 +616,12 @@ async def target_asgi(scope, receive, send):
app = otel_asgi.OpenTelemetryMiddleware(target_asgi)
self.seed_app(app)
self.send_default_request()

metrics_list = self.memory_metrics_reader.get_metrics_data()
assertions = 0
for resource_metric in metrics_list.resource_metrics:
for scope_metrics in resource_metric.scope_metrics:
for metric in scope_metrics.metrics:
if metric.name != "http.server.duration":
if metric.name == "http.server.active_requests":
continue
for point in metric.data.data_points:
if isinstance(point, HistogramDataPoint):
Expand All @@ -617,7 +630,7 @@ async def target_asgi(scope, receive, send):
expected_target,
)
assertions += 1
self.assertEqual(assertions, 1)
self.assertEqual(assertions, 2)

def test_no_metric_for_websockets(self):
self.scope = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@
_expected_metric_names = [
"http.server.active_requests",
"http.server.duration",
"http.server.response.size",
]
_recommended_attrs = {
"http.server.active_requests": _active_requests_count_attrs,
"http.server.duration": {*_duration_attrs, SpanAttributes.HTTP_TARGET},
"http.server.response.size": {
*_duration_attrs,
SpanAttributes.HTTP_TARGET,
},
}


Expand Down Expand Up @@ -187,7 +192,7 @@ def test_fastapi_metrics(self):
for resource_metric in metrics_list.resource_metrics:
self.assertTrue(len(resource_metric.scope_metrics) == 1)
for scope_metric in resource_metric.scope_metrics:
self.assertTrue(len(scope_metric.metrics) == 2)
self.assertTrue(len(scope_metric.metrics) == 3)
for metric in scope_metric.metrics:
self.assertIn(metric.name, _expected_metric_names)
data_points = list(metric.data.data_points)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@
_expected_metric_names = [
"http.server.active_requests",
"http.server.duration",
"http.server.response.size",
]
_recommended_attrs = {
"http.server.active_requests": _active_requests_count_attrs,
"http.server.duration": _duration_attrs,
"http.server.response.size": _duration_attrs,
}


Expand Down Expand Up @@ -128,7 +130,7 @@ def test_starlette_metrics(self):
for resource_metric in metrics_list.resource_metrics:
self.assertTrue(len(resource_metric.scope_metrics) == 1)
for scope_metric in resource_metric.scope_metrics:
self.assertTrue(len(scope_metric.metrics) == 2)
self.assertTrue(len(scope_metric.metrics) == 3)
for metric in scope_metric.metrics:
self.assertIn(metric.name, _expected_metric_names)
data_points = list(metric.data.data_points)
Expand Down

0 comments on commit 60753e2

Please sign in to comment.