Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `opentelemetry-instrumentation-httpx`: fix missing metric response attributes when tracing is disabled
([#3615](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3615))
- `opentelemetry-instrumentation-asgi`: fix excluded_urls in instrumentation-asgi
([#3567](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3567))
- `opentelemetry-resource-detector-containerid`: make it more quiet on platforms without cgroups
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,6 @@ def _apply_request_client_attributes_to_span(

def _apply_response_client_attributes_to_span(
span: Span,
metric_attributes: dict[str, typing.Any],
status_code: int,
http_version: str,
semconv: _StabilityMode,
Expand All @@ -433,6 +432,30 @@ def _apply_response_client_attributes_to_span(
http_status_code = http_status_to_status_code(status_code)
span.set_status(http_status_code)

if http_status_code == StatusCode.ERROR and _report_new(semconv):
# http semconv transition: new error.type
span_attributes[ERROR_TYPE] = str(status_code)

if http_version and _report_new(semconv):
# http semconv transition: http.flavor -> network.protocol.version
_set_http_network_protocol_version(
span_attributes,
http_version.replace("HTTP/", ""),
semconv,
)

for key, val in span_attributes.items():
span.set_attribute(key, val)


def _apply_response_client_attributes_to_metrics(
span: Span | None,
metric_attributes: dict[str, typing.Any],
status_code: int,
http_version: str,
semconv: _StabilityMode,
) -> None:
"""Apply response attributes to metric attributes."""
# Set HTTP status code in metric attributes
_set_status(
span,
Expand All @@ -443,26 +466,12 @@ def _apply_response_client_attributes_to_span(
sem_conv_opt_in_mode=semconv,
)

if http_status_code == StatusCode.ERROR and _report_new(semconv):
# http semconv transition: new error.type
span_attributes[ERROR_TYPE] = str(status_code)

if http_version and _report_new(semconv):
# http semconv transition: http.flavor -> network.protocol.version
_set_http_network_protocol_version(
metric_attributes,
http_version.replace("HTTP/", ""),
semconv,
)
if _report_new(semconv):
_set_http_network_protocol_version(
span_attributes,
http_version.replace("HTTP/", ""),
semconv,
)

for key, val in span_attributes.items():
span.set_attribute(key, val)


class SyncOpenTelemetryTransport(httpx.BaseTransport):
Expand Down Expand Up @@ -592,11 +601,19 @@ def handle_request(
_extract_response(response)
)

# Always apply response attributes to metrics
_apply_response_client_attributes_to_metrics(
span,
metric_attributes,
status_code,
http_version,
self._sem_conv_opt_in_mode,
)

if span.is_recording():
# apply http client response attributes according to semconv
_apply_response_client_attributes_to_span(
span,
metric_attributes,
status_code,
http_version,
self._sem_conv_opt_in_mode,
Expand Down Expand Up @@ -777,11 +794,19 @@ async def handle_async_request(
_extract_response(response)
)

# Always apply response attributes to metrics
_apply_response_client_attributes_to_metrics(
span,
metric_attributes,
status_code,
http_version,
self._sem_conv_opt_in_mode,
)

if span.is_recording():
# apply http client response attributes according to semconv
_apply_response_client_attributes_to_span(
span,
metric_attributes,
status_code,
http_version,
self._sem_conv_opt_in_mode,
Expand Down Expand Up @@ -1001,11 +1026,19 @@ def _handle_request_wrapper( # pylint: disable=too-many-locals
_extract_response(response)
)

# Always apply response attributes to metrics
_apply_response_client_attributes_to_metrics(
span,
metric_attributes,
status_code,
http_version,
sem_conv_opt_in_mode,
)

if span.is_recording():
# apply http client response attributes according to semconv
_apply_response_client_attributes_to_span(
span,
metric_attributes,
status_code,
http_version,
sem_conv_opt_in_mode,
Expand Down Expand Up @@ -1109,11 +1142,19 @@ async def _handle_async_request_wrapper( # pylint: disable=too-many-locals
_extract_response(response)
)

# Always apply response attributes to metrics
_apply_response_client_attributes_to_metrics(
span,
metric_attributes,
status_code,
http_version,
sem_conv_opt_in_mode,
)

if span.is_recording():
# apply http client response attributes according to semconv
_apply_response_client_attributes_to_span(
span,
metric_attributes,
status_code,
http_version,
sem_conv_opt_in_mode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,60 @@ def test_custom_meter_provider(self):
)
self.assertEqual(data_point.count, 1)

def _run_disabled_tracing_metrics_attributes_test(
self,
url: str,
expected_attributes: dict,
) -> None:
with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span:
client = self.create_client(
self.create_transport(
tracer_provider=trace.NoOpTracerProvider()
)
)
mock_span.is_recording.return_value = False
self.perform_request(url, client=client)

self.assertFalse(mock_span.is_recording())
self.assertTrue(mock_span.is_recording.called)

metrics = self.assert_metrics(num_metrics=1)
duration_data_point = metrics[0].data.data_points[0]

self.assertEqual(duration_data_point.count, 1)
self.assertEqual(
dict(duration_data_point.attributes),
expected_attributes,
)

def test_metrics_have_response_attributes_with_disabled_tracing(
self,
) -> None:
"""Test that metrics have response attributes when tracing is disabled."""
self._run_disabled_tracing_metrics_attributes_test(
url=self.URL,
expected_attributes={
SpanAttributes.HTTP_STATUS_CODE: 200,
SpanAttributes.HTTP_METHOD: "GET",
SpanAttributes.HTTP_SCHEME: "http",
},
)

def test_metrics_have_response_attributes_with_disabled_tracing_new_semconv(
self,
) -> None:
"""Test that metrics have response attributes when tracing is disabled with new semantic conventions."""
self._run_disabled_tracing_metrics_attributes_test(
url="http://mock:8080/status/200",
expected_attributes={
SERVER_ADDRESS: "mock",
HTTP_REQUEST_METHOD: "GET",
HTTP_RESPONSE_STATUS_CODE: 200,
NETWORK_PROTOCOL_VERSION: "1.1",
SERVER_PORT: 8080,
},
)

def test_response_hook(self):
transport = self.create_transport(
tracer_provider=self.tracer_provider,
Expand Down