Skip to content

Commit a717ca7

Browse files
authored
feat(logger): add option to clear state per invocation (#467)
1 parent 086be96 commit a717ca7

File tree

3 files changed

+106
-5
lines changed

3 files changed

+106
-5
lines changed

aws_lambda_powertools/logging/logger.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,18 @@ def _configure_sampling(self):
260260
)
261261

262262
def inject_lambda_context(
263-
self, lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = None, correlation_id_path: str = None
263+
self,
264+
lambda_handler: Callable[[Dict, Any], Any] = None,
265+
log_event: bool = None,
266+
correlation_id_path: str = None,
267+
clear_state: bool = False,
264268
):
265269
"""Decorator to capture Lambda contextual info and inject into logger
266270
267271
Parameters
268272
----------
273+
clear_state : bool, optional
274+
Instructs logger to remove any custom keys previously added
269275
lambda_handler : Callable
270276
Method to inject the lambda context
271277
log_event : bool, optional
@@ -311,7 +317,10 @@ def handler(event, context):
311317
if lambda_handler is None:
312318
logger.debug("Decorator called with parameters")
313319
return functools.partial(
314-
self.inject_lambda_context, log_event=log_event, correlation_id_path=correlation_id_path
320+
self.inject_lambda_context,
321+
log_event=log_event,
322+
correlation_id_path=correlation_id_path,
323+
clear_state=clear_state,
315324
)
316325

317326
log_event = resolve_truthy_env_var_choice(
@@ -322,7 +331,11 @@ def handler(event, context):
322331
def decorate(event, context):
323332
lambda_context = build_lambda_context_model(context)
324333
cold_start = _is_cold_start()
325-
self.append_keys(cold_start=cold_start, **lambda_context.__dict__)
334+
335+
if clear_state:
336+
self.structure_logs(cold_start=cold_start, **lambda_context.__dict__)
337+
else:
338+
self.append_keys(cold_start=cold_start, **lambda_context.__dict__)
326339

327340
if correlation_id_path:
328341
self.set_correlation_id(jmespath.search(correlation_id_path, event))

docs/core/logger.md

+70-2
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,9 @@ We provide [built-in JMESPath expressions](#built-in-correlation-id-expressions)
231231

232232
### Appending additional keys
233233

234-
!!! info "Keys might be persisted across invocations"
235-
Always set additional keys as part of your handler to ensure they have the latest value. Additional keys are kept in memory as part of a Logger instance and might be reused in non-cold start scenarios.
234+
!!! info "Custom keys are persisted across warm invocations"
235+
Always set additional keys as part of your handler to ensure they have the latest value, or explicitly clear them with [`clear_state=True`](#clearing-all-state).
236+
236237

237238
You can append additional keys using either mechanism:
238239

@@ -426,6 +427,73 @@ You can remove any additional key from Logger state using `remove_keys`.
426427
}
427428
```
428429

430+
#### Clearing all state
431+
432+
Logger is commonly initialized in the global scope. Due to [Lambda Execution Context reuse](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), this means that custom keys can be persisted across invocations. If you want all custom keys to be deleted, you can use `clear_state=True` param in `inject_lambda_context` decorator.
433+
434+
!!! info
435+
This is useful when you add multiple custom keys conditionally, instead of setting a default `None` value if not present. Any key with `None` value is automatically removed by Logger.
436+
437+
!!! danger "This can have unintended side effects if you use Layers"
438+
Lambda Layers code is imported before the Lambda handler.
439+
440+
This means that `clear_state=True` will instruct Logger to remove any keys previously added before Lambda handler execution proceeds.
441+
442+
You can either avoid running any code as part of Lambda Layers global scope, or override keys with their latest value as part of handler's execution.
443+
444+
=== "collect.py"
445+
446+
```python hl_lines="5 8"
447+
from aws_lambda_powertools import Logger
448+
449+
logger = Logger(service="payment")
450+
451+
@logger.inject_lambda_context(clear_state=True)
452+
def handler(event, context):
453+
if event.get("special_key"):
454+
# Should only be available in the first request log
455+
# as the second request doesn't contain `special_key`
456+
logger.append_keys(debugging_key="value")
457+
458+
logger.info("Collecting payment")
459+
```
460+
461+
=== "#1 request"
462+
463+
```json hl_lines="7"
464+
{
465+
"level": "INFO",
466+
"location": "collect.handler:10",
467+
"message": "Collecting payment",
468+
"timestamp": "2021-05-03 11:47:12,494+0200",
469+
"service": "payment",
470+
"special_key": "debug_key",
471+
"cold_start": true,
472+
"lambda_function_name": "test",
473+
"lambda_function_memory_size": 128,
474+
"lambda_function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test",
475+
"lambda_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72"
476+
}
477+
```
478+
479+
=== "#2 request"
480+
481+
```json hl_lines="7"
482+
{
483+
"level": "INFO",
484+
"location": "collect.handler:10",
485+
"message": "Collecting payment",
486+
"timestamp": "2021-05-03 11:47:12,494+0200",
487+
"service": "payment",
488+
"cold_start": false,
489+
"lambda_function_name": "test",
490+
"lambda_function_memory_size": 128,
491+
"lambda_function_arn": "arn:aws:lambda:eu-west-1:12345678910:function:test",
492+
"lambda_request_id": "52fdfc07-2182-154f-163f-5f0f9a621d72"
493+
}
494+
```
495+
496+
429497
### Logging exceptions
430498

431499
Use `logger.exception` method to log contextual information about exceptions. Logger will include `exception_name` and `exception` keys to aid troubleshooting and error enumeration.

tests/functional/test_logger.py

+20
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,23 @@ def handler(event, context):
562562
# THEN we should output to a file not stdout
563563
log = log_file.read_text()
564564
assert "custom handler" in log
565+
566+
567+
def test_clear_state_on_inject_lambda_context(lambda_context, stdout, service_name):
568+
# GIVEN
569+
logger = Logger(service=service_name, stream=stdout)
570+
571+
# WHEN clear_state is set and a key was conditionally added in the first invocation
572+
@logger.inject_lambda_context(clear_state=True)
573+
def handler(event, context):
574+
if event.get("add_key"):
575+
logger.append_keys(my_key="value")
576+
logger.info("Foo")
577+
578+
# THEN custom key should only exist in the first log
579+
handler({"add_key": True}, lambda_context)
580+
handler({}, lambda_context)
581+
582+
first_log, second_log = capture_multiple_logging_statements_output(stdout)
583+
assert "my_key" in first_log
584+
assert "my_key" not in second_log

0 commit comments

Comments
 (0)