diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a31f1a2d5..421d28888e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2136)) - `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call ([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178)) +- AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245)) ## Version 1.22.0/0.43b0 (2023-12-14) 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 index 391bc32f60..130622f8c8 100644 --- 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 @@ -97,6 +97,7 @@ def custom_event_context_extractor(lambda_event): get_tracer_provider, ) from opentelemetry.trace.propagation import get_current_span +from opentelemetry.trace.status import Status, StatusCode logger = logging.getLogger(__name__) @@ -278,6 +279,7 @@ def _set_api_gateway_v2_proxy_attributes( return span +# pylint: disable=too-many-statements def _instrument( wrapped_module_name, wrapped_function_name, @@ -287,6 +289,8 @@ def _instrument( disable_aws_context_propagation: bool = False, meter_provider: MeterProvider = None, ): + # pylint: disable=too-many-locals + # pylint: disable=too-many-statements def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches call_wrapped, instance, args, kwargs ): @@ -350,7 +354,13 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches lambda_context.aws_request_id, ) - result = call_wrapped(*args, **kwargs) + 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 the request came from an API Gateway, extract http attributes from the event # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#api-gateway @@ -398,6 +408,9 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches "MeterProvider was missing `force_flush` method. This is necessary in case of a Lambda freeze and would exist in the OTel SDK implementation." ) + if exception is not None: + raise exception.with_traceback(exception.__traceback__) + return result wrap_function_wrapper( diff --git a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py index 259375c481..a878d0f06a 100644 --- a/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py +++ b/instrumentation/opentelemetry-instrumentation-aws-lambda/tests/mocks/lambda_function.py @@ -19,3 +19,7 @@ def handler(event, context): def rest_api_handler(event, context): return {"statusCode": 200, "body": "200 ok"} + + +def handler_exc(event, context): + raise Exception("500 internal server error") 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 index 5e4aaf0312..2fa4aafee5 100644 --- 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 @@ -40,7 +40,7 @@ from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase -from opentelemetry.trace import NoOpTracerProvider, SpanKind +from opentelemetry.trace import NoOpTracerProvider, SpanKind, StatusCode from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) @@ -410,6 +410,27 @@ def test_lambda_handles_list_event(self): assert spans + def test_lambda_handles_handler_exception(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() + + 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()