diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c108e893e..89edee4c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > Use [this search for a list of all CHANGELOG.md files in this repo](https://github.com/search?q=repo%3Aopen-telemetry%2Fopentelemetry-python-contrib+path%3A**%2FCHANGELOG.md&type=code). ## Unreleased +### Fixed +- `opentelemetry-instrumentation`: fix `_set_status` overriding existing span status and description ([#4410](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4410)) ## Version 1.41.0/0.62b0 (2026-04-09) - + ### Added - `opentelemetry-instrumentation-asgi`: Respect `suppress_http_instrumentation` context in ASGI middleware to skip server span creation when HTTP instrumentation is suppressed @@ -28,7 +30,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-sqlalchemy`: implement new semantic convention opt-in migration ([#4110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4110)) -### Fixed - `opentelemetry-docker-tests`: Replace deprecated `SpanAttributes` from `opentelemetry.semconv.trace` with `opentelemetry.semconv._incubating.attributes` ([#4339](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4339)) @@ -45,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-grpc`: Fix bidirectional streaming RPCs raising `AttributeError: 'generator' object has no attribute 'add_done_callback'` ([#4259](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4259)) - `opentelemetry-instrumentation-aiokafka`: fix `Unclosed AIOKafkaProducer` warning and `RuntimeWarning: coroutine was never awaited` in tests - ([#4384](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4384)) + ([#4384](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4384)) - `opentelemetry-instrumentation-aiokafka`: Fix compatibility with aiokafka 0.13 by calling `_key_serializer`/`_value_serializer` directly instead of the internal `_serialize` method whose signature changed in 0.13 from `(topic, key, value)` to `(key, value, headers)` diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 1edd18d038..0c1595ffce 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -610,6 +610,26 @@ def _set_db_operation( # General +def _set_span_status(span: Span, status: StatusCode) -> None: + status_priority = { + StatusCode.UNSET: 0, + StatusCode.OK: 1, + StatusCode.ERROR: 2, + } + current = getattr(span, "status", None) + if current is not None: + if status_priority.get(status, 0) < status_priority.get( + current.status_code, 0 + ): + return + description = ( + current.description + if current.description and status == current.status_code + else None + ) + span.set_status(Status(status, description)) + else: + span.set_status(Status(status)) def _set_status( @@ -650,7 +670,7 @@ def _set_status( span.set_attribute(ERROR_TYPE, status_code_str) metrics_attributes[ERROR_TYPE] = status_code_str if span.is_recording(): - span.set_status(Status(status)) + _set_span_status(span, status) def _get_schema_url(mode: _StabilityMode) -> str: diff --git a/opentelemetry-instrumentation/tests/test_semconv_status.py b/opentelemetry-instrumentation/tests/test_semconv_status.py new file mode 100644 index 0000000000..dd8aecf487 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_semconv_status.py @@ -0,0 +1,86 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +import unittest +from unittest.mock import MagicMock + +from opentelemetry.instrumentation._semconv import ( + _set_status, + _StabilityMode, +) +from opentelemetry.trace.status import Status, StatusCode + + +def _make_span(status_code, description=None): + span = MagicMock() + span.is_recording.return_value = True + span.status = Status(status_code, description) + return span + + +class TestSetStatus(unittest.TestCase): + def test_does_not_downgrade_error_to_ok(self): + """ERROR status should not be overridden by a lower priority OK""" + span = _make_span(StatusCode.ERROR, "original error") + _set_status( + span, + {}, + 200, + "200", + server_span=True, + sem_conv_opt_in_mode=_StabilityMode.DEFAULT, + ) + for call in span.set_status.call_args_list: + args = call[0] + if args: + self.assertNotEqual(args[0].status_code, StatusCode.OK) + + def test_does_not_wipe_description_with_none(self): + """Same ERROR status should preserve existing description""" + span = _make_span(StatusCode.ERROR, "keep this message") + _set_status( + span, + {}, + 500, + "500", + server_span=True, + sem_conv_opt_in_mode=_StabilityMode.DEFAULT, + ) + last_call = span.set_status.call_args + if last_call: + status_arg = last_call[0][0] + self.assertEqual(status_arg.description, "keep this message") + + def test_upgrades_unset_to_error(self): + """UNSET status should be upgraded to ERROR""" + span = _make_span(StatusCode.UNSET) + _set_status( + span, + {}, + 500, + "500", + server_span=True, + sem_conv_opt_in_mode=_StabilityMode.DEFAULT, + ) + span.set_status.assert_called() + last_call = span.set_status.call_args[0][0] + self.assertEqual(last_call.status_code, StatusCode.ERROR) + + def test_unset_to_ok(self): + """UNSET status should be upgraded to OK for 2xx""" + span = _make_span(StatusCode.UNSET) + _set_status( + span, + {}, + 200, + "200", + server_span=False, + sem_conv_opt_in_mode=_StabilityMode.DEFAULT, + ) + last_call = span.set_status.call_args[0][0] + self.assertEqual(last_call.status_code, StatusCode.UNSET)