Skip to content

feat(logger-utils): preserve log level for discovered third-party top-level loggers #4299

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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