Repository: open-telemetry/opentelemetry-python

Issue:
set_result fails inside span, improve code or documentation
I think leaving this as it is an invitation to have people fall into this pit, like I just di. Either the interface should be extended to see if the caller passed a valid code or improve the documentation to make it clear what is expected and how someone can work around it. I am happy to do the documentation update if someone can tell me how I can propose a documentation change.

_Originally posted by @jspurestore in https://github.com/open-telemetry/opentelemetry-python/issues/1742#issuecomment-1132096662_


Discussion:
Making a doc update would be creating a pr directly with the [opentelemetry.io](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/instrumentation/python) repository.
The work around people find is actually the intended way to use it but I can understand where they are coming from. Here is the api documentation for `set_staus`
https://opentelemetry-python.readthedocs.io/en/latest/_modules/opentelemetry/trace/span.html#Span.set_status. It's easy to overlook the difference between `Status` and `StatusCode`. We discussed about this in past and decided just keep the current signature. Given how easy it is to mistake this api, I think we might want to revisit our decision.

@open-telemetry/python-approvers what do you think about changing the
`set_status(status: Status)` to 
`set_status(status: Union[Status, StatusCode], description: Optional[str] = None)`
> The work around people find is actually the intended way to use it but I can understand where they are coming from. Here is the api documentation for `set_staus` https://opentelemetry-python.readthedocs.io/en/latest/_modules/opentelemetry/trace/span.html#Span.set_status. It's easy to overlook the difference between `Status` and `StatusCode`. We discussed about this in past and decided just keep the current signature. Given how easy it is to mistake this api, I think we might want to revisit our decision.
> 
> @open-telemetry/python-approvers what do you think about changing the `set_status(status: Status)` to `set_status(status: Union[Status, StatusCode], description: Optional[str] = None)`

What would `description` be used for?
For the `description` attr of `Status` along with `status_code`.
> For the `description` attr of `Status` along with `status_code`.

Ok, sounds good :+1: 

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py
--- a/opentelemetry-api/src/opentelemetry/trace/span.py
+++ b/opentelemetry-api/src/opentelemetry/trace/span.py
@@ -5,7 +5,7 @@
 import typing
 from collections import OrderedDict
 
-from opentelemetry.trace.status import Status
+from opentelemetry.trace.status import Status, StatusCode
 from opentelemetry.util import types
 
 # The key MUST begin with a lowercase letter or a digit,
@@ -137,7 +137,11 @@ def is_recording(self) -> bool:
         """
 
     @abc.abstractmethod
-    def set_status(self, status: Status) -> None:
+    def set_status(
+        self,
+        status: typing.Union[Status, StatusCode],
+        description: typing.Optional[str] = None,
+    ) -> None:
         """Sets the Status of the Span. If used, this will override the default
         Span status.
         """
@@ -524,7 +528,11 @@ def add_event(
     def update_name(self, name: str) -> None:
         pass
 
-    def set_status(self, status: Status) -> None:
+    def set_status(
+        self,
+        status: typing.Union[Status, StatusCode],
+        description: typing.Optional[str] = None,
+    ) -> None:
         pass
 
     def record_exception(
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
@@ -889,16 +889,34 @@ def is_recording(self) -> bool:
         return self._end_time is None
 
     @_check_span_ended
-    def set_status(self, status: trace_api.Status) -> None:
+    def set_status(
+        self,
+        status: typing.Union[Status, StatusCode],
+        description: typing.Optional[str] = None,
+    ) -> None:
         # Ignore future calls if status is already set to OK
         # Ignore calls to set to StatusCode.UNSET
-        if (
-            self._status
-            and self._status.status_code is StatusCode.OK
-            or status.status_code is StatusCode.UNSET
-        ):
-            return
-        self._status = status
+        if isinstance(status, Status):
+            if (
+                self._status
+                and self._status.status_code is StatusCode.OK
+                or status.status_code is StatusCode.UNSET
+            ):
+                return
+            if description is not None:
+                logger.warning(
+                    "Description %s ignored. Use either `Status` or `(StatusCode, Description)`",
+                    description,
+                )
+            self._status = status
+        elif isinstance(status, StatusCode):
+            if (
+                self._status
+                and self._status.status_code is StatusCode.OK
+                or status is StatusCode.UNSET
+            ):
+                return
+            self._status = Status(status, description)
 
     def __exit__(
         self,


Test patch:
diff --git a/opentelemetry-api/tests/trace/test_globals.py b/opentelemetry-api/tests/trace/test_globals.py
--- a/opentelemetry-api/tests/trace/test_globals.py
+++ b/opentelemetry-api/tests/trace/test_globals.py
@@ -12,7 +12,7 @@ class TestSpan(trace.NonRecordingSpan):
     recorded_exception = None
     recorded_status = Status(status_code=StatusCode.UNSET)
 
-    def set_status(self, status):
+    def set_status(self, status, description=None):
         self.recorded_status = status
 
     def end(self, end_time=None):
diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py
--- a/opentelemetry-sdk/tests/trace/test_trace.py
+++ b/opentelemetry-sdk/tests/trace/test_trace.py
@@ -45,7 +45,7 @@
     get_span_with_dropped_attributes_events_links,
     new_tracer,
 )
-from opentelemetry.trace import StatusCode
+from opentelemetry.trace import Status, StatusCode
 from opentelemetry.util._time import _time_ns
 
 
@@ -903,6 +903,39 @@ def test_span_override_start_and_end_time(self):
         span.end(end_time)
         self.assertEqual(end_time, span.end_time)
 
+    def test_span_set_status(self):
+
+        span1 = self.tracer.start_span("span1")
+        span1.set_status(Status(status_code=StatusCode.ERROR))
+        self.assertEqual(span1.status.status_code, StatusCode.ERROR)
+        self.assertEqual(span1.status.description, None)
+
+        span2 = self.tracer.start_span("span2")
+        span2.set_status(
+            Status(status_code=StatusCode.ERROR, description="desc")
+        )
+        self.assertEqual(span2.status.status_code, StatusCode.ERROR)
+        self.assertEqual(span2.status.description, "desc")
+
+        span3 = self.tracer.start_span("span3")
+        span3.set_status(StatusCode.ERROR)
+        self.assertEqual(span3.status.status_code, StatusCode.ERROR)
+        self.assertEqual(span3.status.description, None)
+
+        span4 = self.tracer.start_span("span4")
+        span4.set_status(StatusCode.ERROR, "span4 desc")
+        self.assertEqual(span4.status.status_code, StatusCode.ERROR)
+        self.assertEqual(span4.status.description, "span4 desc")
+
+        span5 = self.tracer.start_span("span5")
+        with self.assertLogs(level=WARNING):
+            span5.set_status(
+                Status(status_code=StatusCode.ERROR, description="desc"),
+                description="ignored",
+            )
+        self.assertEqual(span5.status.status_code, StatusCode.ERROR)
+        self.assertEqual(span5.status.description, "desc")
+
     def test_ended_span(self):
         """Events, attributes are not allowed after span is ended"""