I have been going around in circles on this problem for several days now and I am no closer to a solution than when I started.
I have reviewed all of the other stackoverflow entries dealing with the pytest caplog fixture and I have narrowed my problem down to the use of logging.config.dictConfig()
I have tried multiple configurations, with and without propagate=True
, and they all result in the same problem ... logging is not captured when using dictConfig()
.
Pytest logging when used in conjunction with config.dictConfig()
is broken.
Here's my test code which demonstrates the problem:
# =====================
# File: test_caplog.py
# =====================
class TestCapLog:
def _test_logger(self, tf_caplog):
"""Display caplog capture text"""
# display capture log
print("\nCAPLOG:")
output = tf_caplog.text.rstrip('\n').split(sep='\n')
if output == ['']:
print("Nothing captured")
else:
for i in range(len(output)):
print(f'{i}: {output[i]}')
def test_caplog0_root(self, caplog):
"""Test caplog 'root' logger w/o dictConfig()"""
import logging
# use logging configuration "as-is"
logger = logging.getLogger()
# log at all logging levels
logger.debug('DEBUG: log entry captured')
logger.info('INFO: log entry captured')
logger.error('ERROR: log entry captured')
logger.warning('WARNING: log entry captured')
self._test_logger(caplog)
def test_caplog1_main1(self, caplog):
"""Test caplog 'main' logger w/ dictConfig(), propagate=False"""
import logging.config
import logging
import log_config
# configure logging, propagate False
log_config.LOGGING['loggers']['main']['propagate'] = False
logging.config.dictConfig(log_config.LOGGING)
logger = logging.getLogger(name='main')
# log at all logging levels
logger.debug('DEBUG: log entry captured')
logger.info('INFO: log entry captured')
logger.error('ERROR: log entry captured')
logger.warning('WARNING: log entry captured')
self._test_logger(caplog)
def test_caplog1_main2(self, caplog):
"""Test caplog 'main' logger w/ dictConfig(), propagate=True"""
import logging.config
import logging
import log_config
# configure logging, propagate True
log_config.LOGGING['loggers']['main']['propagate'] = True
logging.config.dictConfig(log_config.LOGGING)
logger = logging.getLogger(name='main')
# log at all logging levels
logger.debug('DEBUG: log entry captured')
logger.info('INFO: log entry captured')
logger.error('ERROR: log entry captured')
logger.warning('WARNING: log entry captured')
self._test_logger(caplog)
Here's the logging configuration file
# =====================
# File: log_config.py
# =====================
"""logging configuration support"""
# System imports
import logging.handlers
import sys
#: logging formatters
_formatters = {
'msgonly': {
'format': '%(message)s'
},
'minimal': {
'format': '(%(name)s) %(message)s'
},
'normal': {
'format': '%(asctime)s (%(name)s) %(levelname)s %(message)s'
},
'debug': {
'format': '%(asctime)s (%(name)s) %(levelname)s %(module)s %(funcName)s %(message)s'
}
}
#: logging stream handler string
LOGGING_STREAM_HANDLER = 'logging.StreamHandler'
#: logging timed file handler string
LOGGING_TIMED_FILE_HANDLER = 'logging.handlers.TimedRotatingFileHandler'
#: logging handlers
_handlers = {
'debugHandler': {
'class': LOGGING_STREAM_HANDLER,
'level': logging.DEBUG,
'formatter': 'debug',
'stream': sys.stdout,
},
'consoleHandler': {
'class': LOGGING_STREAM_HANDLER,
'level': logging.DEBUG,
'formatter': 'normal',
'stream': sys.stdout,
},
'fileHandler': {
'class': LOGGING_TIMED_FILE_HANDLER,
'level': logging.DEBUG,
'formatter': 'normal',
'filename': 'logging.log',
'when': 'D',
'interval': 1,
'backupCount': 7,
'delay': True,
},
}
#: Loggers
_loggers = {
'': {
'level': logging.INFO,
'handlers': ['consoleHandler', 'fileHandler'],
'qualname': 'root',
'propagate': False,
},
'root': {
'level': logging.DEBUG,
'handlers': ['debugHandler', 'fileHandler'],
'qualname': 'root',
'propagate': False,
},
'__main__': {
'level': logging.DEBUG,
'handlers': ['debugHandler', 'fileHandler'],
'qualname': '__main__',
'propagate': False,
},
'main': {
'level': logging.DEBUG,
'handlers': ['debugHandler', 'fileHandler'],
'qualname': 'main',
'propagate': False,
},
}
#: Configuration dictionary
LOGGING = {
"version": 1,
"loggers": _loggers,
"handlers": _handlers,
"formatters": _formatters,
}
The 3 tests that I run are:
- logging using the
root
logger with no call todictConfig()
- logging using named logger (
main
) with call todictConfig()
andpropagate=False
- logging using named logger (
main
) with call todictConfig()
andpropagate=True
What follows is the output of executing my test code:
/home/mark/PycharmProjects/pytest_caplog/venv/bin/python /home/mark/.local/share/JetBrains/pycharm-2022.2.2/plugins/python/helpers/pycharm/_jb_pytest_runner.py --path /home/mark/PycharmProjects/pytest_caplog/test_caplog.py
Testing started at 1:09 AM ...
Launching pytest with arguments /home/mark/PycharmProjects/pytest_caplog/test_caplog.py --no-header --no-summary -q in /home/mark/PycharmProjects/pytest_caplog
============================= test session starts ==============================
collecting ... collected 3 items
test_caplog.py::TestCapLog::test_caplog0_root PASSED [ 33%]
CAPLOG:
0: ERROR root:test_caplog.py:23 ERROR: log entry captured
1: WARNING root:test_caplog.py:24 WARNING: log entry captured
test_caplog.py::TestCapLog::test_caplog1_main1 PASSED [ 66%]2022-12-22 01:09:28,810 (main) DEBUG test_caplog test_caplog1_main1 DEBUG: log entry captured
2022-12-22 01:09:28,810 (main) INFO test_caplog test_caplog1_main1 INFO: log entry captured
2022-12-22 01:09:28,810 (main) ERROR test_caplog test_caplog1_main1 ERROR: log entry captured
2022-12-22 01:09:28,811 (main) WARNING test_caplog test_caplog1_main1 WARNING: log entry captured
CAPLOG:
Nothing captured
test_caplog.py::TestCapLog::test_caplog1_main2 PASSED [100%]2022-12-22 01:09:28,815 (main) DEBUG test_caplog test_caplog1_main2 DEBUG: log entry captured
2022-12-22 01:09:28,815 (main) DEBUG DEBUG: log entry captured
2022-12-22 01:09:28,815 (main) INFO test_caplog test_caplog1_main2 INFO: log entry captured
2022-12-22 01:09:28,815 (main) INFO INFO: log entry captured
2022-12-22 01:09:28,815 (main) ERROR test_caplog test_caplog1_main2 ERROR: log entry captured
2022-12-22 01:09:28,815 (main) ERROR ERROR: log entry captured
2022-12-22 01:09:28,816 (main) WARNING test_caplog test_caplog1_main2 WARNING: log entry captured
2022-12-22 01:09:28,816 (main) WARNING WARNING: log entry captured
CAPLOG:
Nothing captured
============================== 3 passed in 0.03s ===============================
Process finished with exit code 0
The only way that I have been able to get caplog to behave as I would expect it to behave is to not use dictConfig()
and write my own get_logger()
function.
That seems like a waste and would not be necessary if the pytest caplog
fixture would respect the dictConfig()
settings.
I have read through the pytest documentation and none of the caplog
examples that I have found address using anything other than the root
logger.
At this point I am reconsidering my decision to switch from the standard Python unittest
capability to pytest
This is a major blocker for me.
Any help that anyone can give me will be greatly appreciated.
I am not sure if this solution acceptable for you but you can have a look. Seems like you need to overwrite
logging-plugin
caplog_handler
property that is used bycaplog
fixture afterdictConfig
call.You can write your own fixture that sets config and overwrites
caplog_handler
property oflogging-plugin
instance with yourLogCaptureHandler
that is described in config. Also this handler must be specified with loggers that needs it.Also keep in mind that your logging config must not be reconfigured during tests with
logging.config.dictConfig(log_config.LOGGING)
because it will cause recreation of handlers.So logging configuration will be done only with your
logging_config
fixture.To change config before test you can use indirect parametrization. Example of changing
propagate
inmain
logger in 3rd test:Also you can rewrite fixture to use mergedeep to merge your initial config (
LOGGING
) withrequest.param
just to avoid defining and passing whole config before@pytest.mark.parametrize
.