Repository: open-telemetry/opentelemetry-python

Issue:
`Span.record_exception` doesn't use the `exception` parameter to format the stacktrace
This line:

https://github.com/open-telemetry/opentelemetry-python/blob/8ad10f7d8fd59be49f3d2e683824ff921c0781f8/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py#L991

formats a stacktrace for the currently handled exception, which may be different from the exception passed to the method.

Here's an example of how this can go wrong:

```python
from opentelemetry.sdk.trace import TracerProvider

span = TracerProvider().get_tracer(__name__).start_span('')

try:
    raise ValueError('bad')
except ValueError as e:
    # This happens to work correctly because `e` is the currently handled exception.
    span.record_exception(e)
    print(span.events[0].attributes['exception.stacktrace'])
    """
    Traceback (most recent call last):
      File "...", line ..., in <module>
        raise ValueError('bad')
    ValueError: bad
    """

    exc = e

# This doesn't work because there is no longer an exception being currently handled.
span.record_exception(exc)
print(span.events[1].attributes['exception.stacktrace'])
"""
NoneType: None
"""

try:
    raise TypeError('worse')
except TypeError:
    # Recording an old exception means that the type and message don't match the stacktrace.
    span.record_exception(exc)
    print(span.events[2].attributes['exception.type'])
    """
    ValueError
    """
    print(span.events[2].attributes['exception.stacktrace'])
    """
    Traceback (most recent call last):
      File "...", line ..., in <module>
        raise TypeError('worse')
    TypeError: worse
    """
```

Replacing the faulty line with this works correctly:

```python
stacktrace = ''.join(traceback.format_exception(type(exception), exception, exception.__traceback__))
```


Discussion:
Care to open a PR?

Resolving patch:
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
@@ -988,13 +988,12 @@ def record_exception(
         escaped: bool = False,
     ) -> None:
         """Records an exception as a span event."""
-        try:
-            stacktrace = traceback.format_exc()
-        except Exception:  # pylint: disable=broad-except
-            # workaround for python 3.4, format_exc can raise
-            # an AttributeError if the __context__ on
-            # an exception is None
-            stacktrace = "Exception occurred on stacktrace formatting"
+        # TODO: keep only exception as first argument after baseline is 3.10
+        stacktrace = "".join(
+            traceback.format_exception(
+                type(exception), value=exception, tb=exception.__traceback__
+            )
+        )
         _attributes = {
             "exception.type": exception.__class__.__name__,
             "exception.message": str(exception),


Test patch:
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
@@ -1292,6 +1292,23 @@ def test_record_exception_context_manager(self):
         finally:
             self.assertEqual(len(span.events), 0)
 
+    def test_record_exception_out_of_scope(self):
+        span = trace._Span("name", mock.Mock(spec=trace_api.SpanContext))
+        out_of_scope_exception = ValueError("invalid")
+        span.record_exception(out_of_scope_exception)
+        exception_event = span.events[0]
+        self.assertEqual("exception", exception_event.name)
+        self.assertEqual(
+            "invalid", exception_event.attributes["exception.message"]
+        )
+        self.assertEqual(
+            "ValueError", exception_event.attributes["exception.type"]
+        )
+        self.assertIn(
+            "ValueError: invalid",
+            exception_event.attributes["exception.stacktrace"],
+        )
+
 
 def span_event_start_fmt(span_processor_name, span_name):
     return span_processor_name + ":" + span_name + ":start"