From 7fcf7d5f0eda9274bf1c6865b49924ce87e6b13e Mon Sep 17 00:00:00 2001 From: JustAnotherArchivist Date: Wed, 18 Dec 2019 13:58:43 +0000 Subject: [PATCH] Make logging configurable through config file --- http2irc.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/http2irc.py b/http2irc.py index 39d1ace..165aef1 100644 --- a/http2irc.py +++ b/http2irc.py @@ -8,13 +8,11 @@ import logging import os.path import signal import ssl +import string import sys import toml -logging.basicConfig(level = logging.DEBUG, format = '{asctime} {levelname} {message}', style = '{') - - SSL_CONTEXTS = {'yes': True, 'no': False, 'insecure': ssl.SSLContext()} @@ -56,10 +54,24 @@ class Config(dict): logging.info(repr(obj)) # Sanity checks - if any(x not in ('irc', 'web', 'maps') for x in obj.keys()): + if any(x not in ('logging', 'irc', 'web', 'maps') for x in obj.keys()): raise InvalidConfig('Unknown sections found in base object') if any(not isinstance(x, collections.abc.Mapping) for x in obj.values()): raise InvalidConfig('Invalid section type(s), expected objects/dicts') + if 'logging' in obj: + if any(x not in ('level', 'format') for x in obj['logging']): + raise InvalidConfig('Unknown key found in log section') + if 'level' in obj['logging'] and obj['logging']['level'] not in ('DEBUG', 'INFO', 'WARNING', 'ERROR'): + raise InvalidConfig('Invalid log level') + if 'format' in obj['logging']: + if not isinstance(obj['logging']['format'], str): + raise InvalidConfig('Invalid log format') + try: + #TODO: Replace with logging.Formatter's validate option (3.8+); this test does not cover everything that could be wrong (e.g. invalid format spec or conversion) + # This counts the number of replacement fields. Formatter.parse yields tuples whose second value is the field name; if it's None, there is no field (e.g. literal text). + assert sum(1 for x in string.Formatter().parse(obj['logging']['format']) if x[1] is not None) > 0 + except (ValueError, AssertionError) as e: + raise InvalidConfig('Invalid log format: parsing failed') from e if 'irc' in obj: if any(x not in ('host', 'port', 'ssl', 'nick', 'real', 'certfile', 'certkeyfile') for x in obj['irc']): raise InvalidConfig('Unknown key found in irc section') @@ -107,7 +119,7 @@ class Config(dict): #TODO: Check values # Default values - finalObj = {'irc': {'host': 'irc.hackint.org', 'port': 6697, 'ssl': 'yes', 'nick': 'h2ibot', 'real': 'I am an http2irc bot.', 'certfile': None, 'certkeyfile': None}, 'web': {'host': '127.0.0.1', 'port': 8080}, 'maps': {}} + finalObj = {'logging': {'level': 'INFO', 'format': '{asctime} {levelname} {message}'}, 'irc': {'host': 'irc.hackint.org', 'port': 6697, 'ssl': 'yes', 'nick': 'h2ibot', 'real': 'I am an http2irc bot.', 'certfile': None, 'certkeyfile': None}, 'web': {'host': '127.0.0.1', 'port': 8080}, 'maps': {}} # Fill in default values for the maps for key, map_ in obj['maps'].items(): @@ -119,13 +131,13 @@ class Config(dict): map_['auth'] = False # Merge in what was read from the config file and set keys on self - for key in ('irc', 'web', 'maps'): + for key in ('logging', 'irc', 'web', 'maps'): if key in obj: finalObj[key].update(obj[key]) self[key] = finalObj[key] def __repr__(self): - return f'' + return f'' def reread(self): return Config(self._filename) @@ -412,12 +424,24 @@ class WebServer: raise aiohttp.web.HTTPOk() +def configure_logging(config): + #TODO: Replace with logging.basicConfig(..., force = True) (Py 3.8+) + root = logging.getLogger() + root.setLevel(getattr(logging, config['logging']['level'])) + root.handlers = [] #FIXME: Undocumented attribute of logging.Logger + formatter = logging.Formatter(config['logging']['format'], style = '{') + stderrHandler = logging.StreamHandler() + stderrHandler.setFormatter(formatter) + root.addHandler(stderrHandler) + + async def main(): if len(sys.argv) != 2: print('Usage: http2irc.py CONFIGFILE', file = sys.stderr) sys.exit(1) configFile = sys.argv[1] config = Config(configFile) + configure_logging(config) loop = asyncio.get_running_loop() @@ -442,6 +466,7 @@ async def main(): logging.error(f'Config reload failed: {e!s}') return config = newConfig + configure_logging(config) irc.update_config(config) webserver.update_config(config) loop.add_signal_handler(signal.SIGUSR1, sigusr1_callback)