Skip to content

Apply the specified status precedence in Span.set_status - #5662

Open
WatchTree-19 wants to merge 7 commits into
open-telemetry:mainfrom
WatchTree-19:fix-5661-set-status-keeps-description
Open

WatchTree-19 wants to merge 7 commits into
open-telemetry:mainfrom
WatchTree-19:fix-5661-set-status-keeps-description

Conversation

@WatchTree-19

@WatchTree-19 WatchTree-19 commented Sep 15, 2026

Copy link
Copy Markdown

Description

Fixes #5661.

Span.set_status now applies the precedence the specification gives, Ok > Error > Unset, in one place.

Before this the rule was split across two ad-hoc conditions, one of which only reads correctly if you know Python's and/or precedence, and nothing tested it. The SDK was mostly doing the right thing already: of the twelve transitions in the lattice, eleven were correct on main. The one that was not is #5661, where a bare Status(StatusCode.ERROR) replaced Status(StatusCode.ERROR, "...") and the description was silently lost. The status code did not change, so nothing looked wrong; only the message that carried the diagnostic value disappeared.

That is reachable without anyone doing anything odd. A library sets a specific error on a span, and later generic error handling, exception bookkeeping or an instrumentation helper sets a plain ERROR on the same span.

The decision now lives in _accepts_status, and it is the whole rule:

  • Unset is never recorded. An attempt to set it is ignored, whatever is already there.
  • Ok is final, so Error never displaces Ok. Once Unset and Ok are handled, any code that differs from the one recorded outranks it and is recorded.
  • A status repeating the code already recorded is ignored unless it supplies a description where none was recorded, so a message already on the span is never replaced and never dropped.

There is no separate ranking helper. Because Unset is ignored and Ok is final, every remaining change of code is an upgrade, so _accepts_status only has to ask whether the code differs and, when it does not, whether a missing description is being filled in.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

opentelemetry-sdk/tests/trace/test_trace.py passes in full: 116 passed, 35 subtests passed.

The thirteen transitions are a _PRECEDENCE_CASES table in TestSpan, each entry being the calls to make and the status code and description expected. The table is driven through self.subTest and run twice, once through Status instances and once through the StatusCode overload, so the overload is exercised on every case rather than on one.

The cases were checked to discriminate rather than merely pass:

  • reverting the same-code rule to the earlier new_status.description is not None turns two subtests red
  • removing _accepts_status altogether turns twelve red

ruff check and ruff format are clean and pylint scores 10.00/10 on both changed files.

The behaviour was also reproduced against released opentelemetry-sdk 1.44.0 on CPython 3.11 before anything was changed, using the snippet in #5661.

Does This PR Require a Contrib Repo Change?

  • Yes. - Link to PR:
  • No.

Checklist:

  • Followed the style guidelines of this project
  • Changelogs have been updated
  • Unit tests have been added
  • Documentation has been updated

On the changelog: .changelog/ names fragments after the PR number, which does not exist until this is opened, so <pr>.fixed follows as a second commit straight after.

AI assistance was used and is disclosed on the commits with an Assisted-by: trailer, per AGENTS.md.

Fixes open-telemetry#5661.

set_status returns early when the span is already OK and when the incoming
status is UNSET, but nothing stops a second ERROR replacing a first one. A bare
Status(StatusCode.ERROR) therefore overwrites Status(StatusCode.ERROR, "...")
and the description is gone. The status code is unchanged, so the loss is
silent.

The assignment is now declined when the incoming status carries no description,
the recorded one does, and the codes match. A described status still replaces a
described status, a bare status still lands when there is nothing to keep, and a
different status code still takes precedence as before.

Assisted-by: Claude Opus 5
Signed-off-by: WatchTree-19 <119982314+WatchTree-19@users.noreply.github.com>
@WatchTree-19
WatchTree-19 requested a review from a team as a code owner September 15, 2026 12:37
Assisted-by: Claude Opus 5
Signed-off-by: WatchTree-19 <119982314+WatchTree-19@users.noreply.github.com>
@herin049

