Repository: open-telemetry/opentelemetry-python-contrib

Issue:
AWS Lambda/APIGW unhandled exception causes UnboundLocalError
**Describe your environment** 
AWS Lambda, python3.12, using [opentelemetry-lambda layer-python 0.5](https://github.com/open-telemetry/opentelemetry-lambda/releases/tag/layer-python%2F0.5.0) which includes opentelemetry-python 1.23.0 and opentelemetry-python-contrib 0.44b0

**Steps to reproduce**
A lambda function invoked with APIGW, with auto-instrumentation, after an unhandled exception causes:
```
[ERROR] UnboundLocalError: cannot access local variable 'result' where it is not associated with a value
Traceback (most recent call last):
  File "/opt/python/wrapt/wrappers.py", line 598, in __call__
    return self._self_wrapper(self.__wrapped__, self._self_instance,
  File "/opt/python/opentelemetry/instrumentation/aws_lambda/__init__.py", line 378, in _instrumented_lambda_handler_call
    if isinstance(result, dict) and result.get("statusCode"):
    ^^^^^^
```

This can be reproduced with this code:
```python
import json
import random
import logging

from opentelemetry import trace

tracer = trace.get_tracer_provider().get_tracer(__name__)
logger = logging.getLogger()


def lambda_handler(event, context):
    if random.random() < 0.1:  # 10% chance to raise an error
        raise Exception('Injected error')
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world",
        }),
    }
```


**What is the expected behavior?**
The original exception is propagated (and not the `UnboundLocalError`)

**What is the actual behavior?**
The original exception is not propagated but this exception is raised instead:
```
[ERROR] UnboundLocalError: cannot access local variable 'result' where it is not associated with a value
Traceback (most recent call last):
  File "/opt/python/wrapt/wrappers.py", line 598, in __call__
    return self._self_wrapper(self.__wrapped__, self._self_instance,
  File "/opt/python/opentelemetry/instrumentation/aws_lambda/__init__.py", line 378, in _instrumented_lambda_handler_call
    if isinstance(result, dict) and result.get("statusCode"):
    ^^^^^^
```

**Additional context**
It seems that it would be enough to declare `result` at the beginning of this [block of code](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/fdcbbddb6c753e5e9d494ba399a5b4bcab4afc3f/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py#L367-L392):

```python
    result = None  # Assign a default value to result
    exception = None
            try:
                result = call_wrapped(*args, **kwargs)
            except Exception as exc:  # pylint: disable=W0703
                exception = exc
                span.set_status(Status(StatusCode.ERROR))
                span.record_exception(exception)

            if isinstance(lambda_event, dict) and lambda_event.get(
                "requestContext"
            ):
                span.set_attribute(SpanAttributes.FAAS_TRIGGER, "http")

                if lambda_event.get("version") == "2.0":
                    _set_api_gateway_v2_proxy_attributes(lambda_event, span)
                else:
                    _set_api_gateway_v1_proxy_attributes(lambda_event, span)

                if isinstance(result, dict) and result.get("statusCode"):
                    span.set_attribute(
                        SpanAttributes.HTTP_STATUS_CODE,
                        result.get("statusCode"),
                    )
```



Discussion:

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py
@@ -365,6 +365,7 @@ def _instrumented_lambda_handler_call(  # noqa pylint: disable=too-many-branches
                 )
 
             exception = None
+            result = None
             try:
                 result = call_wrapped(*args, **kwargs)
             except Exception as exc:  # pylint: disable=W0703


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py
--- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py
+++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py
@@ -436,6 +436,31 @@ def test_lambda_handles_handler_exception(self):
 
         exc_env_patch.stop()
 
+    def test_lambda_handles_handler_exception_with_api_gateway_proxy_event(
+        self,
+    ):
+        exc_env_patch = mock.patch.dict(
+            "os.environ",
+            {_HANDLER: "tests.mocks.lambda_function.handler_exc"},
+        )
+        exc_env_patch.start()
+        AwsLambdaInstrumentor().instrument()
+        # instrumentor re-raises the exception
+        with self.assertRaises(Exception):
+            mock_execute_lambda(
+                {"requestContext": {"http": {"method": "GET"}}}
+            )
+
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+        span = spans[0]
+        self.assertEqual(span.status.status_code, StatusCode.ERROR)
+        self.assertEqual(len(span.events), 1)
+        event = span.events[0]
+        self.assertEqual(event.name, "exception")
+
+        exc_env_patch.stop()
+
     def test_uninstrument(self):
         AwsLambdaInstrumentor().instrument()