davidA
davidA

Reputation: 13664

pytest: selective log levels on a per-module basis

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?

Upvotes: 20

Views: 2272

Answers (4)

xpkr
xpkr

Reputation: 51

I had the same problem and solved it by setting the log levels in conftest.py. Since the logging module is a singleton, this will then have a global impact for the the whole pytest run.

in conftest.py

import logging
...
# Set sensible log levels for sub and third-party modules
logging.getLogger("module1").setLevel(logging.INFO)
logging.getLogger("some.external.module").setLevel(logging.WARNING)

...

You can then use custom cli args to modify the log levels yourself.

Upvotes: 1

gagarwa
gagarwa

Reputation: 1492

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

logging.getLogger("module_name").setLevel(logging.log_level)

Upvotes: 2

user14919794
user14919794

Reputation: 36

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")

Upvotes: 0

Dennis Golomazov
Dennis Golomazov

Reputation: 17339

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.

Upvotes: 1

Related Questions