diff --git a/aws_lambda_powertools/logging/utils.py b/aws_lambda_powertools/logging/utils.py index eb299c888a2..3e1c3c69aed 100644 --- a/aws_lambda_powertools/logging/utils.py +++ b/aws_lambda_powertools/logging/utils.py @@ -9,6 +9,7 @@ def copy_config_to_registered_loggers( source_logger: Logger, log_level: Optional[Union[int, str]] = None, + ignore_log_level=False, exclude: Optional[Set[str]] = None, include: Optional[Set[str]] = None, ) -> None: @@ -16,10 +17,13 @@ def copy_config_to_registered_loggers( Parameters ---------- + ignore_log_level source_logger : Logger Powertools for AWS Lambda (Python) Logger to copy configuration from log_level : Union[int, str], optional Logging level to set to registered loggers, by default uses source_logger logging level + ignore_log_level: bool + Whether to not touch log levels for discovered loggers. log_level param is disregarded when this is set. include : Optional[Set[str]], optional List of logger names to include, by default all registered loggers are included exclude : Optional[Set[str]], optional @@ -54,7 +58,7 @@ def copy_config_to_registered_loggers( registered_loggers = _find_registered_loggers(source_logger, loggers, filter_func) for logger in registered_loggers: - _configure_logger(source_logger, logger, level) + _configure_logger(source_logger=source_logger, logger=logger, level=level, ignore_log_level=ignore_log_level) def _include_registered_loggers_filter(loggers: Set[str]): @@ -78,13 +82,21 @@ def _find_registered_loggers( return root_loggers -def _configure_logger(source_logger: Logger, logger: logging.Logger, level: Union[int, str]) -> None: +def _configure_logger( + source_logger: Logger, + logger: logging.Logger, + level: Union[int, str], + ignore_log_level: bool = False, +) -> None: + # customers may not want to copy the same log level from Logger to discovered loggers + if not ignore_log_level: + logger.setLevel(level) + source_logger.debug(f"Logger {logger} reconfigured to use logging level {level}") + logger.handlers = [] - logger.setLevel(level) logger.propagate = False # ensure we don't propagate logs to existing loggers, #1073 source_logger.append_keys(name="%(name)s") # include logger name, see #1267 - source_logger.debug(f"Logger {logger} reconfigured to use logging level {level}") for source_handler in source_logger.handlers: logger.addHandler(source_handler) source_logger.debug(f"Logger {logger} reconfigured to use {source_handler}") diff --git a/docs/core/logger.md b/docs/core/logger.md index 758762b547c..ec81463622a 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -811,10 +811,11 @@ for the given name and level to the logging module. By default, this logs all bo You can copy the Logger setup to all or sub-sets of registered external loggers. Use the `copy_config_to_registered_logger` method to do this. -???+ tip - To help differentiate between loggers, we include the standard logger `name` attribute for all loggers we copied configuration to. +!!! tip "We include the logger `name` attribute for all loggers we copied configuration to help you differentiate them." + +By default all registered loggers will be modified. You can change this behavior by providing `include` and `exclude` attributes. -By default all registered loggers will be modified. You can change this behavior by providing `include` and `exclude` attributes. You can also provide optional `log_level` attribute external loggers will be configured with. +You can also provide optional `log_level` attribute external top-level loggers will be configured with, by default it'll use the source logger log level. You can opt-out by using `ignore_log_level=True` parameter. ```python hl_lines="10" title="Cloning Logger config to all other registered standard loggers" ---8<-- "examples/logger/src/cloning_logger_config.py" diff --git a/tests/functional/test_logger_utils.py b/tests/functional/test_logger_utils.py index 7ca9e1198ac..5a95e2c54a2 100644 --- a/tests/functional/test_logger_utils.py +++ b/tests/functional/test_logger_utils.py @@ -131,8 +131,8 @@ def test_copy_config_to_ext_loggers_include_exclude(stdout, logger, log_level): # AND external logger_1 is also in EXCLUDE list utils.copy_config_to_registered_loggers( source_logger=powertools_logger, - include={logger_1.name, logger_2.name}, exclude={logger_1.name}, + include={logger_1.name, logger_2.name}, ) msg = "test message3" logger_2.info(msg) @@ -175,8 +175,8 @@ def test_copy_config_to_ext_loggers_custom_log_level(stdout, logger, log_level, # AND external logger used with custom log_level utils.copy_config_to_registered_loggers( source_logger=powertools_logger, - include={logger.name}, log_level=level_to_set, + include={logger.name}, ) msg = "test message4" logger.warning(msg) @@ -263,7 +263,7 @@ def test_copy_config_to_ext_loggers_no_duplicate_logs(stdout, logger, log_level) # WHEN configuration copied from Powertools for AWS Lambda (Python) logger # AND external logger used with custom log_level - utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include={logger.name}, log_level=level) + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, log_level=level, include={logger.name}) msg = "test message4" logger.warning(msg) @@ -294,3 +294,22 @@ def test_logger_name_is_included_during_copy(stdout, logger, log_level): assert logger1_log["name"] == logger_1.name assert logger2_log["name"] == logger_2.name assert pt_log["name"] == powertools_logger.name + + +def test_copy_config_to_ext_loggers_but_preserve_log_levels(stdout, logger, log_level): + # GIVEN two external loggers and Powertools for AWS Lambda (Python) logger initialized + third_party_log_level = logging.CRITICAL + + logger_1 = logger() + logger_2 = logger() + logger_1.setLevel(third_party_log_level) + logger_2.setLevel(third_party_log_level) + + powertools_logger = Logger(service=service_name(), stream=stdout) + + # WHEN configuration copied from Powertools for AWS Lambda (Python) logger to ALL external loggers + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, ignore_log_level=True) + + # THEN external loggers log levels should be preserved + assert logger_1.level != powertools_logger.log_level + assert logger_2.level != powertools_logger.log_level