Skip to content

Commit 8ad7e04

Browse files
authored
improv: override Tracer auto-capture response/exception via env vars (#259)
* improv: add capture_response/error env vars * improv: add tests for capture_response/error env vars * docs: update env vars * improv: address Nicolas feedback; improve docs * docs: fix wording on disable exception example
1 parent dfb97af commit 8ad7e04

File tree

8 files changed

+151
-40
lines changed

8 files changed

+151
-40
lines changed

aws_lambda_powertools/shared/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import os
2+
3+
TRACER_CAPTURE_RESPONSE_ENV: str = os.getenv("POWERTOOLS_TRACER_CAPTURE_RESPONSE", "true")
4+
TRACER_CAPTURE_ERROR_ENV: str = os.getenv("POWERTOOLS_TRACER_CAPTURE_ERROR", "true")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from distutils.util import strtobool
2+
3+
4+
def resolve_env_var_choice(env: str, choice: bool = None) -> bool:
5+
return choice if choice is not None else strtobool(env)

aws_lambda_powertools/tracing/tracer.py

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
import logging
66
import os
77
from distutils.util import strtobool
8-
from typing import Any, Callable, Dict, List, Tuple
8+
from typing import Any, Callable, Dict, List, Optional, Tuple
99

1010
import aws_xray_sdk
1111
import aws_xray_sdk.core
1212

13+
from aws_lambda_powertools.shared.constants import TRACER_CAPTURE_ERROR_ENV, TRACER_CAPTURE_RESPONSE_ENV
14+
from aws_lambda_powertools.shared.functions import resolve_env_var_choice
15+
1316
is_cold_start = True
1417
logger = logging.getLogger(__name__)
1518

@@ -34,6 +37,10 @@ class Tracer:
3437
disable tracer (e.g. `"true", "True", "TRUE"`)
3538
POWERTOOLS_SERVICE_NAME : str
3639
service name
40+
POWERTOOLS_TRACER_CAPTURE_RESPONSE : str
41+
disable auto-capture response as metadata (e.g. `"true", "True", "TRUE"`)
42+
POWERTOOLS_TRACER_CAPTURE_ERROR : str
43+
disable auto-capture error as metadata (e.g. `"true", "True", "TRUE"`)
3744
3845
Parameters
3946
----------
@@ -226,7 +233,12 @@ def patch(self, modules: Tuple[str] = None):
226233
else:
227234
aws_xray_sdk.core.patch(modules)
228235

229-
def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = None, capture_response: bool = True):
236+
def capture_lambda_handler(
237+
self,
238+
lambda_handler: Callable[[Dict, Any], Any] = None,
239+
capture_response: Optional[bool] = None,
240+
capture_error: Optional[bool] = None,
241+
):
230242
"""Decorator to create subsegment for lambda handlers
231243
232244
As Lambda follows (event, context) signature we can remove some of the boilerplate
@@ -237,7 +249,9 @@ def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = No
237249
lambda_handler : Callable
238250
Method to annotate on
239251
capture_response : bool, optional
240-
Instructs tracer to not include handler's response as metadata, by default True
252+
Instructs tracer to not include handler's response as metadata
253+
capture_error : bool, optional
254+
Instructs tracer to not include handler's error as metadata, by default True
241255
242256
Example
243257
-------
@@ -264,10 +278,15 @@ def handler(event, context):
264278
# Return a partial function with args filled
265279
if lambda_handler is None:
266280
logger.debug("Decorator called with parameters")
267-
return functools.partial(self.capture_lambda_handler, capture_response=capture_response)
281+
return functools.partial(
282+
self.capture_lambda_handler, capture_response=capture_response, capture_error=capture_error
283+
)
268284

269285
lambda_handler_name = lambda_handler.__name__
270286

287+
capture_response = resolve_env_var_choice(env=TRACER_CAPTURE_RESPONSE_ENV, choice=capture_response)
288+
capture_error = resolve_env_var_choice(env=TRACER_CAPTURE_ERROR_ENV, choice=capture_error)
289+
271290
@functools.wraps(lambda_handler)
272291
def decorate(event, context):
273292
with self.provider.in_subsegment(name=f"## {lambda_handler_name}") as subsegment:
@@ -290,15 +309,17 @@ def decorate(event, context):
290309
except Exception as err:
291310
logger.exception(f"Exception received from {lambda_handler_name}")
292311
self._add_full_exception_as_metadata(
293-
method_name=lambda_handler_name, error=err, subsegment=subsegment
312+
method_name=lambda_handler_name, error=err, subsegment=subsegment, capture_error=capture_error
294313
)
295314
raise
296315

297316
return response
298317

299318
return decorate
300319

301-
def capture_method(self, method: Callable = None, capture_response: bool = True):
320+
def capture_method(
321+
self, method: Callable = None, capture_response: Optional[bool] = None, capture_error: Optional[bool] = None
322+
):
302323
"""Decorator to create subsegment for arbitrary functions
303324
304325
It also captures both response and exceptions as metadata
@@ -317,7 +338,9 @@ def capture_method(self, method: Callable = None, capture_response: bool = True)
317338
method : Callable
318339
Method to annotate on
319340
capture_response : bool, optional
320-
Instructs tracer to not include method's response as metadata, by default True
341+
Instructs tracer to not include method's response as metadata
342+
capture_error : bool, optional
343+
Instructs tracer to not include handler's error as metadata, by default True
321344
322345
Example
323346
-------
@@ -449,51 +472,65 @@ async def async_tasks():
449472
# Return a partial function with args filled
450473
if method is None:
451474
logger.debug("Decorator called with parameters")
452-
return functools.partial(self.capture_method, capture_response=capture_response)
475+
return functools.partial(
476+
self.capture_method, capture_response=capture_response, capture_error=capture_error
477+
)
453478

