From 84ae211347f70272533d8d5c84d37db5dde3053f Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Thu, 9 Apr 2026 19:48:26 +0530 Subject: [PATCH 01/10] fix: _set_status should not override existing span status (#3713) --- .../opentelemetry/instrumentation/_semconv.py | 16 ++++- .../tests/test_semconv_status.py | 65 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 opentelemetry-instrumentation/tests/test_semconv_status.py diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 1edd18d038..02faee76e1 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -650,7 +650,21 @@ 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)) + current_status = getattr(span, "status", None) + if current_status is not None: + _STATUS_PRIORITY = { + StatusCode.UNSET: 0, + StatusCode.OK: 1, + StatusCode.ERROR: 2, + } + current_priority = _STATUS_PRIORITY.get(current_status.status_code, 0) + incoming_priority = _STATUS_PRIORITY.get(status, 0) + if incoming_priority < current_priority: + return + description = current_status.description if current_status.description and status == current_status.status_code else None + span.set_status(Status(status, description)) + else: + span.set_status(Status(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..aee3a3ca4b --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_semconv_status.py @@ -0,0 +1,65 @@ +# 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.trace.status import Status, StatusCode +from opentelemetry.instrumentation._semconv import ( + _set_status, + _StabilityMode, +) + + +class TestSetStatus(unittest.TestCase): + + def _make_span(self, status_code, description=None): + span = MagicMock() + span.is_recording.return_value = True + span.status = Status(status_code, description) + return span + + def test_does_not_downgrade_error_to_ok(self): + """ERROR status should not be overridden by a lower priority OK""" + span = self._make_span(StatusCode.ERROR, "original error") + _set_status(span, {}, 200, "200", server_span=True, + sem_conv_opt_in_mode=_StabilityMode.DEFAULT) + # set_status should NOT have been called (no downgrade) + 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 = self._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 = self._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 = self._make_span(StatusCode.UNSET) + _set_status(span, {}, 200, "200", server_span=False, + sem_conv_opt_in_mode=_StabilityMode.DEFAULT) + span.set_status.assert_called() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From c2114c5472d23f0a12918b29503d12ac200198a6 Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Thu, 9 Apr 2026 20:00:07 +0530 Subject: [PATCH 02/10] changelog: add entry for _set_status fix (#3713) --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c108e893e..4e12991ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ## 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 @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `opentelemetry-instrumentation`: fix `_set_status` overriding existing span status and description ([#4410](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4410)) - `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)) - `opentelemetry-instrumentation-confluent-kafka`: Skip `recv` span creation when `poll()` returns no message or `consume()` returns an empty list, avoiding empty spans on idle polls @@ -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)` From d712f06893f7bcfa5024e549abb729e42f043fbc Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Thu, 9 Apr 2026 20:09:42 +0530 Subject: [PATCH 03/10] style: fix formatting in _semconv.py --- .../src/opentelemetry/instrumentation/_semconv.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 02faee76e1..852a6ca6e5 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -657,11 +657,18 @@ def _set_status( StatusCode.OK: 1, StatusCode.ERROR: 2, } - current_priority = _STATUS_PRIORITY.get(current_status.status_code, 0) + current_priority = _STATUS_PRIORITY.get( + current_status.status_code, 0 + ) incoming_priority = _STATUS_PRIORITY.get(status, 0) if incoming_priority < current_priority: return - description = current_status.description if current_status.description and status == current_status.status_code else None + description = ( + current_status.description + if current_status.description + and status == current_status.status_code + else None + ) span.set_status(Status(status, description)) else: span.set_status(Status(status)) From 3ac46997d66a0fac8719726498820c3a417b90b7 Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Fri, 10 Apr 2026 12:27:03 +0530 Subject: [PATCH 04/10] fix: resolve lint and formatting issues in _semconv.py and tests --- .../opentelemetry/instrumentation/_semconv.py | 43 ++++++++-------- .../tests/test_semconv_status.py | 49 +++++++++++++------ 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 852a6ca6e5..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,28 +670,7 @@ def _set_status( span.set_attribute(ERROR_TYPE, status_code_str) metrics_attributes[ERROR_TYPE] = status_code_str if span.is_recording(): - current_status = getattr(span, "status", None) - if current_status is not None: - _STATUS_PRIORITY = { - StatusCode.UNSET: 0, - StatusCode.OK: 1, - StatusCode.ERROR: 2, - } - current_priority = _STATUS_PRIORITY.get( - current_status.status_code, 0 - ) - incoming_priority = _STATUS_PRIORITY.get(status, 0) - if incoming_priority < current_priority: - return - description = ( - current_status.description - if current_status.description - and status == current_status.status_code - else None - ) - span.set_status(Status(status, description)) - else: - 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 index aee3a3ca4b..bdbebf055a 100644 --- a/opentelemetry-instrumentation/tests/test_semconv_status.py +++ b/opentelemetry-instrumentation/tests/test_semconv_status.py @@ -8,15 +8,15 @@ import unittest from unittest.mock import MagicMock -from opentelemetry.trace.status import Status, StatusCode + from opentelemetry.instrumentation._semconv import ( _set_status, _StabilityMode, ) +from opentelemetry.trace.status import Status, StatusCode class TestSetStatus(unittest.TestCase): - def _make_span(self, status_code, description=None): span = MagicMock() span.is_recording.return_value = True @@ -26,9 +26,14 @@ def _make_span(self, status_code, description=None): def test_does_not_downgrade_error_to_ok(self): """ERROR status should not be overridden by a lower priority OK""" span = self._make_span(StatusCode.ERROR, "original error") - _set_status(span, {}, 200, "200", server_span=True, - sem_conv_opt_in_mode=_StabilityMode.DEFAULT) - # set_status should NOT have been called (no downgrade) + _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: @@ -37,8 +42,14 @@ def test_does_not_downgrade_error_to_ok(self): def test_does_not_wipe_description_with_none(self): """Same ERROR status should preserve existing description""" span = self._make_span(StatusCode.ERROR, "keep this message") - _set_status(span, {}, 500, "500", server_span=True, - sem_conv_opt_in_mode=_StabilityMode.DEFAULT) + _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] @@ -47,8 +58,14 @@ def test_does_not_wipe_description_with_none(self): def test_upgrades_unset_to_error(self): """UNSET status should be upgraded to ERROR""" span = self._make_span(StatusCode.UNSET) - _set_status(span, {}, 500, "500", server_span=True, - sem_conv_opt_in_mode=_StabilityMode.DEFAULT) + _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) @@ -56,10 +73,12 @@ def test_upgrades_unset_to_error(self): def test_unset_to_ok(self): """UNSET status should be upgraded to OK for 2xx""" span = self._make_span(StatusCode.UNSET) - _set_status(span, {}, 200, "200", server_span=False, - sem_conv_opt_in_mode=_StabilityMode.DEFAULT) + _set_status( + span, + {}, + 200, + "200", + server_span=False, + sem_conv_opt_in_mode=_StabilityMode.DEFAULT, + ) span.set_status.assert_called() - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file From 8a33f4a12704529423c0e1eafe123fa65b9a0e5a Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Fri, 10 Apr 2026 12:43:38 +0530 Subject: [PATCH 05/10] fix: convert _make_span to module-level function to fix pylint R6301 --- .../tests/test_semconv_status.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/opentelemetry-instrumentation/tests/test_semconv_status.py b/opentelemetry-instrumentation/tests/test_semconv_status.py index bdbebf055a..f268a82823 100644 --- a/opentelemetry-instrumentation/tests/test_semconv_status.py +++ b/opentelemetry-instrumentation/tests/test_semconv_status.py @@ -16,16 +16,17 @@ from opentelemetry.trace.status import Status, StatusCode -class TestSetStatus(unittest.TestCase): - def _make_span(self, status_code, description=None): - span = MagicMock() - span.is_recording.return_value = True - span.status = Status(status_code, description) - return span +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 = self._make_span(StatusCode.ERROR, "original error") + span = _make_span(StatusCode.ERROR, "original error") _set_status( span, {}, @@ -41,7 +42,7 @@ def test_does_not_downgrade_error_to_ok(self): def test_does_not_wipe_description_with_none(self): """Same ERROR status should preserve existing description""" - span = self._make_span(StatusCode.ERROR, "keep this message") + span = _make_span(StatusCode.ERROR, "keep this message") _set_status( span, {}, @@ -57,7 +58,7 @@ def test_does_not_wipe_description_with_none(self): def test_upgrades_unset_to_error(self): """UNSET status should be upgraded to ERROR""" - span = self._make_span(StatusCode.UNSET) + span = _make_span(StatusCode.UNSET) _set_status( span, {}, @@ -72,7 +73,7 @@ def test_upgrades_unset_to_error(self): def test_unset_to_ok(self): """UNSET status should be upgraded to OK for 2xx""" - span = self._make_span(StatusCode.UNSET) + span = _make_span(StatusCode.UNSET) _set_status( span, {}, From c280aebadce4010a2ab54e385a1e086f0360ec93 Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Fri, 10 Apr 2026 14:21:27 +0530 Subject: [PATCH 06/10] fix: add self assertion in test_unset_to_ok to fix pylint R6301 --- opentelemetry-instrumentation/tests/test_semconv_status.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/tests/test_semconv_status.py b/opentelemetry-instrumentation/tests/test_semconv_status.py index f268a82823..dd8aecf487 100644 --- a/opentelemetry-instrumentation/tests/test_semconv_status.py +++ b/opentelemetry-instrumentation/tests/test_semconv_status.py @@ -82,4 +82,5 @@ def test_unset_to_ok(self): server_span=False, 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.UNSET) From 7214d2963e6c839f23bc95910a1e2d856e790cef Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Fri, 10 Apr 2026 14:36:51 +0530 Subject: [PATCH 07/10] fix: move changelog entry to Unreleased section --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e12991ae4..89edee4c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ 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 @@ -28,9 +30,7 @@ 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-instrumentation`: fix `_set_status` overriding existing span status and description ([#4410](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4410)) - `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)) - `opentelemetry-instrumentation-confluent-kafka`: Skip `recv` span creation when `poll()` returns no message or `consume()` returns an empty list, avoiding empty spans on idle polls From c07192ba350abf437863f0324231ac159db27b65 Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Tue, 14 Apr 2026 07:57:03 +0530 Subject: [PATCH 08/10] fix: address maintainer feedback - move dict to module level and fix test description --- .../opentelemetry/instrumentation/_semconv.py | 36 ++++++++++--------- .../tests/test_semconv_status.py | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 0c1595ffce..85c6f6b79a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -610,26 +610,28 @@ def _set_db_operation( # General +_STATUS_CODE_PRIORITY = { + StatusCode.UNSET: 0, + StatusCode.OK: 1, + StatusCode.ERROR: 2, +} + + 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: + if current is None: span.set_status(Status(status)) + return + if _STATUS_CODE_PRIORITY.get(status, 0) < _STATUS_CODE_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)) def _set_status( diff --git a/opentelemetry-instrumentation/tests/test_semconv_status.py b/opentelemetry-instrumentation/tests/test_semconv_status.py index dd8aecf487..e2692a1dc5 100644 --- a/opentelemetry-instrumentation/tests/test_semconv_status.py +++ b/opentelemetry-instrumentation/tests/test_semconv_status.py @@ -72,7 +72,7 @@ def test_upgrades_unset_to_error(self): self.assertEqual(last_call.status_code, StatusCode.ERROR) def test_unset_to_ok(self): - """UNSET status should be upgraded to OK for 2xx""" + """2xx client spans should not change existing UNSET status""" span = _make_span(StatusCode.UNSET) _set_status( span, From 87c8e78b0ea145625f0235dfc846c450993f828e Mon Sep 17 00:00:00 2001 From: Riya Chaturvedi Date: Fri, 17 Apr 2026 08:49:18 +0530 Subject: [PATCH 09/10] Update opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py Co-authored-by: Mike Goldsmith --- .../src/opentelemetry/instrumentation/_semconv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 85c6f6b79a..31307a38d0 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -618,7 +618,7 @@ def _set_db_operation( def _set_span_status(span: Span, status: StatusCode) -> None: - current = getattr(span, "status", None) + current = span.status if current is None: span.set_status(Status(status)) return From 4826bc308e2dd0b6d6c1fb35936b62cf87ecc8b9 Mon Sep 17 00:00:00 2001 From: RiyaChaturvedi37 Date: Fri, 17 Apr 2026 09:44:07 +0530 Subject: [PATCH 10/10] fix: revert span.status to getattr to fix wsgi/asgi mock tests --- .../src/opentelemetry/instrumentation/_semconv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py index 31307a38d0..85c6f6b79a 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -618,7 +618,7 @@ def _set_db_operation( def _set_span_status(span: Span, status: StatusCode) -> None: - current = span.status + current = getattr(span, "status", None) if current is None: span.set_status(Status(status)) return