Skip to content

Commit 3f74b03

Browse files
authored
Merge pull request #128 from heitorlessa/feat/tracer-disallow-response-metadata
feat: option to disallow tracer to capture response as metadata
2 parents ad00a92 + 91c05e6 commit 3f74b03

File tree

4 files changed

+167
-67
lines changed

4 files changed

+167
-67
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- This is a breaking change if you were graphing/alerting on both application metrics with the same name to compensate this previous malfunctioning
1212
- Marked as bugfix as this is the intended behaviour since the beginning, as you shouldn't have the same application metric with different dimensions
1313

14+
### Added
15+
- **Tracer**: capture_lambda_handler and capture_method decorators now support `capture_response` parameter to not include function's response as part of tracing metadata
16+
1417
## [1.3.1] - 2020-08-22
1518
### Fixed
1619
- **Tracer**: capture_method decorator did not properly handle nested context managers

Diff for: aws_lambda_powertools/tracing/tracer.py

+110-58
Original file line numberDiff line numberDiff line change
@@ -150,27 +150,27 @@ def __init__(
150150
self.auto_patch = self._config["auto_patch"]
151151

152152
if self.disabled:
153-
self.__disable_tracing_provider()
153+
self._disable_tracer_provider()
154154

155155
if self.auto_patch:
156156
self.patch(modules=patch_modules)
157157

158158
def put_annotation(self, key: str, value: Any):
159159
"""Adds annotation to existing segment or subsegment
160160
161+
Parameters
162+
----------
163+
key : str
164+
Annotation key
165+
value : any
166+
Value for annotation
167+
161168
Example
162169
-------
163170
Custom annotation for a pseudo service named payment
164171
165172
tracer = Tracer(service="payment")
166173
tracer.put_annotation("PaymentStatus", "CONFIRMED")
167-
168-
Parameters
169-
----------
170-
key : str
171-
Annotation key (e.g. PaymentStatus)
172-
value : any
173-
Value for annotation (e.g. "CONFIRMED")
174174
"""
175175
if self.disabled:
176176
logger.debug("Tracing has been disabled, aborting put_annotation")
@@ -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)
@@ -266,22 +281,24 @@ def decorate(event, context):
266281
logger.debug("Calling lambda handler")
267282
response = lambda_handler(event, context)
268283
logger.debug("Received lambda handler response successfully")
269-
logger.debug(response)
270284
self._add_response_as_metadata(
271-
function_name=lambda_handler_name, data=response, subsegment=subsegment
285+
method_name=lambda_handler_name,
286+
data=response,
287+
subsegment=subsegment,
288+
capture_response=capture_response,
272289
)
273290
except Exception as err:
274291
logger.exception(f"Exception received from {lambda_handler_name}")
275292
self._add_full_exception_as_metadata(
276-
function_name=lambda_handler_name, error=err, subsegment=subsegment
293+
method_name=lambda_handler_name, error=err, subsegment=subsegment
277294
)
278295
raise
279296

280297
return response
281298

282299
return decorate
283300

284-
def capture_method(self, method: Callable = None):
301+
def capture_method(self, method: Callable = None, capture_response: bool = True):
285302
"""Decorator to create subsegment for arbitrary functions
286303
287304
It also captures both response and exceptions as metadata
@@ -295,6 +312,13 @@ def capture_method(self, method: Callable = None):
295312
`async.gather` is called, or use `in_subsegment_async`
296313
context manager via our escape hatch mechanism - See examples.
297314
315+
Parameters
316+
----------
317+
method : Callable
318+
Method to annotate on
319+
capture_response : bool, optional
320+
Instructs tracer to not include method's response as metadata, by default True
321+
298322
Example
299323
-------
300324
**Custom function using capture_method decorator**
@@ -416,69 +440,84 @@ async def async_tasks():
416440
417441
return { "task": "done", **ret }
418442
419-
Parameters
420-
----------
421-
method : Callable
422-
Method to annotate on
423-
424443
Raises
425444
------
426445
err
427446
Exception raised by method
428447
"""
448+
# If method is None we've been called with parameters
449+
# Return a partial function with args filled
450+
if method is None:
451+
logger.debug("Decorator called with parameters")
452+
return functools.partial(self.capture_method, capture_response=capture_response)
453+
454+
method_name = f"{method.__name__}"
429455

