Skip to content

Commit cbc2e66

Browse files
author
Tom McCarthy
committed
feat: add support for tracing of generators using capture_method decorator
1 parent 6b66e0b commit cbc2e66

File tree

3 files changed

+138
-1
lines changed

3 files changed

+138
-1
lines changed

aws_lambda_powertools/tracing/tracer.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import copy
23
import functools
34
import inspect
@@ -280,7 +281,7 @@ def decorate(event, context):
280281

281282
return decorate
282283

283-
def capture_method(self, method: Callable = None):
284+
def capture_method(self, method: Callable = None): # noqa: C901
284285
"""Decorator to create subsegment for arbitrary functions
285286
286287
It also captures both response and exceptions as metadata
@@ -412,6 +413,44 @@ async def decorate(*args, **kwargs):
412413

413414
return response
414415

416+
elif inspect.isgeneratorfunction(method):
417+
418+
@functools.wraps(method)
419+
def decorate(*args, **kwargs):
420+
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
421+
try:
422+
logger.debug(f"Calling method: {method_name}")
423+
result = yield from method(*args, **kwargs)
424+
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
425+
except Exception as err:
426+
logger.exception(f"Exception received from '{method_name}' method")
427+
self._add_full_exception_as_metadata(
428+
function_name=method_name, error=err, subsegment=subsegment
429+
)
430+
raise
431+
432+
return result
433+
434+
elif hasattr(method, "__wrapped__") and inspect.isgeneratorfunction(method.__wrapped__):
435+
436+
@functools.wraps(method)
437+
@contextlib.contextmanager
438+
def decorate(*args, **kwargs):
439+
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
440+
try:
441+
logger.debug(f"Calling method: {method_name}")
442+
with method(*args, **kwargs) as return_val:
443+
result = return_val
444+
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
445+
except Exception as err:
446+
logger.exception(f"Exception received from '{method_name}' method")
447+
self._add_full_exception_as_metadata(
448+
function_name=method_name, error=err, subsegment=subsegment
449+
)
450+
raise
451+
452+
yield result
453+
415454
else:
416455

417456
@functools.wraps(method)

tests/functional/test_tracing.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import contextlib
2+
13
import pytest
24

35
from aws_lambda_powertools import Tracer
@@ -150,3 +152,36 @@ def sums_values():
150152
return func_1() + func_2()
151153

152154
sums_values()
155+
156+
157+
def test_tracer_yield_with_capture():
158+
# GIVEN tracer method decorator is used
159+
tracer = Tracer(disabled=True)
160+
161+
# WHEN capture_method decorator is applied to a context manager
162+
@tracer.capture_method
163+
@contextlib.contextmanager
164+
def yield_with_capture():
165+
yield "testresult"
166+
167+
# Or WHEN capture_method decorator is applied to a generator function
168+
@tracer.capture_method
169+
def generator_func():
170+
yield "testresult2"
171+
172+
@tracer.capture_lambda_handler
173+
def handler(event, context):
174+
result = []
175+
with yield_with_capture() as yielded_value:
176+
result.append(yielded_value)
177+
178+
gen = generator_func()
179+
180+
result.append(next(gen))
181+
182+
return result
183+
184+
# THEN no exception is thrown, and the functions properly return values
185+
result = handler({}, {})
186+
assert "testresult" in result
187+
assert "testresult2" in result

tests/unit/test_tracing.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import sys
23
from typing import NamedTuple
34
from unittest import mock
@@ -348,3 +349,65 @@ async def greeting(name, message):
348349
put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1]
349350
assert put_metadata_mock_args["key"] == "greeting error"
350351
assert put_metadata_mock_args["namespace"] == "booking"
352+
353+
354+
def test_tracer_yield_from_context_manager(mocker, provider_stub, in_subsegment_mock):
355+
# GIVEN tracer is initialized
356+
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
357+
tracer = Tracer(provider=provider, service="booking")
358+
359+
# WHEN capture_method decorator is used on a context manager
360+
@tracer.capture_method
361+
@contextlib.contextmanager
362+
def yield_with_capture():
363+
yield "test result"
364+
365+
@tracer.capture_lambda_handler
366+
def handler(event, context):
367+
response = []
368+
with yield_with_capture() as yielded_value:
369+
response.append(yielded_value)
370+
371+
return response
372+
373+
result = handler({}, {})
374+
375+
# THEN we should have a subsegment named after the method name
376+
# and add its response as trace metadata
377+
handler_trace, yield_function_trace = in_subsegment_mock.in_subsegment.call_args_list
378+
379+
assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
380+
assert in_subsegment_mock.in_subsegment.call_count == 2
381+
assert handler_trace == mocker.call(name="## handler")
382+
assert yield_function_trace == mocker.call(name="## yield_with_capture")
383+
assert "test result" in result
384+
385+
386+
def test_tracer_yield_from_generator(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 generator function
392+
@tracer.capture_method
393+
def generator_fn():
394+
yield "test result"
395+
396+
@tracer.capture_lambda_handler
397+
def handler(event, context):
398+
gen = generator_fn()
399+
response = list(gen)
400+
401+
return response
402+
403+
result = handler({}, {})
404+
405+
# THEN we should have a subsegment named after the method name
406+
# and add its response as trace metadata
407+
handler_trace, generator_fn_trace = in_subsegment_mock.in_subsegment.call_args_list
408+
409+
assert "test result" in in_subsegment_mock.put_metadata.call_args[1]["value"]
410+
assert in_subsegment_mock.in_subsegment.call_count == 2
411+
assert handler_trace == mocker.call(name="## handler")
412+
assert generator_fn_trace == mocker.call(name="## generator_fn")
413+
assert "test result" in result

0 commit comments

Comments
 (0)