454479
method_name = f"{method.__name__}"
455480

481+
capture_response = resolve_env_var_choice(env=TRACER_CAPTURE_RESPONSE_ENV, choice=capture_response)
482+
capture_error = resolve_env_var_choice(env=TRACER_CAPTURE_ERROR_ENV, choice=capture_error)
483+
456484
if inspect.iscoroutinefunction(method):
457485
return self._decorate_async_function(
458-
method=method, capture_response=capture_response, method_name=method_name
486+
method=method, capture_response=capture_response, capture_error=capture_error, method_name=method_name
459487
)
460488
elif inspect.isgeneratorfunction(method):
461489
return self._decorate_generator_function(
462-
method=method, capture_response=capture_response, method_name=method_name
490+
method=method, capture_response=capture_response, capture_error=capture_error, method_name=method_name
463491
)
464492
elif hasattr(method, "__wrapped__") and inspect.isgeneratorfunction(method.__wrapped__):
465493
return self._decorate_generator_function_with_context_manager(
466-
method=method, capture_response=capture_response, method_name=method_name
494+
method=method, capture_response=capture_response, capture_error=capture_error, method_name=method_name
467495
)
468496
else:
469497
return self._decorate_sync_function(
470-
method=method, capture_response=capture_response, method_name=method_name
498+
method=method, capture_response=capture_response, capture_error=capture_error, method_name=method_name
471499
)
472500

473-
def _decorate_async_function(self, method: Callable = None, capture_response: bool = True, method_name: str = None):
501+
def _decorate_async_function(
502+
self,
503+
method: Callable = None,
504+
capture_response: Optional[bool] = None,
505+
capture_error: Optional[bool] = None,
506+
method_name: str = None,
507+
):
474508
@functools.wraps(method)
475509
async def decorate(*args, **kwargs):
476510
async with self.provider.in_subsegment_async(name=f"## {method_name}") as subsegment:
477511
try:
478512
logger.debug(f"Calling method: {method_name}")
479513
response = await method(*args, **kwargs)
480514
self._add_response_as_metadata(
481-
method_name=method_name,
482-
data=response,
483-
subsegment=subsegment,
484-
capture_response=capture_response,
515+
method_name=method_name, data=response, subsegment=subsegment, capture_response=capture_response
485516
)
486517
except Exception as err:
487518
logger.exception(f"Exception received from '{method_name}' method")
488-
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
519+
self._add_full_exception_as_metadata(
520+
method_name=method_name, error=err, subsegment=subsegment, capture_error=capture_error
521+
)
489522
raise
490523

491524
return response
492525

493526
return decorate
494527

