diff --git a/thespian/system/multiprocCommon.py b/thespian/system/multiprocCommon.py index 947f95f..aae41e9 100644 --- a/thespian/system/multiprocCommon.py +++ b/thespian/system/multiprocCommon.py @@ -2,24 +2,20 @@ python 'multiprocess' module. Intended as a base class, not for direct usage.""" +import signal +import sys +from functools import partial -import logging from thespian.actors import * -from thespian.system.systemBase import systemBase -from thespian.system.systemCommon import actorStartupFailed -from thespian.system.utilis import thesplog, checkActorCapabilities, partition -from thespian.system.transport import * -from thespian.system.logdirector import * -from thespian.system.utilis import setProcName, StatsManager from thespian.system.addressManager import ActorLocalAddress, CannotPickleAddress +from thespian.system.logdirector import * from thespian.system.messages.multiproc import * from thespian.system.sourceLoader import loadModuleFromHashSource -from functools import partial -import multiprocessing -import signal -from datetime import timedelta -import sys - +from thespian.system.systemBase import systemBase +from thespian.system.systemCommon import actorStartupFailed +from thespian.system.transport import * +from thespian.system.utilis import checkActorCapabilities, partition +from thespian.system.utilis import setProcName, _name_to_level MAX_ADMIN_STARTUP_DELAY = timedelta(seconds=5) @@ -512,6 +508,94 @@ def shutdown_signal_detected(signum, frame): return shutdown_signal_detected +def get_min_log_level(logDefs): + """ + Determine the minimum logging level based on the rules in: + https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig + + Note: The level and handlers entries are interpreted as for the root logger, + except that if a non-root logger's level is specified as NOTSET, the system + consults loggers higher up the hierarchy to determine the effective + level of the logger. + + :param logDefs: a logging configuration dictionary object + :return: integer value of the lowest log level, or the default level: logging.WARNING. + + >>> import logging + >>> import logging.config + >>> logDefs = {'version': 1, 'handlers': { + ... 'h1': {'class': 'logging.FileHandler', 'filename': 'example.log', 'filters': [], + ... 'level': "INFO"}, + ... 'h2': {'class': 'logging.FileHandler', 'filename': 'example.log', 'filters': [], + ... 'level': "INFO"}}, 'loggers': {'': {'handlers': ['h1', 'h2'], 'level': "DEBUG"}}} + >>> logging.config.dictConfig(logDefs) # prove the config is considered valid + >>> get_min_log_level(logDefs) # logging.DEBUG + 10 + + >>> logDefs = {'version': 1, 'root': {'level': 10}, 'loggers': {'one': {'level': 20}}} + >>> logging.config.dictConfig(logDefs) + >>> min1 = get_min_log_level(logDefs) + >>> logDefs = {'version': 1, 'root': {'level': 20}, 'loggers': {'one': {'level': 10}}} + >>> logging.config.dictConfig(logDefs) + >>> min2 = get_min_log_level(logDefs) + >>> min1 == min2 == 10 + True + + >>> get_min_log_level({'version' : 1}) # logging.WARNING is default log level + 30 + + >>> logDefs = {'version': 1, + ... 'loggers': {'root': {'level': "INFO"}, 'handler2': {'level': logging.NOTSET}}} + >>> logging.config.dictConfig(logDefs) + >>> get_min_log_level(logDefs) # logging.INFO + 20 + + >>> logDefs = {'version': 1, 'loggers': {'one': {'level': logging.NOTSET}}} + >>> logging.config.dictConfig(logDefs) + >>> get_min_log_level(logDefs) # logging.WARNING + 30 + + >>> logDefs = {'version': 1, + ... 'root': {'level': None}, + ... 'loggers': {'one': {'level': logging.NOTSET}}} + >>> logging.config.dictConfig(logDefs) + >>> get_min_log_level(logDefs) # logging.WARNING + 30 + + """ + + levels = [] + root_data = logDefs.get('root') + if root_data and root_data.get('level'): + if isinstance( root_data.get('level'), str): + root_val = _name_to_level.get(root_data.get('level'). upper()) + else: + root_val = root_data.get('level') + else: + loggers = logDefs.get('loggers', {}) + root_val_empty = loggers.get("", {}).get("level") + root_val_named = loggers.get("root", {}).get("level") + root_val = root_val_empty if root_val_empty else root_val_named + if root_val: + root_val = _name_to_level.get(root_val, root_val) + + def dfs(mapping): + """Traverse and collect""" + for key, val in mapping.items(): + if isinstance(val, dict): + dfs(val) + if key == 'level': + if isinstance(val, str): + val = _name_to_level[val.upper()] + if val == logging.NOTSET and root_val and root_val > logging.NOTSET: + continue + levels.append(val) + + dfs(logDefs) + levels = [tmp for tmp in levels if tmp] # swallow None values + return min(levels) if levels else logging.WARNING + + def startChild(childClass, globalName, endpoint, transportClass, sourceHash, sourceToLoad, parentAddr, adminAddr, notifyAddr, loggerAddr, @@ -544,24 +628,7 @@ def startChild(childClass, globalName, endpoint, transportClass, # logging.shutdown() because (a) that does not do enough to reset, # and (b) it shuts down handlers, but we want to leave the parent's # handlers alone. - if logDefs: - levelIn = lambda d: d.get('level', 0) - minLevelIn = lambda l: min(list(l)) if list(l) else 0 - levels = list( - filter(None, - ([ minLevelIn([levelIn(logDefs[key][subkey]) - for subkey in logDefs[key] - # if subkey in [ 'loggers', 'handlers' - if isinstance(logDefs[key][subkey], dict) - ]) - if key in ['loggers', 'handlers'] and isinstance(logDefs[key], dict) - else (levelIn(logDefs[key]) if key == 'root' - else (logDefs[key] if key == 'level' - else None)) - for key in logDefs ]))) - lowestLevel = minLevelIn(levels) - else: - lowestLevel = 0 + lowestLevel = get_min_log_level(logDefs) logging.root = ThespianLogForwarder(loggerAddr, transport, logLevel=lowestLevel) logging.Logger.root = logging.root logging.Logger.manager = logging.Manager(logging.Logger.root) diff --git a/thespian/system/utilis.py b/thespian/system/utilis.py index f4ecb3c..796c80e 100644 --- a/thespian/system/utilis.py +++ b/thespian/system/utilis.py @@ -17,7 +17,8 @@ "ERROR": logging.ERROR, "WARNING": logging.WARNING, "INFO": logging.INFO, - "DEBUG": logging.DEBUG + "DEBUG": logging.DEBUG, + "NOTSET": logging.NOTSET } # Default/current logging controls