Source code for law.logger

# coding: utf-8

"""
Law logging setup.
"""

__all__ = [
    "get_logger", "setup_logging", "setup_logger", "create_stream_handler", "is_tty_handler",
    "get_tty_handlers", "Logger", "LogFormatter",
]


from collections import defaultdict
import logging

import six

from law.util import no_value, colored, ipykernel, ON_COLAB


_logging_setup = False


[docs]def setup_logging(): """ Sets up the internal law loggers as well as all other loggers listed in the ``"logging"`` config section as (*name*, *level*) pairs. This includes loggers that do not use the ``"law.*"`` namespace which can be seen as a convenient feature to set up custom loggers. """ global _logging_setup # make sure logging is setup only once if _logging_setup: return _logging_setup = True # remove root handlers on colab if ON_COLAB and logging.root.handlers: logging.root.removeHandler(logging.root.handlers[0]) # setup the main law logger first and set its handler which is propagated to subloggers logger = get_logger("law", skip_setup=True) logger = setup_logger(logger, add_console_handler=False) logger.addHandler(create_stream_handler()) # set levels for all loggers and add the console handler for all non-law loggers from law.config import Config for name, level in Config.instance().items("logging"): setup_logger(name, level)
def _logger_setup(logger, value=None): attr = "_law_logger_setup" if value is not None: setattr(logger, attr, value) return getattr(logger, attr, False)
[docs]def get_logger(*args, **kwargs): """ get_logger(*args, skip_setup=False, **kwargs) Replacement for *logging.getLogger* that makes sure that the custom :py:class:`Logger` class is used when new loggers are created and that the logger is properly set up by :py:meth:`setup_logger`. """ skip_setup = kwargs.pop("skip_setup", False) orig_cls = logging.getLoggerClass() logging.setLoggerClass(Logger) try: logger = logging.getLogger(*args, **kwargs) # set it up once if not skip_setup: setup_logger(logger) return logger finally: logging.setLoggerClass(orig_cls)
[docs]def setup_logger(logger, level=None, add_console_handler=None, clear=False, force=False): """ Sets up a *logger*, optionally given by its name, configures it to have a certain *level* and adds a preconfigured console handler when *add_console_handler* is *True*. When *add_console_handler* is a dictionary, its items are forwarded as keyword arguments to the :py:func:`create_stream_handler` which handles the handler setup internally. When *None*, *add_console_handler* is default to *True* in case the logger is not a "law" sublogger and has no tty handlers registered yet. Each logger is setup only once unless *force* is *True. *level* can either be an integer or the name of a level present in the *logging* module. When no *level* is given, the level of the ``"law"`` base logger is used as a default. When the logger already existed and *clear* is *True*, all handlers and filters are removed first. The logger object is returned. """ # get the logger logger = logger if isinstance(logger, logging.Logger) else get_logger(logger, skip_setup=True) name = logger.name # do nothing when the logger was already set up or force is defined if _logger_setup(logger) or force: return logger _logger_setup(logger, True) # sanitize the level if level is None: from law.config import Config level = Config.instance().get_expanded("logging", name, None) if isinstance(level, six.string_types): level = getattr(logging, level.upper(), None) if level is None: level = get_logger("law").level # clear handlers and filters is_existing = name in logging.root.manager.loggerDict if is_existing and clear: for h in list(logger.handlers): logger.removeHandler(h) for f in list(logger.filters): logger.removeFilter(f) # set the level logger.setLevel(level) # add a console handler if add_console_handler is None: add_console_handler = not name.startswith("law.") and not get_tty_handlers(name) if add_console_handler or isinstance(add_console_handler, dict): kwargs = add_console_handler if isinstance(add_console_handler, dict) else {} logger.addHandler(create_stream_handler(**kwargs)) return logger
[docs]def create_stream_handler(handler_kwargs=None, formatter_kwargs=None, formatter_cls=no_value): """ create_stream_handler(handler_kwargs=None, formatter_kwargs=None, formatter_cls=LogFormatter) Creates a new StreamHandler instance, passing all *handler_kwargs* to its constructor, and returns it. When not *None*, an instance of *formatter_cls* is created using *formatter_kwargs* and added to the handler instance. """ # create the handler handler = logging.StreamHandler(**(handler_kwargs or {})) # add a formatter if formatter_cls == no_value: formatter_cls = LogFormatter if formatter_cls is not None: formatter = formatter_cls(**(formatter_kwargs or {})) handler.setFormatter(formatter) return handler
[docs]def is_tty_handler(handler): """ Returns *True* if a logging *handler* is a *StreamHandler* which logs to a tty (i.e. *stdout* or *stderr*), an IPython *OutStream*, or a base *Handler* with a *console* attribute evaluating to *True*. The latter check is intended to cover a variety of handlers provided by custom modules. """ if isinstance(handler, logging.StreamHandler) and getattr(handler, "stream", None): if callable(getattr(handler.stream, "isatty", None)) and handler.stream.isatty(): return True elif ipykernel and isinstance(handler.stream, ipykernel.iostream.OutStream): return True if isinstance(handler, logging.Handler) and getattr(handler, "console", None): return True return False
[docs]def get_tty_handlers(logger): """ Returns a list of all handlers of a *logger* that log to a tty. """ if isinstance(logger, six.string_types): logger = get_logger(logger) return [handler for handler in getattr(logger, "handlers", []) if is_tty_handler(handler)]
[docs]class Logger(logging.Logger): """ Custom logger class that adds an additional set of log methods, i.e., :py:meth:`debug_once`, :py:meth:`info_once`, :py:meth:`warning_once`, :py:meth:`error_once`, :py:meth:`critical_once` and :py:meth:`fatal_once`, that log certain messages only once depending on a string identifier. """ def __init__(self, *args, **kwargs): super(Logger, self).__init__(*args, **kwargs) # names of logs per level that are issued only once self._once_logs = defaultdict(set) def debug_once(self, log_id, *args, **kwargs): # when no log_id is set, but just a message, it is received as log_id if not args: args = (log_id,) if log_id not in self._once_logs["debug"]: self._once_logs["debug"].add(log_id) self.debug(*args, **kwargs) def info_once(self, log_id, *args, **kwargs): # when no log_id is set, but just a message, it is received as log_id if not args: args = (log_id,) if log_id not in self._once_logs["info"]: self._once_logs["info"].add(log_id) self.info(*args, **kwargs) def warning_once(self, log_id, *args, **kwargs): # when no log_id is set, but just a message, it is received as log_id if not args: args = (log_id,) if log_id not in self._once_logs["warning"]: self._once_logs["warning"].add(log_id) self.warning(*args, **kwargs) def error_once(self, log_id, *args, **kwargs): # when no log_id is set, but just a message, it is received as log_id if not args: args = (log_id,) if log_id not in self._once_logs["error"]: self._once_logs["error"].add(log_id) self.error(*args, **kwargs) def critical_once(self, log_id, *args, **kwargs): # when no log_id is set, but just a message, it is received as log_id if not args: args = (log_id,) if log_id not in self._once_logs["critical"]: self._once_logs["critical"].add(log_id) self.critical(*args, **kwargs) def fatal_once(self, log_id, *args, **kwargs): # when no log_id is set, but just a message, it is received as log_id if not args: args = (log_id,) if log_id not in self._once_logs["fatal"]: self._once_logs["fatal"].add(log_id) self.fatal(*args, **kwargs)
[docs]class LogFormatter(logging.Formatter): """ __init__(*args, log_template=None, err_template=None, level_styles=None, name_styles=None, \ msg_styles=None, format_level=None, format_name=None, format_msg=None, **kwargs) Configurable formatter class for colored logs. When set, *log_template*, *err_template*, *level_styles*, *name_styles*, *msg_styles*, *format_level*, *format_name* and *format_msg* control the log formats and styles on instance level. When *None*, they default to the upper case class level attributes described below. All *args* and *kwargs* are forwarded to :py:class:`logging.Formatter`. .. py:classattribute:: LOG_TEMPLATE type: string Template for log messages without stack traces. .. py:classattribute:: ERR_TEMPLATE type: string Template for log messages including stack traces. .. py:classattribute:: LEVEL_STYLES type: dict Style attributes forwarded to :py:func:`law.util.colored` per log level for styling level names in logs .. py:classattribute:: NAME_STYLES type: dict Style attributes forwarded to :py:func:`law.util.colored` per log level for styling logger names in logs. .. py:classattribute:: MSG_STYLES type: dict Style attributes forwarded to :py:func:`law.util.colored` per log level for styling messages in logs. .. py:classattribute:: FORMAT_LEVEL type: callable or None Custom callback to format the log level using the full record. .. py:classattribute:: FORMAT_NAME type: callable, None Custom callback to format the loger name using the full record. .. py:classattribute:: FORMAT_MSG type: callable, None Custom callback to format the log message using the full record. """ LOG_TEMPLATE = "{level}: {name} - {msg}" ERR_TEMPLATE = "{level}: {name} - {msg}\n{traceback}" LEVEL_STYLES = { "DEBUG": {"color": "cyan"}, "INFO": {"color": "green"}, "WARNING": {"color": "yellow"}, "ERROR": {"color": "red"}, "CRITICAL": {"color": "red", "style": "bright"}, "FATAL": {"color": "red", "style": "bright"}, } NAME_STYLES = {} MSG_STYLES = { "WARNING": {"color": "yellow"}, "ERROR": {"color": "red"}, "CRITICAL": {"color": "red", "style": "bright"}, "FATAL": {"color": "red", "style": "bright"}, } FORMAT_LEVEL = None FORMAT_NAME = None FORMAT_MSG = None def __init__(self, *args, **kwargs): self.log_template = kwargs.pop("log_template", self.LOG_TEMPLATE) self.err_template = kwargs.pop("err_template", self.ERR_TEMPLATE) self.level_styles = kwargs.pop("level_styles", self.LEVEL_STYLES) self.name_styles = kwargs.pop("name_styles", self.NAME_STYLES) self.msg_styles = kwargs.pop("msg_styles", self.MSG_STYLES) self.format_level = kwargs.pop("format_level", self.FORMAT_LEVEL) self.format_name = kwargs.pop("format_name", self.FORMAT_NAME) self.format_msg = kwargs.pop("format_msg", self.FORMAT_MSG) super(LogFormatter, self).__init__(*args, **kwargs) def format(self, record): """""" # get and style the level level = self.format_level(record) if callable(self.format_level) else record.levelname level = colored(level, **self.level_styles.get(record.levelname, {})) # get and style the name name = self.format_name(record) if callable(self.format_name) else record.name name = colored(name, **self.name_styles.get(record.levelname, {})) # get and style the message msg = self.format_msg(record) if callable(self.format_msg) else record.getMessage() msg = colored(msg, **self.msg_styles.get(record.levelname, {})) # build template data tmpl = self.log_template data = dict(level=level, name=name, msg=msg) # add traceback and change the template when the record contains exception info if record.exc_info: tmpl = self.err_template data["traceback"] = self.formatException(record.exc_info) return tmpl.format(**data)