Skip to content

Commit 29adc16

Browse files
committed
bug: improve error reporting for errors for logging fileConfig.
1 parent 43eb594 commit 29adc16

File tree

3 files changed

+266
-5
lines changed

3 files changed

+266
-5
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Fixed:
2929
- in roundup-admin, using 'pragma history_length interactively now
3030
sets readline history length. Using -P history_length=10 on the
3131
command line always worked. (John Rouillard)
32+
- enhanced error reporting for errors in ini style logging
33+
configuration. (John Rouillard)
3234

3335
Features:
3436

roundup/configuration.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2634,9 +2634,36 @@ def init_logging(self):
26342634
_file = self["LOGGING_CONFIG"]
26352635
if _file and os.path.isfile(_file):
26362636
if _file.endswith(".ini"):
2637-
logging.config.fileConfig(
2638-
_file,
2639-
disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"])
2637+
try:
2638+
logging.config.fileConfig(
2639+
_file,
2640+
disable_existing_loggers=self[
2641+
"LOGGING_DISABLE_LOGGERS"])
2642+
except (ValueError, RuntimeError,
2643+
KeyError, NameError, ModuleNotFoundError) as e:
2644+
# configparser.DuplicateOptionError includes
2645+
# filename, line number and a useful error.
2646+
# so we don't have to augment it.
2647+
context = []
2648+
if hasattr(e, '__context__') and e.__context__:
2649+
# get additional error info.
2650+
context.append(str(e.__context__))
2651+
if hasattr(e, '__doc__') and e.__doc__:
2652+
context.append(e.__doc__)
2653+
2654+
if isinstance(e, KeyError):
2655+
context.append("No section found with this name.")
2656+
if not context:
2657+
context = ["No additional information available."]
2658+
2659+
raise LoggingConfigError(
2660+
"Error loading logging config from %(file)s.\n\n"
2661+
" %(msg)s\n\n%(context)s\n" % {
2662+
"file": _file,
2663+
"msg": e.args[0],
2664+
"context": " ".join(context),
2665+
}
2666+
)
26402667
elif _file.endswith(".json"):
26412668
config_dict = self.load_config_dict_from_json_file(_file)
26422669
try:

test/test_config.py

Lines changed: 234 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717

18+
import configparser
1819
import errno
1920
import fileinput
2021
import logging
@@ -424,8 +425,8 @@ def inject_fixtures(self, caplog):
424425

425426
@pytest.fixture(autouse=True)
426427
def save_restore_logging(self):
427-
"""Save logger state and try to restore it after all tests in
428-
this class have finished.
428+
"""Save logger state and try to restore it after each test
429+
has finished.
429430
430431
The primary test is testDictLoggerConfigViaJson which
431432
can change the loggers and break tests that depend on caplog
@@ -484,6 +485,18 @@ def save_restore_logging(self):
484485
logging.shutdown()
485486
reload(logging)
486487

488+
def reset_logging(self):
489+
"""https://til.tafkas.net/posts/-resetting-python-logging-before-running-tests/"""
490+
loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict]
491+
loggers.append(logging.getLogger())
492+
for logger in loggers:
493+
handlers = logger.handlers[:]
494+
for handler in handlers:
495+
logger.removeHandler(handler)
496+
handler.close()
497+
logger.setLevel(logging.NOTSET)
498+
logger.propagate = True
499+
487500
backend = 'anydbm'
488501

489502
def setUp(self):
@@ -1562,6 +1575,225 @@ def testDictLoggerConfigViaJson(self):
15621575
(log_config_filename,))
15631576
self.assertEqual(output, target)
15641577

