Repository: open-telemetry/opentelemetry-python-contrib

Issue:
[ASGI] UnboundLocalError local variable 'start' referenced before assignment
This error arises when an exception occurs in [this try block](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/dadcd01524449ddee07a8d8405890a60caeb8c8e/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py#L567) before the `start = default_timer()` line.

In such case the `start` variable is undefined. Unfortunately, [the `finally` block tries to compute the duration using this variable](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/dadcd01524449ddee07a8d8405890a60caeb8c8e/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py#L602).

Bug instroduced with https://github.com/open-telemetry/opentelemetry-python-contrib/commit/cbf005be6fb17f35b4cbaa4e76ac30bb64b3a258


related issue : #1478




Discussion:

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
@@ -538,6 +538,7 @@ async def __call__(self, scope, receive, send):
             receive: An awaitable callable yielding dictionaries
             send: An awaitable callable taking a single dictionary as argument.
         """
+        start = default_timer()
         if scope["type"] not in ("http", "websocket"):
             return await self.app(scope, receive, send)
 
@@ -591,7 +592,6 @@ async def __call__(self, scope, receive, send):
                     send,
                     duration_attrs,
                 )
-                start = default_timer()
 
                 await self.app(scope, otel_receive, otel_send)
         finally:


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py
--- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py
+++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py
@@ -14,6 +14,7 @@
 
 # pylint: disable=too-many-lines
 
+import asyncio
 import sys
 import unittest
 from timeit import default_timer
@@ -796,5 +797,38 @@ async def wrapped_app(scope, receive, send):
         )
 
 
+class TestAsgiApplicationRaisingError(AsgiTestBase):
+    def tearDown(self):
+        pass
+
+    @mock.patch(
+        "opentelemetry.instrumentation.asgi.collect_custom_request_headers_attributes",
+        side_effect=ValueError("whatever"),
+    )
+    def test_asgi_issue_1883(
+        self, mock_collect_custom_request_headers_attributes
+    ):
+        """
+        Test that exception UnboundLocalError local variable 'start' referenced before assignment is not raised
+        See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1883
+        """
+        app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
+        self.seed_app(app)
+        self.send_default_request()
+        try:
+            asyncio.get_event_loop().run_until_complete(
+                self.communicator.stop()
+            )
+        except ValueError as exc_info:
+            self.assertEqual(exc_info.args[0], "whatever")
+        except Exception as exc_info:  # pylint: disable=W0703
+            self.fail(
+                "expecting ValueError('whatever'), received instead: "
+                + str(exc_info)
+            )
+        else:
+            self.fail("expecting ValueError('whatever')")
+
+
 if __name__ == "__main__":
     unittest.main()