From 22c992702f3a4e86c63e83032330b3c2087a20fd Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Thu, 30 Jan 2025 16:15:17 -0300 Subject: [PATCH 1/3] feat(logger): add clear_state method to Logger --- aws_lambda_powertools/logging/logger.py | 10 +++++++ docs/core/logger.md | 27 ++++++++++++++++- examples/logger/src/after_clear_state.json | 7 +++++ examples/logger/src/before_clear_state.json | 20 +++++++++++++ examples/logger/src/clear_state_method.py | 15 ++++++++++ .../required_dependencies/test_logger.py | 29 +++++++++++++++++++ 6 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 examples/logger/src/after_clear_state.json create mode 100644 examples/logger/src/before_clear_state.json create mode 100644 examples/logger/src/clear_state_method.py diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py index ab29b6af649..a407515a92c 100644 --- a/aws_lambda_powertools/logging/logger.py +++ b/aws_lambda_powertools/logging/logger.py @@ -222,6 +222,7 @@ def __init__( choice=sampling_rate, env=os.getenv(constants.LOGGER_LOG_SAMPLING_RATE), ) + self._default_log_keys: dict[str, Any] = {"service": self.service, "sampling_rate": self.sampling_rate} self.child = child self.logger_formatter = logger_formatter self._stream = stream or sys.stdout @@ -605,6 +606,15 @@ def append_context_keys(self, **additional_keys: Any) -> Generator[None, None, N with self.registered_formatter.append_context_keys(**additional_keys): yield + def clear_state(self) -> None: + """Removes all custom keys that were appended to the Logger.""" + # Clear all custom keys from the formatter + self.registered_formatter.clear_state() + + # Reset to default keys + default_keys: dict[Any, Any] = dict(self._default_log_keys) + self.structure_logs(**default_keys) + # These specific thread-safe methods are necessary to manage shared context in concurrent environments. # They prevent race conditions and ensure data consistency across multiple threads. def thread_safe_append_keys(self, **additional_keys: object) -> None: diff --git a/docs/core/logger.md b/docs/core/logger.md index 9915f7cc4b4..38384dae3b9 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -274,13 +274,15 @@ You can remove any additional key from Logger state using `remove_keys`. #### Clearing all state +##### Decorator with clear_state + 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){target="_blank"}, 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. ???+ tip "Tip: When is this useful?" It 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. ???+ danger "Danger: This can have unintended side effects if you use Layers" - Lambda Layers code is imported before the Lambda handler. + Lambda Layers code is imported before the Lambda handler. When a Lambda function starts, it first imports and executes all code in the Layers (including any global scope code) before proceeding to the function's own code. This means that `clear_state=True` will instruct Logger to remove any keys previously added before Lambda handler execution proceeds. @@ -304,6 +306,29 @@ Logger is commonly initialized in the global scope. Due to [Lambda Execution Con --8<-- "examples/logger/src/clear_state_event_two.json" ``` +##### clear_state method + +`clear_state()` is a method you can call explicitly within your code to clear appended keys at any point during the execution of a single Lambda invocation. + +This allows for more granular control over the logger's state within a single function execution, enabling you to reset the logger to its initial state before specific logging operations or at the end of certain processes within the same Lambda run. + +=== "clear_state_method.py" + + ```python hl_lines="12" + --8<-- "examples/logger/src/clear_state_method.py" + ``` +=== "Output before clear_state()" + + ```json hl_lines="9 17" + --8<-- "examples/logger/src/before_clear_state.json" + ``` + +=== "Output after clear_state()" + + ```json hl_lines="4" + --8<-- "examples/logger/src/after_clear_state.json" + ``` + ### Accessing currently configured keys You can view all currently configured keys from the Logger state using the `get_current_keys()` method. This method is useful when you need to avoid overwriting keys that are already configured. diff --git a/examples/logger/src/after_clear_state.json b/examples/logger/src/after_clear_state.json new file mode 100644 index 00000000000..54dd72ed41e --- /dev/null +++ b/examples/logger/src/after_clear_state.json @@ -0,0 +1,7 @@ +{ + "level": "INFO", + "location": "lambda_handler:126", + "message": "State after clearing - only show default keys", + "timestamp": "2025-01-30 13:56:03,158-0300", + "service": "payment" +} \ No newline at end of file diff --git a/examples/logger/src/before_clear_state.json b/examples/logger/src/before_clear_state.json new file mode 100644 index 00000000000..a710dbde0d6 --- /dev/null +++ b/examples/logger/src/before_clear_state.json @@ -0,0 +1,20 @@ +{ + "logs": [ + { + "level": "INFO", + "location": "lambda_handler:122", + "message": "Starting order processing", + "timestamp": "2025-01-30 13:56:03,157-0300", + "service": "payment", + "order_id": "12345" + }, + { + "level": "INFO", + "location": "lambda_handler:124", + "message": "Final state before clearing", + "timestamp": "2025-01-30 13:56:03,157-0300", + "service": "payment", + "order_id": "12345" + } + ] +} \ No newline at end of file diff --git a/examples/logger/src/clear_state_method.py b/examples/logger/src/clear_state_method.py new file mode 100644 index 00000000000..1564b4c2c39 --- /dev/null +++ b/examples/logger/src/clear_state_method.py @@ -0,0 +1,15 @@ +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger(service="payment", level="DEBUG") + + +def lambda_handler(event: dict, context: LambdaContext) -> str: + try: + logger.append_keys(order_id="12345") + logger.info("Starting order processing") + finally: + logger.info("Final state before clearing") + logger.clear_state() + logger.info("State after clearing - only show default keys") + return "Completed" diff --git a/tests/functional/logger/required_dependencies/test_logger.py b/tests/functional/logger/required_dependencies/test_logger.py index 5c85677d73d..ee69aca77b1 100644 --- a/tests/functional/logger/required_dependencies/test_logger.py +++ b/tests/functional/logger/required_dependencies/test_logger.py @@ -1232,3 +1232,32 @@ def test_logger_change_level_child_logger(stdout, service_name): logs = list(stdout.getvalue().strip().split("\n")) assert len(logs) == 1 assert "service" in logs[0] + + +def test_clear_state_with_append_keys(): + # GIVEN a Logger is initialized + logger = Logger(service="service_name", stream=stdout) + + # WHEN append keys are added + logger.append_keys(custom_key="custom_key") + logger.info("message with appended keys") + logger.clear_state() + + # THEN context keys should be cleared + assert "custom_key" not in logger.get_current_keys() + + +def test_clear_state(stdout, service_name): + logger = Logger(service=service_name, stream=stdout) + logger.info("message for the user") + logger.clear_state() + + expected_keys = { + "level": "%(levelname)s", + "location": "%(funcName)s:%(lineno)d", + "message": None, + "timestamp": "%(asctime)s", + "service": service_name, + "sampling_rate": None, + } + assert logger.get_current_keys() == expected_keys From 4a4c958e63b49815857f1548886d0aa61dd85e92 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Fri, 31 Jan 2025 16:56:41 -0300 Subject: [PATCH 2/3] changes after feedback --- aws_lambda_powertools/logging/logger.py | 1 - docs/core/logger.md | 4 +-- .../required_dependencies/test_logger.py | 25 +++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py index a407515a92c..b3cd823cfd8 100644 --- a/aws_lambda_powertools/logging/logger.py +++ b/aws_lambda_powertools/logging/logger.py @@ -232,7 +232,6 @@ def __init__( self._is_deduplication_disabled = resolve_truthy_env_var_choice( env=os.getenv(constants.LOGGER_LOG_DEDUPLICATION_ENV, "false"), ) - self._default_log_keys = {"service": self.service, "sampling_rate": self.sampling_rate} self._logger = self._get_logger() # NOTE: This is primarily to improve UX, so IDEs can autocomplete LambdaPowertoolsFormatter options diff --git a/docs/core/logger.md b/docs/core/logger.md index 38384dae3b9..27fb532ad00 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -308,9 +308,7 @@ Logger is commonly initialized in the global scope. Due to [Lambda Execution Con ##### clear_state method -`clear_state()` is a method you can call explicitly within your code to clear appended keys at any point during the execution of a single Lambda invocation. - -This allows for more granular control over the logger's state within a single function execution, enabling you to reset the logger to its initial state before specific logging operations or at the end of certain processes within the same Lambda run. +You can call `clear_state()` as a method explicitly within your code to clear appended keys at any point during the execution of your Lambda invocation. === "clear_state_method.py" diff --git a/tests/functional/logger/required_dependencies/test_logger.py b/tests/functional/logger/required_dependencies/test_logger.py index ee69aca77b1..92c8e27ff4b 100644 --- a/tests/functional/logger/required_dependencies/test_logger.py +++ b/tests/functional/logger/required_dependencies/test_logger.py @@ -1248,10 +1248,14 @@ def test_clear_state_with_append_keys(): def test_clear_state(stdout, service_name): + # GIVEN a Logger is initialized logger = Logger(service=service_name, stream=stdout) logger.info("message for the user") + + # WHEN the clear_state method is called logger.clear_state() + # THEN the logger's current keys should be reset to their default values expected_keys = { "level": "%(levelname)s", "location": "%(funcName)s:%(lineno)d", @@ -1261,3 +1265,24 @@ def test_clear_state(stdout, service_name): "sampling_rate": None, } assert logger.get_current_keys() == expected_keys + + +def test_clear_state_log_output(stdout, service_name): + # GIVEN a Logger is initialized + logger = Logger(service=service_name, stream=stdout) + + # WHEN we append a custom key and log + logger.append_keys(custom_key="test_value") + logger.info("first message") + + # AND we clear the state and log again + logger.clear_state() + logger.info("second message") + + # THEN the first log should contain the custom key + # AND the second log should not contain the custom key + first_log, second_log = capture_multiple_logging_statements_output(stdout) + + assert "custom_key" in first_log + assert first_log["custom_key"] == "test_value" + assert "custom_key" not in second_log From a8945986836ebc1bdd3ad475d710d8411d9ea131 Mon Sep 17 00:00:00 2001 From: Ana Falcao Date: Fri, 31 Jan 2025 17:38:38 -0300 Subject: [PATCH 3/3] change reset default keys --- aws_lambda_powertools/logging/logger.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py index b3cd823cfd8..61bfb82867b 100644 --- a/aws_lambda_powertools/logging/logger.py +++ b/aws_lambda_powertools/logging/logger.py @@ -611,8 +611,7 @@ def clear_state(self) -> None: self.registered_formatter.clear_state() # Reset to default keys - default_keys: dict[Any, Any] = dict(self._default_log_keys) - self.structure_logs(**default_keys) + self.structure_logs(**self._default_log_keys) # These specific thread-safe methods are necessary to manage shared context in concurrent environments. # They prevent race conditions and ensure data consistency across multiple threads.