Skip to content
20 changes: 16 additions & 4 deletions aws_lambda_powertools/logging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@
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:
"""Copies source Logger level and handler to all registered loggers for consistent formatting.

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
Expand Down Expand Up @@ -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]):
Expand All @@ -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}")
7 changes: 4 additions & 3 deletions docs/core/logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 22 additions & 3 deletions tests/functional/test_logger_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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