430456
if inspect.iscoroutinefunction(method):
431-
decorate = self._decorate_async_function(method=method)
457+
decorate = self._decorate_async_function(
458+
method=method, capture_response=capture_response, method_name=method_name
459+
)
432460
elif inspect.isgeneratorfunction(method):
433-
decorate = self._decorate_generator_function(method=method)
461+
decorate = self._decorate_generator_function(
462+
method=method, capture_response=capture_response, method_name=method_name
463+
)
434464
elif hasattr(method, "__wrapped__") and inspect.isgeneratorfunction(method.__wrapped__):
435-
decorate = self._decorate_generator_function_with_context_manager(method=method)
465+
decorate = self._decorate_generator_function_with_context_manager(
466+
method=method, capture_response=capture_response, method_name=method_name
467+
)
436468
else:
437-
decorate = self._decorate_sync_function(method=method)
469+
decorate = self._decorate_sync_function(
470+
method=method, capture_response=capture_response, method_name=method_name
471+
)
438472

439473
return decorate
440474

441-
def _decorate_async_function(self, method: Callable = None):
442-
method_name = f"{method.__name__}"
443-
475+
def _decorate_async_function(self, method: Callable = None, capture_response: bool = True, method_name: str = None):
444476
@functools.wraps(method)
445477
async def decorate(*args, **kwargs):
446478
async with self.provider.in_subsegment_async(name=f"## {method_name}") as subsegment:
447479
try:
448480
logger.debug(f"Calling method: {method_name}")
449481
response = await method(*args, **kwargs)
450-
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
482+
self._add_response_as_metadata(
483+
method_name=method_name,
484+
data=response,
485+
subsegment=subsegment,
486+
capture_response=capture_response,
487+
)
451488
except Exception as err:
452489
logger.exception(f"Exception received from '{method_name}' method")
453-
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
490+
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
454491
raise
455492

456493
return response
457494

458495
return decorate
459496

460-
def _decorate_generator_function(self, method: Callable = None):
461-
method_name = f"{method.__name__}"
462-
497+
def _decorate_generator_function(
498+
self, method: Callable = None, capture_response: bool = True, method_name: str = None
499+
):
463500
@functools.wraps(method)
464501
def decorate(*args, **kwargs):
465502
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
466503
try:
467504
logger.debug(f"Calling method: {method_name}")
468505
result = yield from method(*args, **kwargs)
469-
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
506+
self._add_response_as_metadata(
507+
method_name=method_name, data=result, subsegment=subsegment, capture_response=capture_response
508+
)
470509
except Exception as err:
471510
logger.exception(f"Exception received from '{method_name}' method")
472-
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
511+
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
473512
raise
474513

475514
return result
476515

477516
return decorate
478517

479-
def _decorate_generator_function_with_context_manager(self, method: Callable = None):
480-
method_name = f"{method.__name__}"
481-
518+
def _decorate_generator_function_with_context_manager(
519+
self, method: Callable = None, capture_response: bool = True, method_name: str = None
520+
):
482521
@functools.wraps(method)
483522
@contextlib.contextmanager
484523
def decorate(*args, **kwargs):
@@ -488,74 +527,87 @@ def decorate(*args, **kwargs):
488527
with method(*args, **kwargs) as return_val:
489528
result = return_val
490529
yield result
491-
self._add_response_as_metadata(function_name=method_name, data=result, subsegment=subsegment)
530+
self._add_response_as_metadata(
531+
method_name=method_name, data=result, subsegment=subsegment, capture_response=capture_response
532+
)
492533
except Exception as err:
493534
logger.exception(f"Exception received from '{method_name}' method")
494-
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
535+
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
495536
raise
496537

497538
return decorate
498539

499-
def _decorate_sync_function(self, method: Callable = None):
500-
method_name = f"{method.__name__}"
501-
540+
def _decorate_sync_function(self, method: Callable = None, capture_response: bool = True, method_name: str = None):
502541
@functools.wraps(method)
503542
def decorate(*args, **kwargs):
504543
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
505544
try:
506545
logger.debug(f"Calling method: {method_name}")
507546
response = method(*args, **kwargs)
508-
self._add_response_as_metadata(function_name=method_name, data=response, subsegment=subsegment)
547+
self._add_response_as_metadata(
548+
method_name=method_name,
549+
data=response,
550+
subsegment=subsegment,
551+
capture_response=capture_response,
552+
)
509553
except Exception as err:
510554
logger.exception(f"Exception received from '{method_name}' method")
511-
self._add_full_exception_as_metadata(function_name=method_name, error=err, subsegment=subsegment)
555+
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
512556
raise
513557

514558
return response
515559

516560
return decorate
517561

