Skip to content

fix(capture_method): should yield inside with for generator method using a context manager #124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions aws_lambda_powertools/tracing/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
91 changes: 91 additions & 0 deletions tests/unit/test_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"