diff --git a/CHANGELOG.md b/CHANGELOG.md index 5981ce910a4..ce5fba748b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.1] - 2020-08-22 +### Fixed +- **Tracer**: capture_method decorator did not properly handle nested context managers + ## [1.3.0] - 2020-08-21 ### Added - **Utilities**: Add new `parameters` utility to retrieve a single or multiple parameters from SSM Parameter Store, Secrets Manager, DynamoDB, or your very own diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index 0ce55e60837..4c12be3fc26 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -487,14 +487,13 @@ def decorate(*args, **kwargs): logger.debug(f"Calling method: {method_name}") with method(*args, **kwargs) as return_val: result = return_val + yield result self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment) except Exception as err: logger.exception(f"Exception received from '{method_name}' method") self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment) raise - yield result - return decorate def _decorate_sync_function(self, method: Callable = None): diff --git a/docs/package-lock.json b/docs/package-lock.json index 9d95c713582..4ca6ae53d77 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -7211,9 +7211,9 @@ "integrity": "sha512-2fvco0F2bBIgqzO8GRP0Jt/91pdrf9KfZ5FsmkYkjERmIJG585cFeFZV4+CO6oTmU3HmCTgfcZuEa7kW8VUh3A==" }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -15976,9 +15976,9 @@ } }, "prismjs": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz", - "integrity": "sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz", + "integrity": "sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==", "requires": { "clipboard": "^2.0.0" } diff --git a/pyproject.toml b/pyproject.toml index 75c74fb9bed..0cfd9c45bed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "1.3.0" +version = "1.3.1" description = "Python utilities for AWS Lambda functions including but not limited to tracing, logging and custom metric" authors = ["Amazon Web Services"] classifiers=[ diff --git a/tests/unit/test_tracing.py b/tests/unit/test_tracing.py index 8f7d9a646dd..16c476ee0fc 100644 --- a/tests/unit/test_tracing.py +++ b/tests/unit/test_tracing.py @@ -383,6 +383,72 @@ def handler(event, context): assert "test result" in result +def test_tracer_yield_from_context_manager_exception_metadata(mocker, provider_stub, in_subsegment_mock): + # GIVEN tracer is initialized + provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) + tracer = Tracer(provider=provider, service="booking") + + # WHEN capture_method decorator is used on a context manager + # and the method raises an exception + @tracer.capture_method + @contextlib.contextmanager + def yield_with_capture(): + yield "partial" + raise ValueError("test") + + with pytest.raises(ValueError): + with yield_with_capture() as partial_val: + assert partial_val == "partial" + + # THEN we should add the exception using method name as key plus error + # and their service name as the namespace + put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1] + assert put_metadata_mock_args["key"] == "yield_with_capture error" + assert isinstance(put_metadata_mock_args["value"], ValueError) + assert put_metadata_mock_args["namespace"] == "booking" + + +def test_tracer_yield_from_nested_context_manager(mocker, provider_stub, in_subsegment_mock): + # GIVEN tracer is initialized + provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) + tracer = Tracer(provider=provider, service="booking") + + # WHEN capture_method decorator is used on a context manager nesting another context manager + class NestedContextManager(object): + def __enter__(self): + self._value = {"result": "test result"} + return self._value + + def __exit__(self, exc_type, exc_val, exc_tb): + self._value["result"] = "exit was called before yielding" + + @tracer.capture_method + @contextlib.contextmanager + def yield_with_capture(): + with NestedContextManager() as nested_context: + yield nested_context + + @tracer.capture_lambda_handler + def handler(event, context): + response = [] + with yield_with_capture() as yielded_value: + response.append(yielded_value["result"]) + + return response + + result = handler({}, {}) + + # THEN we should have a subsegment named after the method name + # and add its response as trace metadata + handler_trace, yield_function_trace = in_subsegment_mock.in_subsegment.call_args_list + + assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"] + assert in_subsegment_mock.in_subsegment.call_count == 2 + assert handler_trace == mocker.call(name="## handler") + assert yield_function_trace == mocker.call(name="## yield_with_capture") + assert "test result" in result + + def test_tracer_yield_from_generator(mocker, provider_stub, in_subsegment_mock): # GIVEN tracer is initialized provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) @@ -411,3 +477,28 @@ def handler(event, context): assert handler_trace == mocker.call(name="## handler") assert generator_fn_trace == mocker.call(name="## generator_fn") assert "test result" in result + + +def test_tracer_yield_from_generator_exception_metadata(mocker, provider_stub, in_subsegment_mock): + # GIVEN tracer is initialized + provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) + tracer = Tracer(provider=provider, service="booking") + + # WHEN capture_method decorator is used on a generator function + # and the method raises an exception + @tracer.capture_method + def generator_fn(): + yield "partial" + raise ValueError("test") + + with pytest.raises(ValueError): + gen = generator_fn() + list(gen) + + # THEN we should add the exception using method name as key plus error + # and their service name as the namespace + put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1] + assert put_metadata_mock_args["key"] == "generator_fn error" + assert put_metadata_mock_args["namespace"] == "booking" + assert isinstance(put_metadata_mock_args["value"], ValueError) + assert str(put_metadata_mock_args["value"]) == "test"