opentelemetry-instrumentation-asgi: always set status code on duration attributes (#2627)

This commit is contained in:
Daniel Hochman
2024-07-12 11:38:40 -05:00
committed by GitHub
parent 43dfc73c4c
commit b697f4ab9a
4 changed files with 104 additions and 43 deletions

View File

@ -61,6 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2153](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2153)) ([#2153](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2153))
- `opentelemetry-instrumentation-asgi` Removed `NET_HOST_NAME` AND `NET_HOST_PORT` from active requests count attribute - `opentelemetry-instrumentation-asgi` Removed `NET_HOST_NAME` AND `NET_HOST_PORT` from active requests count attribute
([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610))
- `opentelemetry-instrumentation-asgi` Bugfix: Middleware did not set status code attribute on duration metrics for non-recording spans.
([#2627](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2627))
## Version 1.25.0/0.46b0 (2024-05-31) ## Version 1.25.0/0.46b0 (2024-05-31)

View File

@ -438,8 +438,6 @@ def set_status_code(
sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT,
): ):
"""Adds HTTP response attributes to span using the status_code argument.""" """Adds HTTP response attributes to span using the status_code argument."""
if not span.is_recording():
return
status_code_str = str(status_code) status_code_str = str(status_code)
try: try:
@ -836,36 +834,16 @@ class OpenTelemetryMiddleware:
) as send_span: ) as send_span:
if callable(self.client_response_hook): if callable(self.client_response_hook):
self.client_response_hook(send_span, scope, message) self.client_response_hook(send_span, scope, message)
status_code = None
if message["type"] == "http.response.start":
status_code = message["status"]
elif message["type"] == "websocket.send":
status_code = 200
if send_span.is_recording(): if send_span.is_recording():
if message["type"] == "http.response.start": if message["type"] == "http.response.start":
status_code = message["status"]
# We record metrics only once
set_status_code(
server_span,
status_code,
duration_attrs,
self._sem_conv_opt_in_mode,
)
set_status_code(
send_span,
status_code,
None,
self._sem_conv_opt_in_mode,
)
expecting_trailers = message.get("trailers", False) expecting_trailers = message.get("trailers", False)
elif message["type"] == "websocket.send":
set_status_code(
server_span,
200,
duration_attrs,
self._sem_conv_opt_in_mode,
)
set_status_code(
send_span,
200,
None,
self._sem_conv_opt_in_mode,
)
send_span.set_attribute("asgi.event.type", message["type"]) send_span.set_attribute("asgi.event.type", message["type"])
if ( if (
server_span.is_recording() server_span.is_recording()
@ -886,6 +864,20 @@ class OpenTelemetryMiddleware:
server_span.set_attributes( server_span.set_attributes(
custom_response_attributes custom_response_attributes
) )
if status_code:
# We record metrics only once
set_status_code(
server_span,
status_code,
duration_attrs,
self._sem_conv_opt_in_mode,
)
set_status_code(
send_span,
status_code,
None,
self._sem_conv_opt_in_mode,
)
propagator = get_global_response_propagator() propagator = get_global_response_propagator()
if propagator: if propagator:

View File

@ -514,7 +514,9 @@ class TestAsgiApplication(AsgiTestBase):
mock_span = mock.Mock() mock_span = mock.Mock()
mock_span.is_recording.return_value = False mock_span.is_recording.return_value = False
mock_tracer.start_as_current_span.return_value = mock_span mock_tracer.start_as_current_span.return_value = mock_span
mock_tracer.start_as_current_span.return_value.__enter__ = mock_span mock_tracer.start_as_current_span.return_value.__enter__ = mock.Mock(
return_value=mock_span
)
mock_tracer.start_as_current_span.return_value.__exit__ = mock_span mock_tracer.start_as_current_span.return_value.__exit__ = mock_span
with mock.patch("opentelemetry.trace.get_tracer") as tracer: with mock.patch("opentelemetry.trace.get_tracer") as tracer:
tracer.return_value = mock_tracer tracer.return_value = mock_tracer
@ -1342,6 +1344,65 @@ class TestAsgiApplication(AsgiTestBase):
) )
self.assertEqual(point.value, 0) self.assertEqual(point.value, 0)
def test_basic_metric_success_nonrecording_span(self):
mock_tracer = mock.Mock()
mock_span = mock.Mock()
mock_span.is_recording.return_value = False
mock_tracer.start_as_current_span.return_value = mock_span
mock_tracer.start_as_current_span.return_value.__enter__ = mock.Mock(
return_value=mock_span
)
mock_tracer.start_as_current_span.return_value.__exit__ = mock_span
with mock.patch("opentelemetry.trace.get_tracer") as tracer:
tracer.return_value = mock_tracer
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
self.seed_app(app)
start = default_timer()
self.send_default_request()
duration = max(round((default_timer() - start) * 1000), 0)
expected_duration_attributes = {
"http.method": "GET",
"http.host": "127.0.0.1",
"http.scheme": "http",
"http.flavor": "1.0",
"net.host.port": 80,
"http.status_code": 200,
}
expected_requests_count_attributes = {
"http.method": "GET",
"http.host": "127.0.0.1",
"http.scheme": "http",
"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:
for point in list(metric.data.data_points):
if isinstance(point, HistogramDataPoint):
self.assertDictEqual(
expected_duration_attributes,
dict(point.attributes),
)
self.assertEqual(point.count, 1)
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 metric.name == "http.server.request.size":
self.assertEqual(128, point.sum)
elif isinstance(point, NumberDataPoint):
self.assertDictEqual(
expected_requests_count_attributes,
dict(point.attributes),
)
self.assertEqual(point.value, 0)
def test_basic_metric_success_new_semconv(self): def test_basic_metric_success_new_semconv(self):
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
self.seed_app(app) self.seed_app(app)

View File

@ -355,29 +355,35 @@ def _set_status(
sem_conv_opt_in_mode, sem_conv_opt_in_mode,
): ):
if status_code < 0: if status_code < 0:
if _report_new(sem_conv_opt_in_mode): metrics_attributes[ERROR_TYPE] = status_code_str
span.set_attribute(ERROR_TYPE, status_code_str) if span.is_recording():
metrics_attributes[ERROR_TYPE] = status_code_str if _report_new(sem_conv_opt_in_mode):
span.set_attribute(ERROR_TYPE, status_code_str)
span.set_status( span.set_status(
Status( Status(
StatusCode.ERROR, StatusCode.ERROR,
"Non-integer HTTP status: " + status_code_str, "Non-integer HTTP status: " + status_code_str,
)
) )
)
else: else:
status = http_status_to_status_code(status_code, server_span=True) status = http_status_to_status_code(status_code, server_span=True)
if _report_old(sem_conv_opt_in_mode): if _report_old(sem_conv_opt_in_mode):
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) if span.is_recording():
span.set_attribute(
SpanAttributes.HTTP_STATUS_CODE, status_code
)
metrics_attributes[SpanAttributes.HTTP_STATUS_CODE] = status_code metrics_attributes[SpanAttributes.HTTP_STATUS_CODE] = status_code
if _report_new(sem_conv_opt_in_mode): if _report_new(sem_conv_opt_in_mode):
span.set_attribute(HTTP_RESPONSE_STATUS_CODE, status_code) if span.is_recording():
span.set_attribute(HTTP_RESPONSE_STATUS_CODE, status_code)
metrics_attributes[HTTP_RESPONSE_STATUS_CODE] = status_code metrics_attributes[HTTP_RESPONSE_STATUS_CODE] = status_code
if status == StatusCode.ERROR: if status == StatusCode.ERROR:
span.set_attribute(ERROR_TYPE, status_code_str) if span.is_recording():
span.set_attribute(ERROR_TYPE, status_code_str)
metrics_attributes[ERROR_TYPE] = status_code_str metrics_attributes[ERROR_TYPE] = status_code_str
span.set_status(Status(status)) if span.is_recording():
span.set_status(Status(status))
# Get schema version based off of opt-in mode # Get schema version based off of opt-in mode