495528
def _decorate_generator_function(
496-
self, method: Callable = None, capture_response: bool = True, method_name: str = None
529+
self,
530+
method: Callable = None,
531+
capture_response: Optional[bool] = None,
532+
capture_error: Optional[bool] = None,
533+
method_name: str = None,
497534
):
498535
@functools.wraps(method)
499536
def decorate(*args, **kwargs):
@@ -506,15 +543,21 @@ def decorate(*args, **kwargs):
506543
)
507544
except Exception as err:
508545
logger.exception(f"Exception received from '{method_name}' method")
509-
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
546+
self._add_full_exception_as_metadata(
547+
method_name=method_name, error=err, subsegment=subsegment, capture_error=capture_error
548+
)
510549
raise
511550

512551
return result
513552

514553
return decorate
515554

516555
def _decorate_generator_function_with_context_manager(
517-
self, method: Callable = None, capture_response: bool = True, method_name: str = None
556+
self,
557+
method: Callable = None,
558+
capture_response: Optional[bool] = None,
559+
capture_error: Optional[bool] = None,
560+
method_name: str = None,
518561
):
519562
@functools.wraps(method)
520563
@contextlib.contextmanager
@@ -530,12 +573,20 @@ def decorate(*args, **kwargs):
530573
)
531574
except Exception as err:
532575
logger.exception(f"Exception received from '{method_name}' method")
533-
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
576+
self._add_full_exception_as_metadata(
577+
method_name=method_name, error=err, subsegment=subsegment, capture_error=capture_error
578+
)
534579
raise
535580

536581
return decorate
537582

538-
def _decorate_sync_function(self, method: Callable = None, capture_response: bool = True, method_name: str = None):
583+
def _decorate_sync_function(
584+
self,
585+
method: Callable = None,
586+
capture_response: Optional[bool] = None,
587+
capture_error: Optional[bool] = None,
588+
method_name: str = None,
589+
):
539590
@functools.wraps(method)
540591
def decorate(*args, **kwargs):
541592
with self.provider.in_subsegment(name=f"## {method_name}") as subsegment:
@@ -550,7 +601,9 @@ def decorate(*args, **kwargs):
550601
)
551602
except Exception as err:
552603
logger.exception(f"Exception received from '{method_name}' method")
553-
self._add_full_exception_as_metadata(method_name=method_name, error=err, subsegment=subsegment)
604+
self._add_full_exception_as_metadata(
605+
method_name=method_name, error=err, subsegment=subsegment, capture_error=capture_error
606+
)
554607
raise
555608

556609
return response
@@ -562,7 +615,7 @@ def _add_response_as_metadata(
562615
method_name: str = None,
563616
data: Any = None,
564617
subsegment: aws_xray_sdk.core.models.subsegment = None,
565-
capture_response: bool = True,
618+
capture_response: Optional[bool] = None,
566619
):
567620
"""Add response as metadata for given subsegment
568621
@@ -575,15 +628,19 @@ def _add_response_as_metadata(
575628
subsegment : aws_xray_sdk.core.models.subsegment, optional
576629
existing subsegment to add metadata on, by default None
577630
capture_response : bool, optional
578-
Do not include response as metadata, by default True
631+
Do not include response as metadata
579632
"""
580633
if data is None or not capture_response or subsegment is None:
581634
return
582635

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

585638
def _add_full_exception_as_metadata(
586-
self, method_name: str = None, error: Exception = None, subsegment: aws_xray_sdk.core.models.subsegment = None
639+
self,
640+
method_name: str = None,
641+
error: Exception = None,
642+
subsegment: aws_xray_sdk.core.models.subsegment = None,
643+
capture_error: Optional[bool] = None,
587644
):
588645
"""Add full exception object as metadata for given subsegment
589646
@@ -595,7 +652,12 @@ def _add_full_exception_as_metadata(
595652
error to add as subsegment metadata, by default None
596653
subsegment : aws_xray_sdk.core.models.subsegment, optional
597654
existing subsegment to add metadata on, by default None
655+
capture_error : bool, optional
656+
Do not include error as metadata, by default True
598657
"""
658+
if not capture_error:
659+
return
660+
599661
subsegment.put_metadata(key=f"{method_name} error", value=error, namespace=self._config["service"])
600662

601663
@staticmethod

