Skip to content
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand All @@ -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)`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
86 changes: 86 additions & 0 deletions opentelemetry-instrumentation/tests/test_semconv_status.py
Original file line number Diff line number Diff line change
@@ -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)
Loading