Skip to content

Commit 84ae211

Browse files
fix: _set_status should not override existing span status (#3713)
1 parent bef5251 commit 84ae211

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,21 @@ def _set_status(
650650
span.set_attribute(ERROR_TYPE, status_code_str)
651651
metrics_attributes[ERROR_TYPE] = status_code_str
652652
if span.is_recording():
653-
span.set_status(Status(status))
653+
current_status = getattr(span, "status", None)
654+
if current_status is not None:
655+
_STATUS_PRIORITY = {
656+
StatusCode.UNSET: 0,
657+
StatusCode.OK: 1,
658+
StatusCode.ERROR: 2,
659+
}
660+
current_priority = _STATUS_PRIORITY.get(current_status.status_code, 0)
661+
incoming_priority = _STATUS_PRIORITY.get(status, 0)
662+
if incoming_priority < current_priority:
663+
return
664+
description = current_status.description if current_status.description and status == current_status.status_code else None
665+
span.set_status(Status(status, description))
666+
else:
667+
span.set_status(Status(status))
654668

655669

656670
def _get_schema_url(mode: _StabilityMode) -> str:
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
import unittest
10+
from unittest.mock import MagicMock
11+
from opentelemetry.trace.status import Status, StatusCode
12+
from opentelemetry.instrumentation._semconv import (
13+
_set_status,
14+
_StabilityMode,
15+
)
16+
17+
18+
class TestSetStatus(unittest.TestCase):
19+
20+
def _make_span(self, status_code, description=None):
21+
span = MagicMock()
22+
span.is_recording.return_value = True
23+
span.status = Status(status_code, description)
24+
return span
25+
26+
def test_does_not_downgrade_error_to_ok(self):
27+
"""ERROR status should not be overridden by a lower priority OK"""
28+
span = self._make_span(StatusCode.ERROR, "original error")
29+
_set_status(span, {}, 200, "200", server_span=True,
30+
sem_conv_opt_in_mode=_StabilityMode.DEFAULT)
31+
# set_status should NOT have been called (no downgrade)
32+
for call in span.set_status.call_args_list:
33+
args = call[0]
34+
if args:
35+
self.assertNotEqual(args[0].status_code, StatusCode.OK)
36+
37+
def test_does_not_wipe_description_with_none(self):
38+
"""Same ERROR status should preserve existing description"""
39+
span = self._make_span(StatusCode.ERROR, "keep this message")
40+
_set_status(span, {}, 500, "500", server_span=True,
41+
sem_conv_opt_in_mode=_StabilityMode.DEFAULT)
42+
last_call = span.set_status.call_args
43+
if last_call:
44+
status_arg = last_call[0][0]
45+
self.assertEqual(status_arg.description, "keep this message")
46+
47+
def test_upgrades_unset_to_error(self):
48+
"""UNSET status should be upgraded to ERROR"""
49+
span = self._make_span(StatusCode.UNSET)
50+
_set_status(span, {}, 500, "500", server_span=True,
51+
sem_conv_opt_in_mode=_StabilityMode.DEFAULT)
52+
span.set_status.assert_called()
53+
last_call = span.set_status.call_args[0][0]
54+
self.assertEqual(last_call.status_code, StatusCode.ERROR)
55+
56+
def test_unset_to_ok(self):
57+
"""UNSET status should be upgraded to OK for 2xx"""
58+
span = self._make_span(StatusCode.UNSET)
59+
_set_status(span, {}, 200, "200", server_span=False,
60+
sem_conv_opt_in_mode=_StabilityMode.DEFAULT)
61+
span.set_status.assert_called()
62+
63+
64+
if __name__ == "__main__":
65+
unittest.main()

0 commit comments

Comments
 (0)