pytest: selective log levels on a per-module basis
Asked Answered
D

3

18

I'm using pytest-3.7.1 which has good support for logging, including live logging to stdout during tests. I'm using --log-cli-level=DEBUG to dump all debug-level logging to the console as it happens.

The problem I have is that --log-cli-level=DEBUG turns on debug logging for all modules in my test program, including third-party dependencies, and it floods the log with a lot of uninteresting output.

Python's logging module has the ability to set logging levels per module. This enables selective logging - for example, in a normal Python program I can turn on debugging for just one or two of my own modules, and restrict the log output to just those, or set different log levels for each module. This enables turning off debug-level logging for noisy libraries.

So what I'd like to do is apply the same concept to pytest's logging - i.e. specify a logging level, from the command line, for specific non-root loggers. For example, if I have a module called test_foo.py then I'm looking for a way to set the log level for this module from the command line.

I'm prepared to roll-my-own if necessary (I know how to add custom arguments to pytest), but before I do that I just want to be sure that there isn't already a solution. Is anyone aware of one?

Diapositive answered 29/8, 2018 at 22:34 Comment(3)
Did you ever find a solution/configuration here? Same issue :(Seton
@Liz Unfortunately not - however as I allude to in my last paragraph, in a previous project I did add my own custom command-line option that selectively turned on debugging for specific components.Diapositive
@Dan D. The alleged "duplicate" is in fact a different question from what I asked. The answer to the linked question does not answer my question.Diapositive
G
1

I had the same problem, and found a solution in another answer:

Instead of --log-cli-level=DEBUG, use --log-level DEBUG. It disables all third-party module logs (in my case, I had plenty of matplotlib logs), but still outputs your app logs for each test that fails.

Grube answered 25/9, 2019 at 19:58 Comment(1)
Thanks, that is useful, although that's more of a workaround for the issue I described since it doesn't address the per-module enable/disable feature, which is a useful feature in-and-of itself, I think.Diapositive
S
1

Enable/Disable/Modify the log level of any module in Python:

logging.getLogger("module_name").setLevel(logging.log_level)
Satisfied answered 21/2, 2023 at 16:18 Comment(0)
S
0

I got this working by writing a factory class and using it to set the level of the root logger to logger.INFO and use the logging level from the command line for all the loggers obtained from the factory. If the logging level from the command line is higher than the minimum global log level you specify in the class (using constant MINIMUM_GLOBAL_LOG_LEVEL), the global log level isn't changed.

import logging

MODULE_FIELD_WIDTH_IN_CHARS = '20'
LINE_NO_FIELD_WIDTH_IN_CHARS = '3'
LEVEL_NAME_FIELD_WIDTH_IN_CHARS = '8'
MINIMUM_GLOBAL_LOG_LEVEL = logging.INFO

class EasyLogger():
    root_logger = logging.getLogger()
    specified_log_level = root_logger.level
    format_string = '{asctime} '
    format_string += '{module:>' + MODULE_FIELD_WIDTH_IN_CHARS + 's}'
    format_string += '[{lineno:' + LINE_NO_FIELD_WIDTH_IN_CHARS + 'd}]'
    format_string += '[{levelname:^' + LEVEL_NAME_FIELD_WIDTH_IN_CHARS + 's}]: '
    format_string += '{message}'
    level_change_warning_sent = False

    @classmethod
    def get_logger(cls, logger_name):
        if not EasyLogger._logger_has_format(cls.root_logger, cls.format_string):
            EasyLogger._setup_root_logger()
        logger = logging.getLogger(logger_name)
        logger.setLevel(cls.specified_log_level)
        return logger

    @classmethod
    def _setup_root_logger(cls):
        formatter = logging.Formatter(fmt=cls.format_string, style='{')
        if not cls.root_logger.hasHandlers():
            handler = logging.StreamHandler()
            cls.root_logger.addHandler(handler)
        for handler in cls.root_logger.handlers:
            handler.setFormatter(formatter)

        cls.root_logger.setLevel(MINIMUM_GLOBAL_LOG_LEVEL)
        if (cls.specified_log_level < MINIMUM_GLOBAL_LOG_LEVEL and
            cls.level_change_warning_sent is False):
            cls.root_logger.log(
                max(cls.specified_log_level, logging.WARNING),
                "Setting log level for %s class to %s, all others to %s" % (
                    __name__,
                    cls.specified_log_level,
                    MINIMUM_GLOBAL_LOG_LEVEL
                )
            )
            cls.level_change_warning_sent = True

    @staticmethod
    def _logger_has_format(logger, format_string):
        for handler in logger.handlers:
            return handler.format == format_string
        return False

The above class is then used to send logs normally as you would with a logging.logger object as follows:

from EasyLogger import EasyLogger

class MySuperAwesomeClass():
    def __init__(self):
        self.logger = EasyLogger.get_logger(__name__)

    def foo(self):
        self.logger.debug("debug message")
        self.logger.info("info message")
        self.logger.warning("warning message")
        self.logger.critical("critical message")
        self.logger.error("error message")

Supremacy answered 14/1, 2022 at 20:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.