From a659301adf28873debdd1ab4175ea90e4f925b45 Mon Sep 17 00:00:00 2001 From: Michal Ploski Date: Fri, 24 Dec 2021 00:13:01 +0100 Subject: [PATCH 1/7] feat(logger): enable powertools logging for imported libraries (#40) --- aws_lambda_powertools/logging/utils.py | 30 ++++++ tests/functional/test_logger_utils.py | 144 +++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 aws_lambda_powertools/logging/utils.py create mode 100644 tests/functional/test_logger_utils.py diff --git a/aws_lambda_powertools/logging/utils.py b/aws_lambda_powertools/logging/utils.py new file mode 100644 index 00000000000..5c360ecf97e --- /dev/null +++ b/aws_lambda_powertools/logging/utils.py @@ -0,0 +1,30 @@ +import logging +from typing import List, Optional, TypeVar + +from .logger import Logger + +PowertoolsLogger = TypeVar("PowertoolsLogger", bound=Logger) + + +def copy_config_to_registered_loggers( + source_logger: PowertoolsLogger, exclude: Optional[List[str]] = None, include: Optional[List[str]] = None +): + root_loggers = [ + logging.getLogger(name) + for name in logging.root.manager.loggerDict + if "." not in name and name != source_logger.name + ] + source_logger.debug(f"Found registered root loggers: {root_loggers}") + if include and not exclude: + root_loggers = [logger for logger in root_loggers if logger.name in include] + elif not include and exclude: + root_loggers = [logger for logger in root_loggers if logger.name not in exclude] + elif include and exclude: + root_loggers = [logger for logger in root_loggers if logger.name in include and logger.name not in exclude] + + source_logger.debug(f"Filtered root loggers: {root_loggers}") + for logger in root_loggers: + logger.handlers = [] + logger.setLevel(source_logger.level) + for source_handler in source_logger.handlers: + logger.addHandler(source_handler) diff --git a/tests/functional/test_logger_utils.py b/tests/functional/test_logger_utils.py new file mode 100644 index 00000000000..7b1e8a30f99 --- /dev/null +++ b/tests/functional/test_logger_utils.py @@ -0,0 +1,144 @@ +import io +import json +import logging +import random +import string +from enum import Enum + +import pytest + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.logging import formatter, utils + + +@pytest.fixture +def stdout(): + return io.StringIO() + + +@pytest.fixture +def log_level(): + class LogLevel(Enum): + NOTSET = 0 + INFO = 20 + + return LogLevel + + +def capture_logging_output(stdout): + return json.loads(stdout.getvalue().strip()) + + +def service_name(): + chars = string.ascii_letters + string.digits + return "".join(random.SystemRandom().choice(chars) for _ in range(15)) + + +@pytest.fixture +def logger(stdout, log_level): + def _logger(): + logging.basicConfig(stream=stdout, level=log_level.NOTSET.value) + logger = logging.getLogger(name=service_name()) + return logger + + return _logger + + +def test_copy_config_to_ext_loggers(stdout, logger, log_level): + + msg = "test message" + + # GIVEN a external logger and powertools logger initialized + logger = logger() + logger_initial_handlers = logger.handlers.copy() + logger_initial_level = logger.level + powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) + + # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used + utils.copy_config_to_registered_loggers(source_logger=powertools_logger) + logger.info(msg) + log = capture_logging_output(stdout) + + # THEN + assert not logger_initial_handlers + assert logger_initial_level == log_level.NOTSET.value + assert len(logger.handlers) == 1 + assert type(logger.handlers[0]) is logging.StreamHandler + assert type(logger.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter + assert logger.level == log_level.INFO.value + assert log["message"] == msg + assert log["level"] == log_level.INFO.name + + +def test_copy_config_to_ext_loggers_include(stdout, logger, log_level): + + msg = "test message" + + # GIVEN a external logger and powertools logger initialized + logger = logger() + powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) + + # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include=[logger.name]) + logger.info(msg) + log = capture_logging_output(stdout) + + # THEN + assert len(logger.handlers) == 1 + assert type(logger.handlers[0]) is logging.StreamHandler + assert type(logger.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter + assert logger.level == log_level.INFO.value + assert log["message"] == msg + assert log["level"] == log_level.INFO.name + + +def test_copy_config_to_ext_loggers_wrong_include(stdout, logger, log_level): + + # GIVEN a external logger and powertools logger initialized + logger = logger() + powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) + + # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include=["non-existing-logger"]) + + # THEN + assert not logger.handlers + + +def test_copy_config_to_ext_loggers_exclude(stdout, logger, log_level): + + # GIVEN a external logger and powertools logger initialized + logger = logger() + powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) + + # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, exclude=[logger.name]) + + # THEN + assert not logger.handlers + + +def test_copy_config_to_ext_loggers_include_exclude(stdout, logger, log_level): + + msg = "test message" + + # GIVEN a external logger and powertools logger initialized + logger_1 = logger() + logger_2 = logger() + + powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) + + # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used + utils.copy_config_to_registered_loggers( + source_logger=powertools_logger, include=[logger_1.name, logger_2.name], exclude=[logger_1.name] + ) + logger_2.info(msg) + log = capture_logging_output(stdout) + # THEN + assert not logger_1.handlers + assert len(logger_2.handlers) == 1 + assert type(logger_2.handlers[0]) is logging.StreamHandler + assert type(logger_2.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter + assert logger_2.level == log_level.INFO.value + assert log["message"] == msg + assert log["level"] == log_level.INFO.name From ffeeebfc5f39c966dd288d9215f292af70d4aa3e Mon Sep 17 00:00:00 2001 From: Michal Ploski Date: Fri, 24 Dec 2021 10:29:08 +0100 Subject: [PATCH 2/7] Add new tests. Fix cyclomatic complexity check error --- aws_lambda_powertools/logging/utils.py | 61 +++++++++++++++++++----- tests/functional/test_logger_utils.py | 66 +++++++++++++++++--------- 2 files changed, 92 insertions(+), 35 deletions(-) diff --git a/aws_lambda_powertools/logging/utils.py b/aws_lambda_powertools/logging/utils.py index 5c360ecf97e..d21a1d28864 100644 --- a/aws_lambda_powertools/logging/utils.py +++ b/aws_lambda_powertools/logging/utils.py @@ -8,23 +8,58 @@ def copy_config_to_registered_loggers( source_logger: PowertoolsLogger, exclude: Optional[List[str]] = None, include: Optional[List[str]] = None -): - root_loggers = [ +) -> None: + """Enable powertools logging for imported libraries. + + Attach source logger handlers to external loggers. + Modify logger level based on source logger attribute. + """ + root_loggers = _find_root_loggers(source_logger, exclude, include) + for logger in root_loggers: + _configure_logger(source_logger, logger) + + +def _include_root_loggers_filter(logger_list: List[str]): + return [ + logging.getLogger(name) for name in logging.root.manager.loggerDict if "." not in name and name in logger_list + ] + + +def _exclude_root_loggers_filter(logger_list: List[str]) -> List[logging.Logger]: + return [ logging.getLogger(name) for name in logging.root.manager.loggerDict - if "." not in name and name != source_logger.name + if "." not in name and name not in logger_list ] - source_logger.debug(f"Found registered root loggers: {root_loggers}") + + +def _find_root_loggers( + source_logger: PowertoolsLogger, exclude: Optional[List[str]] = None, include: Optional[List[str]] = None +) -> list[logging.Logger]: + """Filter root loggers based on provided parameters. + + Ensure powertools logger itself is excluded from final list. + """ + root_loggers = [] if include and not exclude: - root_loggers = [logger for logger in root_loggers if logger.name in include] - elif not include and exclude: - root_loggers = [logger for logger in root_loggers if logger.name not in exclude] + root_loggers = _include_root_loggers_filter(logger_list=include) elif include and exclude: - root_loggers = [logger for logger in root_loggers if logger.name in include and logger.name not in exclude] + exclude = [source_logger.name, *exclude] + root_loggers = _include_root_loggers_filter(logger_list=list(set(include) - set(exclude))) + elif not include and exclude: + exclude = [source_logger.name, *exclude] + root_loggers = _exclude_root_loggers_filter(logger_list=exclude) + else: + root_loggers = _exclude_root_loggers_filter(logger_list=[source_logger.name]) source_logger.debug(f"Filtered root loggers: {root_loggers}") - for logger in root_loggers: - logger.handlers = [] - logger.setLevel(source_logger.level) - for source_handler in source_logger.handlers: - logger.addHandler(source_handler) + return root_loggers + + +def _configure_logger(source_logger: PowertoolsLogger, logger: logging.Logger) -> None: + logger.handlers = [] + logger.setLevel(source_logger.level) + source_logger.debug(f"Logger {logger} reconfigured to use logging level {source_logger.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/tests/functional/test_logger_utils.py b/tests/functional/test_logger_utils.py index 7b1e8a30f99..b7f67118035 100644 --- a/tests/functional/test_logger_utils.py +++ b/tests/functional/test_logger_utils.py @@ -25,15 +25,6 @@ class LogLevel(Enum): return LogLevel -def capture_logging_output(stdout): - return json.loads(stdout.getvalue().strip()) - - -def service_name(): - chars = string.ascii_letters + string.digits - return "".join(random.SystemRandom().choice(chars) for _ in range(15)) - - @pytest.fixture def logger(stdout, log_level): def _logger(): @@ -44,30 +35,43 @@ def _logger(): return _logger +def capture_logging_output(stdout): + return json.loads(stdout.getvalue().strip()) + + +def capture_multiple_logging_statements_output(stdout): + return [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line] + + +def service_name(): + chars = string.ascii_letters + string.digits + return "".join(random.SystemRandom().choice(chars) for _ in range(15)) + + def test_copy_config_to_ext_loggers(stdout, logger, log_level): msg = "test message" # GIVEN a external logger and powertools logger initialized - logger = logger() - logger_initial_handlers = logger.handlers.copy() - logger_initial_level = logger.level + logger_1 = logger() + logger_2 = logger() + powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used utils.copy_config_to_registered_loggers(source_logger=powertools_logger) - logger.info(msg) - log = capture_logging_output(stdout) + logger_1.info(msg) + logger_2.info(msg) + logs = capture_multiple_logging_statements_output(stdout) # THEN - assert not logger_initial_handlers - assert logger_initial_level == log_level.NOTSET.value - assert len(logger.handlers) == 1 - assert type(logger.handlers[0]) is logging.StreamHandler - assert type(logger.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter - assert logger.level == log_level.INFO.value - assert log["message"] == msg - assert log["level"] == log_level.INFO.name + for index, logger in enumerate([logger_1, logger_2]): + assert len(logger.handlers) == 1 + assert type(logger.handlers[0]) is logging.StreamHandler + assert type(logger.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter + assert logger.level == log_level.INFO.value + assert logs[index]["message"] == msg + assert logs[index]["level"] == log_level.INFO.name def test_copy_config_to_ext_loggers_include(stdout, logger, log_level): @@ -134,6 +138,7 @@ def test_copy_config_to_ext_loggers_include_exclude(stdout, logger, log_level): ) logger_2.info(msg) log = capture_logging_output(stdout) + # THEN assert not logger_1.handlers assert len(logger_2.handlers) == 1 @@ -142,3 +147,20 @@ def test_copy_config_to_ext_loggers_include_exclude(stdout, logger, log_level): assert logger_2.level == log_level.INFO.value assert log["message"] == msg assert log["level"] == log_level.INFO.name + + +def test_copy_config_to_ext_loggers_clean_old_handlers(stdout, logger, log_level): + + # GIVEN a external logger with handler and powertools logger initialized + logger = logger() + handler = logging.FileHandler("logfile") + logger.addHandler(handler) + powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) + + # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used + utils.copy_config_to_registered_loggers(source_logger=powertools_logger) + + # THEN + assert len(logger.handlers) == 1 + assert type(logger.handlers[0]) is logging.StreamHandler + assert type(logger.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter From bc290184fd545624d69af7ad9254eed17c95ce61 Mon Sep 17 00:00:00 2001 From: Michal Ploski Date: Fri, 24 Dec 2021 10:42:11 +0100 Subject: [PATCH 3/7] Rename root logger to registered logger to avoid naming collision with real root logger --- aws_lambda_powertools/logging/utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/aws_lambda_powertools/logging/utils.py b/aws_lambda_powertools/logging/utils.py index d21a1d28864..4af42874bba 100644 --- a/aws_lambda_powertools/logging/utils.py +++ b/aws_lambda_powertools/logging/utils.py @@ -14,18 +14,18 @@ def copy_config_to_registered_loggers( Attach source logger handlers to external loggers. Modify logger level based on source logger attribute. """ - root_loggers = _find_root_loggers(source_logger, exclude, include) - for logger in root_loggers: + registered_loggers = _find_registered_loggers(source_logger, exclude, include) + for logger in registered_loggers: _configure_logger(source_logger, logger) -def _include_root_loggers_filter(logger_list: List[str]): +def _include_registered_loggers_filter(logger_list: List[str]): return [ logging.getLogger(name) for name in logging.root.manager.loggerDict if "." not in name and name in logger_list ] -def _exclude_root_loggers_filter(logger_list: List[str]) -> List[logging.Logger]: +def _exclude_registered_loggers_filter(logger_list: List[str]) -> List[logging.Logger]: return [ logging.getLogger(name) for name in logging.root.manager.loggerDict @@ -33,24 +33,24 @@ def _exclude_root_loggers_filter(logger_list: List[str]) -> List[logging.Logger] ] -def _find_root_loggers( +def _find_registered_loggers( source_logger: PowertoolsLogger, exclude: Optional[List[str]] = None, include: Optional[List[str]] = None -) -> list[logging.Logger]: +) -> List[logging.Logger]: """Filter root loggers based on provided parameters. Ensure powertools logger itself is excluded from final list. """ root_loggers = [] if include and not exclude: - root_loggers = _include_root_loggers_filter(logger_list=include) + root_loggers = _include_registered_loggers_filter(logger_list=include) elif include and exclude: exclude = [source_logger.name, *exclude] - root_loggers = _include_root_loggers_filter(logger_list=list(set(include) - set(exclude))) + root_loggers = _include_registered_loggers_filter(logger_list=list(set(include) - set(exclude))) elif not include and exclude: exclude = [source_logger.name, *exclude] - root_loggers = _exclude_root_loggers_filter(logger_list=exclude) + root_loggers = _exclude_registered_loggers_filter(logger_list=exclude) else: - root_loggers = _exclude_root_loggers_filter(logger_list=[source_logger.name]) + root_loggers = _exclude_registered_loggers_filter(logger_list=[source_logger.name]) source_logger.debug(f"Filtered root loggers: {root_loggers}") return root_loggers From 8059b58063e2ce15bad40324110b48b0cdb892d0 Mon Sep 17 00:00:00 2001 From: Michal Ploski Date: Fri, 24 Dec 2021 12:16:56 +0100 Subject: [PATCH 4/7] Refactor code to lower cohesion --- aws_lambda_powertools/logging/utils.py | 56 +++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/aws_lambda_powertools/logging/utils.py b/aws_lambda_powertools/logging/utils.py index 4af42874bba..911bb4e8159 100644 --- a/aws_lambda_powertools/logging/utils.py +++ b/aws_lambda_powertools/logging/utils.py @@ -1,5 +1,5 @@ import logging -from typing import List, Optional, TypeVar +from typing import Callable, List, Optional, TypeVar from .logger import Logger @@ -7,51 +7,51 @@ def copy_config_to_registered_loggers( - source_logger: PowertoolsLogger, exclude: Optional[List[str]] = None, include: Optional[List[str]] = None + source_logger: PowertoolsLogger, + exclude: Optional[List[str]] = None, + include: Optional[List[str]] = None, ) -> None: """Enable powertools logging for imported libraries. Attach source logger handlers to external loggers. Modify logger level based on source logger attribute. + Ensure powertools logger itself is excluded from registered list. """ - registered_loggers = _find_registered_loggers(source_logger, exclude, include) + + if include and not exclude: + loggers = include + filter_func = _include_registered_loggers_filter + elif include and exclude: + exclude = [source_logger.name, *exclude] + loggers = list(set(include) - set(exclude)) + filter_func = _include_registered_loggers_filter + elif not include and exclude: + loggers = [source_logger.name, *exclude] + filter_func = _exclude_registered_loggers_filter + else: + loggers = [source_logger.name] + filter_func = _exclude_registered_loggers_filter + + registered_loggers = _find_registered_loggers(source_logger, loggers, filter_func) for logger in registered_loggers: _configure_logger(source_logger, logger) -def _include_registered_loggers_filter(logger_list: List[str]): - return [ - logging.getLogger(name) for name in logging.root.manager.loggerDict if "." not in name and name in logger_list - ] +def _include_registered_loggers_filter(loggers: List[str]): + return [logging.getLogger(name) for name in logging.root.manager.loggerDict if "." not in name and name in loggers] -def _exclude_registered_loggers_filter(logger_list: List[str]) -> List[logging.Logger]: +def _exclude_registered_loggers_filter(loggers: List[str]) -> List[logging.Logger]: return [ - logging.getLogger(name) - for name in logging.root.manager.loggerDict - if "." not in name and name not in logger_list + logging.getLogger(name) for name in logging.root.manager.loggerDict if "." not in name and name not in loggers ] def _find_registered_loggers( - source_logger: PowertoolsLogger, exclude: Optional[List[str]] = None, include: Optional[List[str]] = None + source_logger: PowertoolsLogger, loggers: List[str], filter_func: Callable ) -> List[logging.Logger]: - """Filter root loggers based on provided parameters. - - Ensure powertools logger itself is excluded from final list. - """ - root_loggers = [] - if include and not exclude: - root_loggers = _include_registered_loggers_filter(logger_list=include) - elif include and exclude: - exclude = [source_logger.name, *exclude] - root_loggers = _include_registered_loggers_filter(logger_list=list(set(include) - set(exclude))) - elif not include and exclude: - exclude = [source_logger.name, *exclude] - root_loggers = _exclude_registered_loggers_filter(logger_list=exclude) - else: - root_loggers = _exclude_registered_loggers_filter(logger_list=[source_logger.name]) - + """Filter root loggers based on provided parameters.""" + root_loggers = filter_func(loggers) source_logger.debug(f"Filtered root loggers: {root_loggers}") return root_loggers From 36420a2b7d10a37d772389562a3ac3c76fea92f7 Mon Sep 17 00:00:00 2001 From: Michal Ploski Date: Wed, 5 Jan 2022 11:35:48 +0100 Subject: [PATCH 5/7] Add custom log level atrribute --- aws_lambda_powertools/logging/utils.py | 70 ++++++++++++++++---------- tests/functional/test_logger_utils.py | 35 +++++++++++-- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/aws_lambda_powertools/logging/utils.py b/aws_lambda_powertools/logging/utils.py index 911bb4e8159..f0e39ddf8f0 100644 --- a/aws_lambda_powertools/logging/utils.py +++ b/aws_lambda_powertools/logging/utils.py @@ -1,54 +1,70 @@ import logging -from typing import Callable, List, Optional, TypeVar +from typing import Callable, List, Optional, Set, Union from .logger import Logger -PowertoolsLogger = TypeVar("PowertoolsLogger", bound=Logger) - def copy_config_to_registered_loggers( - source_logger: PowertoolsLogger, - exclude: Optional[List[str]] = None, - include: Optional[List[str]] = None, + source_logger: Logger, + log_level: Optional[str] = None, + exclude: Optional[Set[str]] = None, + include: Optional[Set[str]] = None, ) -> None: - """Enable powertools logging for imported libraries. - Attach source logger handlers to external loggers. - Modify logger level based on source logger attribute. - Ensure powertools logger itself is excluded from registered list. + """Copies source Logger level and handler to all registered loggers for consistent formatting. + + Parameters + ---------- + source_logger : Logger + Powertools Logger to copy configuration from + log_level : str, optional + Logging level to set to registered loggers, by default uses source_logger logging level + include : Optional[Set[str]], optional + List of logger names to include, by default all registered loggers are included + exclude : Optional[Set[str]], optional + List of logger names to exclude, by default None """ - if include and not exclude: - loggers = include - filter_func = _include_registered_loggers_filter - elif include and exclude: - exclude = [source_logger.name, *exclude] - loggers = list(set(include) - set(exclude)) + level = log_level or source_logger.level + + # Assumptions: Only take parent loggers not children (dot notation rule) + # Steps: + # 1. Default operation: Include all registered loggers + # 2. Only include set? Only add Loggers in the list and ignore all else + # 3. Include and exclude set? Add Logger if it’s in include and not in exclude + # 4. Only exclude set? Ignore Logger in the excluding list + + # Exclude source logger by default + if exclude: + exclude.add(source_logger.name) + else: + exclude = set(source_logger.name) + + # Prepare loggers set + if include: + loggers = include.difference(exclude) filter_func = _include_registered_loggers_filter - elif not include and exclude: - loggers = [source_logger.name, *exclude] - filter_func = _exclude_registered_loggers_filter else: - loggers = [source_logger.name] + loggers = exclude filter_func = _exclude_registered_loggers_filter registered_loggers = _find_registered_loggers(source_logger, loggers, filter_func) for logger in registered_loggers: - _configure_logger(source_logger, logger) + _configure_logger(source_logger, logger, level) -def _include_registered_loggers_filter(loggers: List[str]): +def _include_registered_loggers_filter(loggers: Set[str]): return [logging.getLogger(name) for name in logging.root.manager.loggerDict if "." not in name and name in loggers] -def _exclude_registered_loggers_filter(loggers: List[str]) -> List[logging.Logger]: +def _exclude_registered_loggers_filter(loggers: Set[str]) -> List[logging.Logger]: return [ logging.getLogger(name) for name in logging.root.manager.loggerDict if "." not in name and name not in loggers ] def _find_registered_loggers( - source_logger: PowertoolsLogger, loggers: List[str], filter_func: Callable + source_logger: Logger, loggers: Set[str], filter_func: Callable[[Set[str]], List[logging.Logger]] ) -> List[logging.Logger]: """Filter root loggers based on provided parameters.""" root_loggers = filter_func(loggers) @@ -56,10 +72,10 @@ def _find_registered_loggers( return root_loggers -def _configure_logger(source_logger: PowertoolsLogger, logger: logging.Logger) -> None: +def _configure_logger(source_logger: Logger, logger: logging.Logger, level: Union[int, str]) -> None: logger.handlers = [] - logger.setLevel(source_logger.level) - source_logger.debug(f"Logger {logger} reconfigured to use logging level {source_logger.level}") + logger.setLevel(level) + 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/tests/functional/test_logger_utils.py b/tests/functional/test_logger_utils.py index b7f67118035..1317fefc6ab 100644 --- a/tests/functional/test_logger_utils.py +++ b/tests/functional/test_logger_utils.py @@ -21,6 +21,8 @@ def log_level(): class LogLevel(Enum): NOTSET = 0 INFO = 20 + WARNING = 30 + CRITICAL = 50 return LogLevel @@ -83,7 +85,7 @@ def test_copy_config_to_ext_loggers_include(stdout, logger, log_level): powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used - utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include=[logger.name]) + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include={logger.name}) logger.info(msg) log = capture_logging_output(stdout) @@ -103,7 +105,7 @@ def test_copy_config_to_ext_loggers_wrong_include(stdout, logger, log_level): powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used - utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include=["non-existing-logger"]) + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include={"non-existing-logger"}) # THEN assert not logger.handlers @@ -116,7 +118,7 @@ def test_copy_config_to_ext_loggers_exclude(stdout, logger, log_level): powertools_logger = Logger(service=service_name(), level=log_level.INFO.value, stream=stdout) # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used - utils.copy_config_to_registered_loggers(source_logger=powertools_logger, exclude=[logger.name]) + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, exclude={logger.name}) # THEN assert not logger.handlers @@ -134,7 +136,7 @@ def test_copy_config_to_ext_loggers_include_exclude(stdout, logger, log_level): # WHEN configuration copied from powertools logger to ALL external loggers AND our external logger used utils.copy_config_to_registered_loggers( - source_logger=powertools_logger, include=[logger_1.name, logger_2.name], exclude=[logger_1.name] + source_logger=powertools_logger, include={logger_1.name, logger_2.name}, exclude={logger_1.name} ) logger_2.info(msg) log = capture_logging_output(stdout) @@ -164,3 +166,28 @@ def test_copy_config_to_ext_loggers_clean_old_handlers(stdout, logger, log_level assert len(logger.handlers) == 1 assert type(logger.handlers[0]) is logging.StreamHandler assert type(logger.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter + + +def test_copy_config_to_ext_loggers_custom_log_level(stdout, logger, log_level): + + msg = "test message" + + # GIVEN a external logger and powertools logger initialized + logger = logger() + powertools_logger = Logger(service=service_name(), level=log_level.CRITICAL.value, stream=stdout) + level = log_level.WARNING.name + + # WHEN configuration copied from powertools logger to ALL external loggers + # AND our external logger used with custom log_level + utils.copy_config_to_registered_loggers(source_logger=powertools_logger, include={logger.name}, log_level=level) + logger.warning(msg) + log = capture_logging_output(stdout) + + # THEN + assert len(logger.handlers) == 1 + assert type(logger.handlers[0]) is logging.StreamHandler + assert type(logger.handlers[0].formatter) is formatter.LambdaPowertoolsFormatter + assert powertools_logger.level == log_level.CRITICAL.value + assert logger.level == log_level.WARNING.value + assert log["message"] == msg + assert log["level"] == log_level.WARNING.name From 19b6e2d09178bb2da36d65cc306080ec12c1f5f6 Mon Sep 17 00:00:00 2001 From: Michal Ploski Date: Wed, 5 Jan 2022 11:55:33 +0100 Subject: [PATCH 6/7] Add doc for new method --- docs/core/logger.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/core/logger.md b/docs/core/logger.md index 833d5a5c721..be0a0690e19 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -1062,6 +1062,26 @@ for the given name and level to the logging module. By default, this logs all bo return response.get("Buckets", []) ``` +**How can I enable powertools logging for imported libraries?** + +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. By default all registered loggers will be modified. You can change this behaviour by providing `include` and `exclude` attributes. You can also provide optional `log_level` attribute external loggers will be configured with. + + +=== "structured_logging_external_loggers" + ```python hl_lines="10" + import logging + + from aws_lambda_powertools import Logger + from aws_lambda_powertools.logging import utils + + logger = Logger() + + external_logger = logging.logger() + + utils.copy_config_to_registered_loggers(source_logger=logger) + external_logger.info("test message") + ``` + **What's the difference between `append_keys` and `extra`?** Keys added with `append_keys` will persist across multiple log messages while keys added via `extra` will only be available in a given log message operation. From 2e8f9969c9d214c72c5f06146de5d9a7b96a7726 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Wed, 5 Jan 2022 14:03:31 +0100 Subject: [PATCH 7/7] docs: adhere to new snippet format --- docs/core/logger.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/core/logger.md b/docs/core/logger.md index be0a0690e19..00937657823 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -1067,20 +1067,18 @@ 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. By default all registered loggers will be modified. You can change this behaviour by providing `include` and `exclude` attributes. You can also provide optional `log_level` attribute external loggers will be configured with. -=== "structured_logging_external_loggers" - ```python hl_lines="10" - import logging +```python hl_lines="10" title="Cloning Logger config to all other registered standard loggers" +import logging - from aws_lambda_powertools import Logger - from aws_lambda_powertools.logging import utils +from aws_lambda_powertools import Logger +from aws_lambda_powertools.logging import utils - logger = Logger() +logger = Logger() - external_logger = logging.logger() +external_logger = logging.logger() - utils.copy_config_to_registered_loggers(source_logger=logger) - external_logger.info("test message") - ``` +utils.copy_config_to_registered_loggers(source_logger=logger) +external_logger.info("test message") **What's the difference between `append_keys` and `extra`?**