518562
def _add_response_as_metadata(
519-
self, function_name: str = None, data: Any = None, subsegment: aws_xray_sdk.core.models.subsegment = None
563+
self,
564+
method_name: str = None,
565+
data: Any = None,
566+
subsegment: aws_xray_sdk.core.models.subsegment = None,
567+
capture_response: bool = True,
520568
):
521569
"""Add response as metadata for given subsegment
522570
523571
Parameters
524572
----------
525-
function_name : str, optional
526-
function name to add as metadata key, by default None
573+
method_name : str, optional
574+
method name to add as metadata key, by default None
527575
data : Any, optional
528576
data to add as subsegment metadata, by default None
529577
subsegment : aws_xray_sdk.core.models.subsegment, optional
530578
existing subsegment to add metadata on, by default None
579+
capture_response : bool, optional
580+
Do not include response as metadata, by default True
531581
"""
532-
if data is None or subsegment is None:
582+
if data is None or not capture_response or subsegment is None:
533583
return
534584

535-
subsegment.put_metadata(key=f"{function_name} response", value=data, namespace=self._config["service"])
585+
subsegment.put_metadata(key=f"{method_name} response", value=data, namespace=self._config["service"])
536586

537587
def _add_full_exception_as_metadata(
538-
self, function_name: str = None, error: Exception = None, subsegment: aws_xray_sdk.core.models.subsegment = None
588+
self, method_name: str = None, error: Exception = None, subsegment: aws_xray_sdk.core.models.subsegment = None
539589
):
540590
"""Add full exception object as metadata for given subsegment
541591
542592
Parameters
543593
----------
544-
function_name : str, optional
545-
function name to add as metadata key, by default None
594+
method_name : str, optional
595+
method name to add as metadata key, by default None
546596
error : Exception, optional
547597
error to add as subsegment metadata, by default None
548598
subsegment : aws_xray_sdk.core.models.subsegment, optional
549599
existing subsegment to add metadata on, by default None
550600
"""
551-
subsegment.put_metadata(key=f"{function_name} error", value=error, namespace=self._config["service"])
601+
subsegment.put_metadata(key=f"{method_name} error", value=error, namespace=self._config["service"])
552602

553-
def __disable_tracing_provider(self):
603+
@staticmethod
604+
def _disable_tracer_provider():
554605
"""Forcefully disables tracing"""
555606
logger.debug("Disabling tracer provider...")
556607
aws_xray_sdk.global_sdk_config.set_sdk_enabled(False)
557608

558-
def __is_trace_disabled(self) -> bool:
609+
@staticmethod
610+
def _is_tracer_disabled() -> bool:
559611
"""Detects whether trace has been disabled
560612
561613
Tracing is automatically disabled in the following conditions:
@@ -592,7 +644,7 @@ def __build_config(
592644
provider: aws_xray_sdk.core.xray_recorder = None,
593645
):
594646
""" Populates Tracer config for new and existing initializations """
595-
is_disabled = disabled if disabled is not None else self.__is_trace_disabled()
647+
is_disabled = disabled if disabled is not None else self._is_tracer_disabled()
596648
is_service = service if service is not None else os.getenv("POWERTOOLS_SERVICE_NAME")
597649

598650
self._config["provider"] = provider if provider is not None else self._config["provider"]

Diff for: docs/content/core/tracer.mdx

+14-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ Tracer is an opinionated thin wrapper for [AWS X-Ray Python SDK](https://github.
1717
* Support tracing async methods, generators, and context managers
1818
* Auto patch supported modules, or a tuple of explicit modules supported by AWS X-Ray
1919

20+
<Note type="warning">
21+
<strong>Returning sensitive information from your Lambda handler or functions, where Tracer is used?</strong>
22+
<br/><br/>
23+
You can disable Tracer from capturing their responses as tracing metadata with <strong><code>capture_response=False</code></strong> parameter in both capture_lambda_handler and capture_method decorators.
24+
</Note><br/>
25+
2026
## Initialization
2127

2228
Your AWS Lambda function must have permission to send traces to AWS X-Ray - Here is an example using AWS Serverless Application Model (SAM)
@@ -63,6 +69,10 @@ def handler(event, context):
6369
charge_id = event.get('charge_id')
6470
payment = collect_payment(charge_id)
6571
...
72+
73+
@tracer.capture_lambda_handler(capture_response=False) # highlight-line
74+
def handler(event, context):
75+
return "sensitive_information"
6676
```
6777

6878
### Annotations
@@ -108,7 +118,10 @@ def collect_payment(charge_id):
108118
ret = requests.post(PAYMENT_ENDPOINT) # logic
109119
tracer.put_annotation("PAYMENT_STATUS", "SUCCESS") # custom annotation
110120
return ret
111-
...
121+
122+
@tracer.capture_method(capture_response=False) # highlight-line
123+
def sensitive_information_to_be_processed():
124+
return "sensitive_information"
112125
```
113126

114127
## Asynchronous and generator functions

0 commit comments

Comments
 (0)