herin049 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

We should probably update this to be more in line with what the spec expects:

Namely that Span statuses form a total order Ok > Error > Unset and any attempts to update the status that are not strictly greater than the previous status should be ignored (i.e. all future calls to set status will be ignored when the status is set to Error unless the new status is Ok, and every future call to set status will be ignored when the status is set to Ok). A call to explicitly set the status to Unset should always be ignored.

The specification gives the status codes a total order, Ok > Error > Unset,
says an attempt to set Unset should be ignored, and says the value of the
last call is the one recorded. The SDK already behaved that way, but the
rule was spread across two ad-hoc conditions and one of them relied on
Python's and/or precedence to read correctly, so it was neither obvious nor
covered by tests.

State the order explicitly in _STATUS_PRECEDENCE and move the decision into
_accepts_status, then pin every transition in the lattice with a test. The
enum's own values are Unset=0, Ok=1, Error=2, which is not the specified
order, so the map cannot be replaced by a comparison on the enum.

Within a single status code the last call still wins, with one exception
kept from the original fix: a bare status carries no new information, so
letting it through would drop a description already recorded in favour of
nothing.
@WatchTree-19 WatchTree-19 changed the title Keep an existing status description when a bare Status(ERROR) is set Apply the specified status precedence in Span.set_status Sep 15, 2026
@WatchTree-19

Copy link
Copy Markdown
Author

Thanks for laying the ordering out, I went through each point against the spec. Three of the four are in there: the total order Ok > Error > Unset, "an attempt to set value Unset SHOULD be ignored", and Ok being final. I couldn't find the fourth one, ignoring anything that isn't strictly greater. The spec says "only the value of the last call will be recorded, and implementations are free to ignore previous calls." Taken to the letter, your rule would mean Error("first") followed by Error("second") keeps "first", which is what that last call sentence rules out.

I also wrote all twelve transitions up as tests and ran them against upstream main. Eleven of them pass, and the only one that fails is the bug I first reported. So the SDK was mostly doing the right thing already. The ordering just wasn't written down anywhere: it was split across two conditions, one of which only reads right if you know Python's and/or precedence, and nothing tested it.

What I've pushed now writes the order out explicitly and puts the decision in one method. It adds the twelve tests and keeps last call wins within the same rank, along with the exception for bare status. One thing worth mentioning: the enum is UNSET=0, OK=1, ERROR=2, which isn't the spec's order, so it can't be a plain enum comparison.

The main thing I wanted to check with you: should Error to Error be ignored as you wrote it, even though that goes against the spec's last call sentence? Or should the ordering be made explicit with last call wins kept, which is what's on the branch now?

@Shriprasad-P Shriprasad-P left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice focused PR. Please make sure docs/changelog stay aligned if the repo requires it for this kind of change.

@WatchTree-19

Copy link
Copy Markdown
Author

@herin049 any thoughts on the Error to Error question when you get a minute? Happy to switch it to ignoring a repeat of the same code if that is what you want, it is a small change. I only left it as last call wins because the spec says the value of the last call is the one recorded, and I did not want to diverge from that without you saying so.

@Shriprasad-P thanks. The changelog fragment was rewritten alongside the code, .changelog/5662.fixed now describes the precedence change rather than the original description fix.

@herin049

Copy link
Copy Markdown
Contributor

Namely that Span statuses form a total order Ok > Error > Unset

The last sentence in the spec that states "Only the value of the last call will be recorded, and implementations are free to ignore previous calls." is a little strange and partially contradicts what is stated earlier in the paragraph. As a whole, I take this to mean that if a higher precedence status is set on a span, then previous calls can be ignored.

I would expect that it is safe to ignore calls to set the status to Error if the status already is Error.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Sep 17, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on maintainers · refreshed 2026-09-21 17:31 UTC

