Skip to content

Commit 4e7236d

Browse files
committed
feat: capture_response as metadata option #127
1 parent 330c76a commit 4e7236d

File tree

2 files changed

+107
-26
lines changed

2 files changed

+107
-26
lines changed

aws_lambda_powertools/tracing/tracer.py

+73-26
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,19 @@ def patch(self, modules: Tuple[str] = None):
226226
else:
227227
aws_xray_sdk.core.patch(modules)
228228

229-
def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = None):
229+
def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = None, capture_response: bool = True):
230230
"""Decorator to create subsegment for lambda handlers
231231
232232
As Lambda follows (event, context) signature we can remove some of the boilerplate
233233
and also capture any exception any Lambda function throws or its response as metadata
234234
235+
Parameters
236+
----------
237+
lambda_handler : Callable
238+
Method to annotate on
239+
capture_response : bool, optional
240+
Instructs tracer to not include handler's response as metadata, by default True
241+
235242
Example
236243
-------
237244
**Lambda function using capture_lambda_handler decorator**
@@ -241,16 +248,24 @@ def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = No
241248
def handler(event, context):
242249
...
243250
244-
Parameters
245-
----------
246-
method : Callable
247-
Method to annotate on
251+
**Preventing Tracer to log response as metadata**
252+
253+
tracer = Tracer(service="payment")
254+
@tracer.capture_lambda_handler(capture_response=False)
255+
def handler(event, context):
256+
...
248257
249258
Raises
250259
------
251260
err
252261
Exception raised by method
253262
"""
263+
# If handler is None we've been called with parameters
264+
# Return a partial function with args filled
265+
if lambda_handler is None:
266+
logger.debug("Decorator called with parameters")
267+
return functools.partial(self.capture_lambda_handler, capture_response=capture_response)
268+
254269
lambda_handler_name = lambda_handler.__name__
255270

256271
@functools.wraps(lambda_handler)
@@ -268,7 +283,10 @@ def decorate(event, context):
268283
logger.debug("Received lambda handler response successfully")
269284
logger.debug(response)
270285
self._add_response_as_metadata(
271-
function_name=lambda_handler_name, data=response, subsegment=subsegment
286+
function_name=lambda_handler_name,
287+
data=response,
288+
subsegment=subsegment,
289+
capture_response=capture_response,
272290
)
273291
except Exception as err:
274292
logger.exception(f"Exception received from {lambda_handler_name}")
@@ -281,7 +299,7 @@ def decorate(event, context):
281299

282300
return decorate
283301

284-
def capture_method(self, method: Callable = None):
302+
def capture_method(self, method: Callable = None, capture_response: bool = True):
285303
"""Decorator to create subsegment for arbitrary functions
286304
287305
It also captures both response and exceptions as metadata
@@ -295,6 +313,13 @@ def capture_method(self, method: Callable = None):
295313
`async.gather` is called, or use `in_subsegment_async`
296314
context manager via our escape hatch mechanism - See examples.
297315
316+
Parameters
317+
----------
318+
method : Callable
319+
Method to annotate on
320+
capture_response : bool, optional
321+
Instructs tracer to not include method's response as metadata, by default True
322+
298323
Example
299324
-------
300325
**Custom function using capture_method decorator**
@@ -416,29 +441,31 @@ async def async_tasks():
416441
417442
return { "task": "done", **ret }
418443
419-
Parameters
420-
----------
421-
method : Callable
422-
Method to annotate on
423-
424444
Raises
425445
------
426446
err
427447
Exception raised by method
428448
"""
449+
# If method is None we've been called with parameters
450+
# Return a partial function with args filled
451+
if method is None:
452+
logger.debug("Decorator called with parameters")
453+
return functools.partial(self.capture_method, capture_response=capture_response)
429454

430455
if inspect.iscoroutinefunction(method):
431-
decorate = self._decorate_async_function(method=method)
456+
decorate = self._decorate_async_function(method=method, capture_response=capture_response)
432457
elif inspect.isgeneratorfunction(method):
433-
decorate = self._decorate_generator_function(method=method)
458+
decorate = self._decorate_generator_function(method=method, capture_response=capture_response)
434459
elif hasattr(method, "__wrapped__") and inspect.isgeneratorfunction(method.__wrapped__):
435-
decorate = self._decorate_generator_function_with_context_manager(method=method)
460+
decorate = self._decorate_generator_function_with_context_manager(
461+
method=method, capture_response=capture_response
462+
)
436463
else:
437-
decorate = self._decorate_sync_function(method=method)
464+
decorate = self._decorate_sync_function(method=method, capture_response=capture_response)
438465

439466
return decorate
440467

441-
def _decorate_async_function(self, method: Callable = None):
468+
def _decorate_async_function(self, method: Callable = None, capture_response: bool = True):
442469
method_name = f"{method.__name__}"
443470

444471
@functools.wraps(method)
@@ -447,7 +474,12 @@ async def decorate(*args, **kwargs):
447474
try:
448475
logger.debug(f"Calling method: {method_name}")
449476
response = await method(*args, **kwargs)
450-
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
477+
self._add_response_as_metadata(
478+
function_name=method_name,
479+
data=response,
480+
subsegment=subsegment,
481+
capture_response=capture_response,
482+
)
451483
except Exception as err:
452484
logger.exception(f"Exception received from '{method_name}' method")
453485
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
@@ -457,7 +489,7 @@ async def decorate(*args, **kwargs):
457489

