Source code for triumvirate.logger

"""
Program Logger (:mod:`~triumvirate.logger`)
===========================================

Configure the program logger.

.. autosummary::
    setup_logger

"""  # numpydoc ignore=SS01
import logging
import sys
import time
import warnings


class _ElapsedLogFormatter(logging.Formatter):
    """Elapsed-time logging formatter.

    """

    _start_time = time.time()

    def format(self, record):
        """Add elapsed time in hours, minutes and seconds to the log
        record.

        Parameters
        ----------
        record : :class:`Logging.LogRecord`
            See :class:`logging.LogRecord`.

        Returns
        -------
        str
            Modified log record with elapsed time.

        """
        elapsed_time = record.created - self._start_time
        h, remainder_time = divmod(elapsed_time, 3600)
        m, s = divmod(remainder_time, 60)

        record.elapsed = f"(+{int(h):02d}:{int(m):02d}:{int(s):02d})"

        return logging.Formatter.format(self, record)


class _CppLogAdapter(logging.LoggerAdapter):
    """C++ logging adapter.

    """

    def process(self, msg, kwargs):
        """Adapt logger message.

        Parameters
        ----------
        msg : str
            See :class:`logging.LoggerAdapter`.
        kwargs : dict
            See :class:`logging.LoggerAdapter`.

        Returns
        -------
        str
            Adapted log message with C++ runtime indication.

        """
        # Extract passed state variable or resort to default from `extra`.
        cpp_state = kwargs.pop('cpp_state', self.extra['cpp_state'])

        if isinstance(cpp_state, str):
            if cpp_state.lower() == 'start':
                return "%s (entering C++)" % msg, kwargs
            if cpp_state.lower() == 'end':
                return "%s (exited C++)" % msg, kwargs

        if cpp_state:  # merely indicating in CPP state, no extra text
            return "%s (in C++)" % msg, kwargs
        return "%s" % msg, kwargs


# Modify `warnings.formatwarning`.
def _format_warning(message, category, filename, lineno, line=None):
    """Modify formatter for warning messages in logged record.

    See Also
    --------
    :func:`warnings.formatwarning`
        For implementation details.

    """  # numpydoc ignore=PR01,RT01
    msg = warnings.WarningMessage(
        message, category, filename, lineno, None, line
    )

    msg_txt = "{} ({}:{}:{}{{}})".format(
        str(msg.message).replace('{', '{{').replace('}', '}}'),
        msg.filename, msg.lineno, msg.category.__name__
    )

    if msg.line is None:
        try:
            import linecache
            linesource = linecache.getline(msg.filename, msg.lineno)
        except Exception:
            linesource = None
    else:
        linesource = msg.line

    if linesource:
        linesource = linesource.strip()
        msg_txt = msg_txt.format(" --> \"" + linesource + "\"")
    else:
        msg_txt = msg_txt.format("")

    return msg_txt


[docs] def setup_logger(log_level=logging.INFO): """Set up and return a customised logger with elapsed time, C++ runtime indication and formatted warning messages. Parameters ---------- log_level : int, optional Logging devel (default is `logging.INFO`). Returns ------- logger : :class:`logging.LoggerAdapter` Customised logger. See Also -------- :mod:`logging` For more details of the Python logging facility. """ # Set formatter. formatter = _ElapsedLogFormatter( fmt='[%(asctime)s %(elapsed)s %(levelname).4s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # Set handler. handler = logging.StreamHandler(sys.stdout) handler.setFormatter(formatter) # Instantiate logger. logger_ = logging.getLogger() if logger_.hasHandlers(): logger_.handlers.clear() logger_.addHandler(handler) logger_.setLevel(log_level) # Adapt logger for C++ code indication. logger = _CppLogAdapter(logger_, {'cpp_state': False}) # Adapt logger to capture warnings. warnings.formatwarning = _format_warning logging.captureWarnings(True) return logger