Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Asyncio Instrumentation future cancelledError
**Describe your environment** Describe any aspect of your environment relevant to the problem, including your Python version, [platform](https://docs.python.org/3/library/platform.html), version numbers of installed dependencies, information about your cloud hosting provider, etc. If you're reporting a problem with a specific version of a library in this repo, please check whether the problem has been fixed on main.

**Steps to reproduce**
Describe exactly how to reproduce the error. Include a code sample if applicable.
Using python auto instrumentation version 0.44b0  the real trace source is being deprecated and we see only this:
`{"line":44,"module":"runners","timestamp":"2024-04-18 15:02:21.526124+00:00","message":"[asyncio] Exception in callback <function AsyncioInstrumentor.trace_future.<locals>.callback at 0x7fa8d2f47c70>\nhandle: <Handle AsyncioInstrumentor.trace_future.<locals>.callback>","commit":"-","service_name":"******","hostname":"***********","level":"ERROR","exception_type":"CancelledError","traceback":"File \"uvloop/cbhandles.pyx\", line 63, in uvloop.loop.Handle._run\n\nFile \"/otel-auto-instrumentation-python/opentelemetry/instrumentation/asyncio/__init__.py\", line 310, in callback\n    309  def callback(f):\n--> 310      exception = f.exception()\n    311      attr = {\n    ..................................................\n     f = <Future cancelled>\n    ..................................................\n\nCancelledError"}`

**What is the expected behavior?**
What did you expect to see?
SPAN collected with no errors

**What is the actual behavior?**
What did you see instead?
trace source does not run with an error log

**Additional context**
Add any other context about the problem here.
Calling the exception() method when future is in the cancelled state is causing a CancelledError, so I think we should check the cancelled state first and call f.exception() only if it's not cancelled.



Discussion:

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-asyncio/src/opentelemetry/instrumentation/asyncio/__init__.py
@@ -116,21 +116,11 @@ class AsyncioInstrumentor(BaseInstrumentor):
         "run_coroutine_threadsafe",
     ]
 
-    def __init__(self):
-        super().__init__()
-        self.process_duration_histogram = None
-        self.process_created_counter = None
-
-        self._tracer = None
-        self._meter = None
-        self._coros_name_to_trace: set = set()
-        self._to_thread_name_to_trace: set = set()
-        self._future_active_enabled: bool = False
-
     def instrumentation_dependencies(self) -> Collection[str]:
         return _instruments
 
     def _instrument(self, **kwargs):
+        # pylint: disable=attribute-defined-outside-init
         self._tracer = get_tracer(
             __name__, __version__, kwargs.get("tracer_provider")
         )
@@ -307,13 +297,17 @@ def trace_future(self, future):
         )
 
         def callback(f):
-            exception = f.exception()
             attr = {
                 "type": "future",
+                "state": (
+                    "cancelled"
+                    if f.cancelled()
+                    else determine_state(f.exception())
+                ),
             }
-            state = determine_state(exception)
-            attr["state"] = state
-            self.record_process(start, attr, span, exception)
+            self.record_process(
+                start, attr, span, None if f.cancelled() else f.exception()
+            )
 
         future.add_done_callback(callback)
         return future


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py
new file mode 100644
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-asyncio/tests/test_asyncio_future_cancellation.py
@@ -0,0 +1,60 @@
+import asyncio
+from unittest.mock import patch
+
+from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor
+from opentelemetry.instrumentation.asyncio.environment_variables import (
+    OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED,
+)
+from opentelemetry.test.test_base import TestBase
+from opentelemetry.trace import get_tracer
+
+
+class TestTraceFuture(TestBase):
+    @patch.dict(
+        "os.environ", {OTEL_PYTHON_ASYNCIO_FUTURE_TRACE_ENABLED: "true"}
+    )
+    def setUp(self):
+        super().setUp()
+        self._tracer = get_tracer(
+            __name__,
+        )
+        self.instrumentor = AsyncioInstrumentor()
+        self.instrumentor.instrument()
+
+    def tearDown(self):
+        super().tearDown()
+        self.instrumentor.uninstrument()
+
+    def test_trace_future_cancelled(self):
+        async def future_cancelled():
+            with self._tracer.start_as_current_span("root"):
+                future = asyncio.Future()
+                future = self.instrumentor.trace_future(future)
+                future.cancel()
+
+        try:
+            asyncio.run(future_cancelled())
+        except asyncio.CancelledError as exc:
+            self.assertEqual(isinstance(exc, asyncio.CancelledError), True)
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 2)
+        self.assertEqual(spans[0].name, "root")
+        self.assertEqual(spans[1].name, "asyncio future")
+
+        metrics = (
+            self.memory_metrics_reader.get_metrics_data()
+            .resource_metrics[0]
+            .scope_metrics[0]
+            .metrics
+        )
+        self.assertEqual(len(metrics), 2)
+
+        self.assertEqual(metrics[0].name, "asyncio.process.duration")
+        self.assertEqual(
+            metrics[0].data.data_points[0].attributes["state"], "cancelled"
+        )
+
+        self.assertEqual(metrics[1].name, "asyncio.process.created")
+        self.assertEqual(
+            metrics[1].data.data_points[0].attributes["state"], "cancelled"
+        )