458490
return decorate
459491

460-
def _decorate_generator_function(self, method: Callable = None):
492+
def _decorate_generator_function(self, method: Callable = None, capture_response: bool = True):
461493
method_name = f"{method.__name__}"
462494

463495
@functools.wraps(method)
@@ -466,7 +498,9 @@ def decorate(*args, **kwargs):
466498
try:
467499
logger.debug(f"Calling method: {method_name}")
468500
result = yield from method(*args, **kwargs)
469-
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
501+
self._add_response_as_metadata(
502+
function_name=method_name, data=result, subsegment=subsegment, capture_response=capture_response
503+
)
470504
except Exception as err:
471505
logger.exception(f"Exception received from '{method_name}' method")
472506
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
@@ -476,7 +510,7 @@ def decorate(*args, **kwargs):
476510

477511
return decorate
478512

479-
def _decorate_generator_function_with_context_manager(self, method: Callable = None):
513+
def _decorate_generator_function_with_context_manager(self, method: Callable = None, capture_response: bool = True):
480514
method_name = f"{method.__name__}"
481515

482516
@functools.wraps(method)
@@ -488,15 +522,17 @@ def decorate(*args, **kwargs):
488522
with method(*args, **kwargs) as return_val:
489523
result = return_val
490524
yield result
491-
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
525+
self._add_response_as_metadata(
526+
function_name=method_name, data=result, subsegment=subsegment, capture_response=capture_response
527+
)
492528
except Exception as err:
493529
logger.exception(f"Exception received from '{method_name}' method")
494530
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
495531
raise
496532

497533
return decorate
498534

499-
def _decorate_sync_function(self, method: Callable = None):
535+
def _decorate_sync_function(self, method: Callable = None, capture_response: bool = True):
500536
method_name = f"{method.__name__}"
501537

502538
@functools.wraps(method)
@@ -505,7 +541,12 @@ def decorate(*args, **kwargs):
505541
try:
506542
logger.debug(f"Calling method: {method_name}")
507543
response = method(*args, **kwargs)
508-
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
544+
self._add_response_as_metadata(
545+
function_name=method_name,
546+
data=response,
547+
subsegment=subsegment,
548+
capture_response=capture_response,
549+
)
509550
except Exception as err:
510551
logger.exception(f"Exception received from '{method_name}' method")
511552
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
@@ -516,7 +557,11 @@ def decorate(*args, **kwargs):
516557
return decorate
517558

518559
def _add_response_as_metadata(
519-
self, function_name: str = None, data: Any = None, subsegment: aws_xray_sdk.core.models.subsegment = None
560+
self,
561+
function_name: str = None,
562+
data: Any = None,
563+
subsegment: aws_xray_sdk.core.models.subsegment = None,
564+
capture_response: bool = True,
520565
):
521566
"""Add response as metadata for given subsegment
522567
@@ -528,8 +573,10 @@ def _add_response_as_metadata(
528573
data to add as subsegment metadata, by default None
529574
subsegment : aws_xray_sdk.core.models.subsegment, optional
530575
existing subsegment to add metadata on, by default None
576+
capture_response : bool, optional
577+
Do not include response as metadata, by default True
531578
"""
532-
if data is None or subsegment is None:
579+
if data is None or not capture_response or subsegment is None:
533580
return
534581

535582
subsegment.put_metadata(key=f"{function_name} response", value=data, namespace=self._config["service"])

tests/unit/test_tracing.py

+34
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,37 @@ def generator_fn():
502502
assert put_metadata_mock_args["namespace"] == "booking"
503503
assert isinstance(put_metadata_mock_args["value"], ValueError)
504504
assert str(put_metadata_mock_args["value"]) == "test"
505+
506+
507+
def test_tracer_lambda_handler_does_not_add_response_as_metadata(mocker, provider_stub, in_subsegment_mock):
508+
# GIVEN tracer is initialized
509+
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
510+
tracer = Tracer(provider=provider, auto_patch=False)
511+
512+
# WHEN capture_lambda_handler decorator is used
513+
# and the handler response is empty
514+
@tracer.capture_lambda_handler(capture_response=False)
515+
def handler(event, context):
516+
return "response"
517+
518+
handler({}, mocker.MagicMock())
519+
520+
# THEN we should not add any metadata
521+
assert in_subsegment_mock.put_metadata.call_count == 0
522+
523+
524+
def test_tracer_method_does_not_add_response_as_metadata(mocker, provider_stub, in_subsegment_mock):
525+
# GIVEN tracer is initialized
526+
provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment)
527+
tracer = Tracer(provider=provider, auto_patch=False)
528+
529+
# WHEN capture_method decorator is used
530+
# and the method response is empty
531+
@tracer.capture_method(capture_response=False)
532+
def greeting(name, message):
533+
return "response"
534+
535+
greeting(name="Foo", message="Bar")
536+
537+
# THEN we should not add any metadata
538+
assert in_subsegment_mock.put_metadata.call_count == 0

0 commit comments

Comments
 (0)