Merge when ready.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@WatchTree-19

Copy link
Copy Markdown
Author

Thanks. The changelog fragment is already in the branch, .changelog/5662.fixed, which is the format this repo moved to instead of editing CHANGELOG.md directly. It covers all three behaviours the patch changes: the Ok > Error > Unset order being enforced explicitly, Unset always being ignored, and a bare Status(ERROR) no longer dropping a description that was already recorded. No user-facing docs reference the old behaviour, so nothing else needed there as far as I can see, but happy to add to it if you think somewhere else should call this out.

Comment thread opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py Outdated
Comment thread opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py Outdated
Comment thread opentelemetry-sdk/tests/trace/test_trace.py Outdated
…Tests

Three points from the review.

A same-code status now has to fill in a description that is missing rather
than merely carry one. Previously any described status replaced a described
status of the same code, on the reading that the last call wins. The review
asked that an already populated description not be overwritten, and that
also settles the Error to Error question raised earlier in the thread: a
repeat of a code already recorded is ignored unless it adds a message where
there was none. One assertion inverts with this, the case that used to
expect the second description now expects the first.

The precedence map becomes _status_precedence, a small function with a match
expression and a default arm. A code the ordering has not been taught about
ranks below Unset, so an unknown status cannot displace one already
recorded, and it raises nothing at runtime.

The twelve transition tests become a table of thirteen cases driven through
self.subTest, and the table is now run twice, once through Status instances
and once through the StatusCode overload, which covers the overload on every
case rather than on one.

Assisted-by: Claude Opus 5
Signed-off-by: WatchTree-19 <119982314+WatchTree-19@users.noreply.github.com>
@WatchTree-19

Copy link
Copy Markdown
Author

Thanks, all three are in 69b6ab6 and I have replied on each thread.

The one behaviour change is the first. A status that repeats the code already recorded is now ignored unless it supplies a description where none was recorded, so a message already on the span is never replaced and never dropped. That is your Error to Error point as well, and I am happy with it: the ordering is about codes, and once the code is unchanged the only call left carrying new information is one that adds a message. So the last call sentence still governs, it just governs the description rather than licensing a bare overwrite.

Verification: 116 passed and 35 subtests passed in opentelemetry-sdk/tests/trace/test_trace.py on CPython 3.11, ruff check and ruff format clean, pylint 10.00/10 on both changed files. The cases were checked to discriminate rather than merely pass. Reverting the same-code rule to the earlier description is not None turns two subtests red, and removing _accepts_status altogether turns twelve red.

The changelog fragment now describes the precedence behaviour as it stands, and I have rewritten the PR description to match, since it still named the old tests.

Comment thread opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Same-code handling incorrectly prevents replacing an existing error description with a newer one.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Updates SDK span status handling to enforce status-code precedence and preserve error descriptions.

Changes:

  • Centralizes status precedence logic.
  • Adds transition tests for both accepted input forms.
  • Adds a changelog entry.
File Description
opentelemetry-sdk/​src/​opentelemetry/​sdk/​trace/​__init__.py Implements status precedence.
opentelemetry-sdk/​tests/​trace/​test_trace.py Tests status transitions.
.changelog/​5662.fixed Documents the fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py Outdated
Comment thread .changelog/5662.fixed Outdated
Once Unset is ignored and Ok is final, any code that differs from the one
recorded is an upgrade, so the rank comparison is not needed. No change in
behaviour. Also fixes the article before Unset in the changelog fragment.

Assisted-by: Claude Opus 5
Signed-off-by: WatchTree-19 <119982314+WatchTree-19@users.noreply.github.com>
@herin049 herin049 moved this to Approved PRs in Python PR digest Sep 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Approved PRs

Development

Successfully merging this pull request may close these issues.

Span.set_status: a bare Status(ERROR) replaces an existing Status(ERROR, description) and drops the description

4 participants