KarrLab/wc_utils

View on GitHub
wc_utils/debug_logs/config.py

Summary

Maintainability
A
2 hrs
Test Coverage
C
74%
import yaml
import sys
""" Configure debug log files.

:Author: Arthur Goldberg <Arthur.Goldberg@mssm.edu>
:Date: 2016-09-22
:Copyright: 2016-2018, Karr Lab
:License: MIT
"""

from os import makedirs, path
from pkg_resources import resource_filename
from wc_utils.config.core import ConfigPaths
try:
    # try importing logging2 because logging2 can be installed in Windows
    # although logging2 relies on syslog which only works on Unix
    import logging2
except ModuleNotFoundError:  # pragma: no cover
    logging2 = None

paths = ConfigPaths(
    default=resource_filename('wc_utils', 'debug_logs/config.default.cfg'),
    schema=resource_filename('wc_utils', 'debug_logs/config.schema.cfg'),
    user=(
        'debug.cfg',
        path.expanduser('~/.wc/debug.cfg'),
    ),
)


class LoggerConfigurator(object):
    ''' A class with static methods that configures log files. '''

    @staticmethod
    def from_yaml(config_path):
        """ Create and configure logs from a YAML file which describes their configuration

        Deprecated in favor of ConfigObj

        Returns:
            :obj:`dict`: handlers 
            :obj:`dict`: loggers

        Args:
            config_path (:obj:`str`): path to configuration file written in YAML
        """

        with open(config_path, 'r') as file:
            config = yaml.load(file, Loader=yaml.SafeLoader)

        return LoggerConfigurator.from_dict(config)

    @staticmethod
    def from_dict(config):
        """ Create and configure logs from a dictionary which describes their configuration

        Caution: because `logging2.Logger()` caches handlers and loggers, `from_dict()` may not create
        a logger with the configuration requested. In particular, creating a logger that has the same
        name as an existing logger will return the *existing log* without considering any of the
        parameters provided by `config`. In addition, adding a handler to a logger (using `logger.add_handler())`)
        will silently fail if the name of the handler being added is the same as the name of an
        existing handler used by the logger.

        Args:
            config (:obj:`dict`): dictionary of logger configurations

        Returns:
            :obj:`dict`: handlers 
            :obj:`dict`: loggers

        Raises:
            :obj:`log.ConfigurationError`: For unsupported handler types
            :obj:`ModuleNotFoundError`: If `logging2` is not installed
        """
        if logging2 is None:
            raise ModuleNotFoundError("'logging2' must be installed")  # pragma: no cover

        # create handlers
        # risky: handlers are shared between loggers. thus,
        # any modifications of handlers by one logger may affect another.
        handlers = {}
        for name, config_handler in config.get('handlers', {}).items():
            extra_opts = set(config_handler.keys()).difference(set(['class', 'filename', 'encoding', 'level']))
            if extra_opts:
                raise ConfigurationError('Handler configuration does not support options "{}"'.format(
                    '", "'.join(extra_opts)))

            class_name = config_handler.get('class', 'StdOutHandler')
            level = getattr(logging2.LogLevel, config_handler.get('level', 'debug').lower())

            if class_name in ['StdErrHandler', 'StdOutHandler']:
                cls = getattr(logging2, class_name)
                handler = cls(name=name, level=level)

            elif class_name == 'FileHandler':
                filename = path.expanduser(config_handler['filename'])

                if not path.isdir(path.dirname(filename)):
                    makedirs(path.dirname(filename))

                if not path.isfile(filename):
                    open(filename, 'w').close()

                encoding = config_handler.get('encoding', 'utf-8')

                handler = logging2.FileHandler(filename, name=name, level=level, encoding=encoding)

            else:
                raise ConfigurationError('Unsupported handler class: ' + class_name)

            handlers[name] = handler

        # create loggers
        loggers = {}
        for name, config_logger in config.get('loggers', {}).items():
            extra_opts = set(config_logger.keys()).difference(set(['template', 'timezone', 'handler', 'additional_context']))
            if extra_opts:
                raise ConfigurationError('Logger configuration does not support options "{}"'.format(
                    '", "'.join(extra_opts)))

            template = config_logger.get('template', None)
            timezone = config_logger.get('timezone', None)
            additional_context = config_logger.get('additional_context', None)

            if 'handler' in config_logger and config_logger['handler'] in handlers:
                handler = handlers[config_logger['handler']]
            else:
                raise ConfigurationError("A handler must be defined.")

            loggers[name] = logging2.Logger(name=name, template=template, timezone=timezone,
                                            handler=handler, additional_context=additional_context)

        # return handlers and loggers
        return handlers, loggers


class ConfigurationError(Exception):
    """ An error in a logging configuration """
    pass