Skip to content

Commit 14f6dfe

Browse files
fix(capture_method): should yield inside with (#124)
Changes: * capture_method should yield from within the "with" statement * Add missing test cases Closes #112
1 parent 6736af2 commit 14f6dfe

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

aws_lambda_powertools/tracing/tracer.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -487,14 +487,13 @@ def decorate(*args, **kwargs):
487487
logger.debug(f"Calling method: {method_name}")
488488
with method(*args, **kwargs) as return_val:
489489
result = return_val
490+
yield result
490491
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
491492
except Exception as err:
492493
logger.exception(f"Exception received from '{method_name}' method")
493494
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
494495
raise
495496

496-
yield result
497-
498497
return decorate
499498

500499
def _decorate_sync_function(self, method: Callable = None):

tests/unit/test_tracing.py

+91
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,72 @@ def handler(event, context):
383383
assert "test result" in result
384384

385385

386+
def test_tracer_yield_from_context_manager_exception_metadata(mocker, provider_stub, in_subsegment_mock):
387+
# GIVEN tracer is initialized
388+
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
389+
tracer = Tracer(provider=provider, service="booking")
390+
391+
# WHEN capture_method decorator is used on a context manager
392+
# and the method raises an exception
393+
@tracer.capture_method
394+
@contextlib.contextmanager
395+
def yield_with_capture():
396+
yield "partial"
397+
raise ValueError("test")
398+
399+
with pytest.raises(ValueError):
400+
with yield_with_capture() as partial_val:
401+
assert partial_val == "partial"
402+
403+
# THEN we should add the exception using method name as key plus error
404+
# and their service name as the namespace
405+
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
406+
assert put_metadata_mock_args["key"] == "yield_with_capture error"
407+
assert isinstance(put_metadata_mock_args["value"], ValueError)
408+
assert put_metadata_mock_args["namespace"] == "booking"
409+
410+
411+
def test_tracer_yield_from_nested_context_manager(mocker, provider_stub, in_subsegment_mock):
412+
# GIVEN tracer is initialized
413+
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
414+
tracer = Tracer(provider=provider, service="booking")
415+
416+
# WHEN capture_method decorator is used on a context manager nesting another context manager
417+
class NestedContextManager(object):
418+
def __enter__(self):
419+
self._value = {"result": "test result"}
420+
return self._value
421+
422+
def __exit__(self, exc_type, exc_val, exc_tb):
423+
self._value["result"] = "exit was called before yielding"
424+
425+
@tracer.capture_method
426+
@contextlib.contextmanager
427+
def yield_with_capture():
428+
with NestedContextManager() as nested_context:
429+
yield nested_context
430+
431+
@tracer.capture_lambda_handler
432+
def handler(event, context):
433+
response = []
434+
with yield_with_capture() as yielded_value:
435+
response.append(yielded_value["result"])
436+
437+
return response
438+
439+
result = handler({}, {})
440+
441+
# THEN we should have a subsegment named after the method name
442+
# and add its response as trace metadata
443+
handler_trace, yield_function_trace = in_subsegment_mock.in_subsegment.call_args_list
444+
445+
assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
446+
assert in_subsegment_mock.in_subsegment.call_count == 2
447+
assert handler_trace == mocker.call(name="## handler")
448+
assert yield_function_trace == mocker.call(name="## yield_with_capture")
449+
assert "test result" in result
450+
451+
386452
def test_tracer_yield_from_generator(mocker, provider_stub, in_subsegment_mock):
387453
# GIVEN tracer is initialized
388454
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
@@ -411,3 +477,28 @@ def handler(event, context):
411477
assert handler_trace == mocker.call(name="## handler")
412478
assert generator_fn_trace == mocker.call(name="## generator_fn")
413479
assert "test result" in result
480+
481+
482+
def test_tracer_yield_from_generator_exception_metadata(mocker, provider_stub, in_subsegment_mock):
483+
# GIVEN tracer is initialized
484+
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
485+
tracer = Tracer(provider=provider, service="booking")
486+
487+
# WHEN capture_method decorator is used on a generator function
488+
# and the method raises an exception
489+
@tracer.capture_method
490+
def generator_fn():
491+
yield "partial"
492+
raise ValueError("test")
493+
494+
with pytest.raises(ValueError):
495+
gen = generator_fn()
496+
list(gen)
497+
498+
# THEN we should add the exception using method name as key plus error
499+
# and their service name as the namespace
500+
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
501+
assert put_metadata_mock_args["key"] == "generator_fn error"
502+
assert put_metadata_mock_args["namespace"] == "booking"
503+
assert isinstance(put_metadata_mock_args["value"], ValueError)
504+
assert str(put_metadata_mock_args["value"]) == "test"

0 commit comments

Comments
 (0)