docs/content/core/tracer.mdx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,16 @@ def handler(event, context):
6565
...
6666
```
6767

68+
### disabling response auto-capture
69+
70+
> New in 1.9.0
71+
6872
<Note type="warning">
6973
<strong>Returning sensitive information from your Lambda handler or functions, where Tracer is used?</strong>
7074
<br/><br/>
71-
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.
7275
</Note><br/>
7376

77+
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.
7478

7579
```python:title=do_not_capture_response_as_metadata.py
7680
# Disables Tracer from capturing response and adding as metadata
@@ -80,6 +84,25 @@ def handler(event, context):
8084
return "sensitive_information"
8185
```
8286

87+
### disabling exception auto-capture
88+
89+
> New in 1.10.0
90+
91+
<Note type="warning">
92+
<strong>Can exceptions contain sensitive information from your Lambda handler or functions, where Tracer is used?</strong>
93+
<br/><br/>
94+
</Note><br/>
95+
96+
You can disable Tracer from capturing their exceptions as tracing metadata with <strong><code>capture_error=False</code></strong> parameter in both capture_lambda_handler and capture_method decorators.
97+
98+
```python:title=do_not_capture_exception_as_metadata.py
99+
# Disables Tracer from capturing exception and adding as metadata
100+
# Useful when dealing with sensitive data
101+
@tracer.capture_lambda_handler(capture_error=False) # highlight-line
102+
def handler(event, context):
103+
raise ValueError("some sensitive info in the stack trace...")
104+
```
105+
83106
### Annotations
84107

85108
Annotations are key-values indexed by AWS X-Ray on a per trace basis. You can use them to filter traces as well as to create [Trace Groups](https://aws.amazon.com/about-aws/whats-new/2018/11/aws-xray-adds-the-ability-to-group-traces/).

docs/content/index.mdx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,23 @@ Utility | Description
122122

123123
## Environment variables
124124

125+
<Note type="info">
126+
<strong>Explicit parameters take precedence over environment variables.</strong><br/><br/>
127+
</Note>
128+
125129
**Environment variables** used across suite of utilities.
126130

127-
Environment variable | Description | Utility
128-
------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------
129-
**POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | All
130-
**POWERTOOLS_METRICS_NAMESPACE** | Sets namespace used for metrics | [Metrics](./core/metrics)
131-
**POWERTOOLS_TRACE_DISABLED** | Disables tracing | [Tracing](./core/tracer)
132-
**POWERTOOLS_TRACE_MIDDLEWARES** | Creates sub-segment for each custom middleware | [Middleware factory](./utilities/middleware_factory)
133-
**POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | [Logging](./core/logger)
134-
**POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logger)
135-
**LOG_LEVEL** | Sets logging level | [Logging](./core/logger)
131+
Environment variable | Description | Utility | Default
132+
------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------
133+
**POWERTOOLS_SERVICE_NAME** | Sets service name used for tracing namespace, metrics dimension and structured logging | All | `"service_undefined"`
134+
**POWERTOOLS_METRICS_NAMESPACE** | Sets namespace used for metrics | [Metrics](./core/metrics) | `None`
135+
**POWERTOOLS_TRACE_DISABLED** | Disables tracing | [Tracing](./core/tracer) | `false`
136+
**POWERTOOLS_TRACER_CAPTURE_RESPONSE** | Captures Lambda or method return as metadata. | [Tracing](./core/tracer) | `true`
137+
**POWERTOOLS_TRACER_CAPTURE_ERROR** | Captures Lambda or method exception as metadata. | [Tracing](./core/tracer) | `true`
138+
**POWERTOOLS_TRACE_MIDDLEWARES** | Creates sub-segment for each custom middleware | [Middleware factory](./utilities/middleware_factory) | `false`
139+
**POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | [Logging](./core/logger) | `false`
140+
**POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logger) | `0`
141+
**LOG_LEVEL** | Sets logging level | [Logging](./core/logger) | `INFO`
136142

137143
## Debug mode
138144

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from aws_lambda_powertools.shared.functions import resolve_env_var_choice
2+
3+
4+
def test_resolve_env_var_choice_explicit_wins_over_env_var():
5+
assert resolve_env_var_choice(env="true", choice=False) is False
6+
7+
8+
def test_resolve_env_var_choice_env_wins_over_absent_explicit():
9+
assert resolve_env_var_choice(env="true") == 1

0 commit comments

Comments
 (0)