1578+
def testIniFileLoggerConfig(self):
1579+
1580+
# good base test case
1581+
config1 = dedent("""
1582+
[loggers]
1583+
keys=root,roundup,roundup.http,roundup.hyperdb,actions,schema,extension,detector
1584+
1585+
[logger_root]
1586+
#DEBUG, INFO, WARNING, ERROR, CRITICAL
1587+
#also for root only NOTSET (all)
1588+
level=DEBUG
1589+
handlers=basic
1590+
1591+
[logger_roundup]
1592+
#DEBUG, INFO, WARNING, ERROR, CRITICAL
1593+
#also for root only NOTSET (all)
1594+
level=DEBUG
1595+
handlers=rotate
1596+
qualname=roundup
1597+
propagate=0
1598+
1599+
[logger_roundup.http]
1600+
level=INFO
1601+
handlers=rotate_weblog
1602+
qualname=roundup.http
1603+
propagate=0
1604+
1605+
[logger_roundup.hyperdb]
1606+
level=WARNING
1607+
handlers=rotate
1608+
qualname=roundup.hyperdb
1609+
propagate=0
1610+
1611+
[logger_actions]
1612+
#DEBUG, INFO, WARNING, ERROR, CRITICAL
1613+
#also for root only NOTSET (all)
1614+
level=DEBUG
1615+
handlers=rotate
1616+
qualname=actions
1617+
propagate=0
1618+
1619+
[logger_detector]
1620+
#DEBUG, INFO, WARNING, ERROR, CRITICAL
1621+
#also for root only NOTSET (all)
1622+
level=DEBUG
1623+
handlers=rotate
1624+
qualname=detector
1625+
propagate=0
1626+
1627+
[logger_schema]
1628+
level=DEBUG
1629+
handlers=rotate
1630+
qualname=schema
1631+
propagate=0
1632+
1633+
[logger_extension]
1634+
level=DEBUG
1635+
handlers=rotate
1636+
qualname=extension
1637+
propagate=0
1638+
1639+
[handlers]
1640+
keys=basic,rotate,rotate_weblog
1641+
1642+
[handler_basic]
1643+
class=StreamHandler
1644+
args=(sys.stderr,)
1645+
formatter=basic
1646+
1647+
[handler_rotate]
1648+
class=logging.handlers.RotatingFileHandler
1649+
args=('roundup.log','a', 512000, 2)
1650+
formatter=basic
1651+
1652+
[handler_rotate_weblog]
1653+
class=logging.handlers.RotatingFileHandler
1654+
args=('httpd.log','a', 512000, 2)
1655+
formatter=plain
1656+
1657+
[formatters]
1658+
keys=basic,plain
1659+
1660+
[formatter_basic]
1661+
format=%(asctime)s %(name)s:%(module)s.%(funcName)s,%(levelname)s: %(message)s
1662+
datefmt=%Y-%m-%d %H:%M:%S
1663+
1664+
[formatter_plain]
1665+
format=%(message)s
1666+
""")
1667+
1668+
log_config_filename = os.path.join(self.instance.tracker_home,
1669+
"_test_log_config.ini")
1670+
1671+
# happy path
1672+
with open(log_config_filename, "w") as log_config_file:
1673+
log_config_file.write(config1)
1674+
1675+
self.db.config.LOGGING_CONFIG = "_test_log_config.ini"
1676+
1677+
# verify we have a clean environment
1678+
self.assertEqual(len(logging.getLogger('roundup.http').handlers), 0)
1679+
1680+
# always returns None
1681+
self.db.config.init_logging()
1682+
1683+
# verify that logging loaded and handler is set
1684+
# default log config doesn't define handlers for roundup.http
1685+
self.assertEqual(len(logging.getLogger('roundup.http').handlers), 1)
1686+
self.reset_logging()
1687+
1688+
# use undefined enumeration
1689+
test_config = config1.replace("=DEBUG\n", "=DEBUF\n")
1690+
with open(log_config_filename, "w") as log_config_file:
1691+
log_config_file.write(test_config)
1692+
1693+
with self.assertRaises(configuration.LoggingConfigError) as cm:
1694+
config = self.db.config.init_logging()
1695+
1696+
# verify that logging was reset
1697+
# default log config doesn't define handlers for roundup.http
1698+
self.assertEqual(len(logging.getLogger('roundup.http').handlers), 0)
1699+
1700+
self.assertEqual(
1701+
cm.exception.args[0].replace(r'\\','\\'),
1702+
('Error loading logging config from %s.\n\n'
1703+
" Unknown level: 'DEBUF'\n\n"
1704+
'Inappropriate argument value (of correct type).\n' %
1705+
log_config_filename)
1706+
)
1707+
self.reset_logging()
1708+
1709+
1710+
# add a syntax error "= foo"
1711+
test_config = config1.replace("=DEBUG\n", "=DEBUG\n=foo\n", 1)
1712+
with open(log_config_filename, "w") as log_config_file:
1713+
log_config_file.write(test_config)
1714+
1715+
with self.assertRaises(configuration.LoggingConfigError) as cm:
1716+
config = self.db.config.init_logging()
1717+
1718+
# verify that logging was reset
1719+
# default log config doesn't define handlers for roundup.http
1720+
self.assertEqual(len(logging.getLogger('roundup.http').handlers), 0)
1721+
1722+
self.assertEqual(
1723+
cm.exception.args[0].replace(r'\\','\\'),
1724+
("Error loading logging config from %(filename)s.\n\n"
1725+
" %(filename)s is invalid: Source contains parsing errors: "
1726+
"'%(filename)s'\n\t[line 9]: '=foo\\n'\n\n"
1727+
"Source contains parsing errors: '%(filename)s'\n"
1728+
"\t[line 9]: '=foo\\n' Unspecified run-time error.\n" %
1729+
{"filename": log_config_filename})
1730+
)
1731+
self.reset_logging()
1732+
1733+
# handler = basic to handler = basi
1734+
test_config = config1.replace("handlers=basic\n", "handlers=basi\n", 1)
1735+
with open(log_config_filename, "w") as log_config_file:
1736+
log_config_file.write(test_config)
1737+
1738+
with self.assertRaises(configuration.LoggingConfigError) as cm:
1739+
config = self.db.config.init_logging()
1740+
1741+
# verify that logging was reset
1742+
# default log config doesn't define handlers for roundup.http
1743+
self.assertEqual(len(logging.getLogger('roundup.http').handlers), 0)
1744+
1745+
self.assertEqual(
1746+
cm.exception.args[0].replace(r'\\','\\'),
1747+
("Error loading logging config from %(filename)s.\n\n"
1748+
" basi\n\n"
1749+
"Mapping key not found. No section found with this name.\n" %
1750+
{"filename": log_config_filename})
1751+
)
1752+
self.reset_logging()
1753+
1754+
# Change class to missing class
1755+
test_config = config1.replace("class=StreamHandler\n",
1756+
"class=SHAndler\n", 1)
1757+
with open(log_config_filename, "w") as log_config_file:
1758+
log_config_file.write(test_config)
1759+
1760+
with self.assertRaises(configuration.LoggingConfigError) as cm:
1761+
config = self.db.config.init_logging()
1762+
1763+
# verify that logging was reset
1764+
# default log config doesn't define handlers for roundup.http
1765+
self.assertEqual(len(logging.getLogger('roundup.http').handlers), 0)
1766+
1767+
self.assertEqual(
1768+
cm.exception.args[0].replace(r'\\','\\'),
1769+
("Error loading logging config from %(filename)s.\n\n"
1770+
" No module named 'SHAndler'\n\n"
1771+
"name 'SHAndler' is not defined Module not found.\n" %
1772+
{"filename": log_config_filename})
1773+
)
1774+
self.reset_logging()
1775+
1776+
# remove section to cause duplicate option definition
1777+
test_config = config1.replace("[logger_roundup.http]\n",
1778+
"\n", 1)
1779+
with open(log_config_filename, "w") as log_config_file:
1780+
log_config_file.write(test_config)
1781+
1782+
with self.assertRaises(configparser.DuplicateOptionError) as cm:
1783+
config = self.db.config.init_logging()
1784+
1785+
# verify that logging was reset
1786+
# default log config doesn't define handlers for roundup.http
1787+
self.assertEqual(len(logging.getLogger('roundup.http').handlers), 0)
1788+
1789+
self.assertEqual(
1790+
str(cm.exception).replace(r'\\','\\'),
1791+
("While reading from '%(filename)s' [line 20]: "
1792+
"option 'level' in section 'logger_roundup' already exists" %
1793+
{"filename": log_config_filename})
1794+
)
1795+
self.reset_logging()
1796+
15651797
def test_missing_logging_config_file(self):
15661798
saved_config = self.db.config['LOGGING_CONFIG']
15671799

0 commit comments

Comments
 (0)