Module skm_pyutils.log

Logging related functions.

Expand source code
"""
Logging related functions.
"""
import datetime
import logging
import os
import sys
import traceback

import psutil

from skm_pyutils.path import make_path_if_not_exists


def log_exception(ex, more_info="", location=None):
    """
    Log an expection to file and additional info.

    Parameters
    ----------
    ex : Exception
        The python exception that occurred
    more_info : str, optional
        Additional string to log, default is ""
    location : str, optional
        Where to store the log, default is
        home/.skm_python/caught_errors.txt

    Returns
    -------
    None

    """
    if location is None:
        default_loc = get_default_log_loc("caught_errors.txt")
    else:
        default_loc = location

    now = datetime.datetime.now()
    make_path_if_not_exists(default_loc)
    with open(default_loc, "a+") as f:
        f.write("\n----------Caught Exception at {}----------\n".format(now))
        traceback.print_exc(file=f)
    logging.error(
        "{} failed with caught exception.\nSee {} for more information.".format(
            more_info, default_loc
        ),
        exc_info=False,
    )


def default_excepthook(exc_type, exc_value, exc_traceback):
    """Any uncaught exceptions will be logged from here."""
    default_loc = get_default_log_loc("uncaught_errors.txt")

    file_logger = logging.getLogger(__name__)
    file_logger.propagate = False
    handler = logging.FileHandler(default_loc)
    file_logger.addHandler(handler)

    # Don't catch CTRL+C exceptions
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return

    now = datetime.datetime.now()
    file_logger.critical(
        "\n----------Uncaught Exception at {}----------".format(now),
        exc_info=(exc_type, exc_value, exc_traceback),
    )

    print("\nA fatal error occurred in this Python program")
    print(
        "The error info was: {}".format(
            "".join(
                traceback.format_exception(exc_type, exc_value, exc_traceback)
            ).strip()
        )
    )
    print("Please report this to {} and provide the file {}".format("us", default_loc))

    sys.exit(-1)


def override_excepthook(excepthook=None):
    """
    Change sys.excepthook behaviour.
    
    Parameters
    ----------
    excepthook : function, optional
        This is what sys.excepthook will be overridden by.
        See https://docs.python.org/3/library/sys.html#sys.excepthook
        for the function specification.
        If None is passed, uses the default excepthook in this module.
    
    Returns
    -------
    None

    """
    if excepthook is None:
        excepthook = default_excepthook
    sys.excepthook = excepthook


def get_default_log_loc(name):
    default_loc = os.path.join(os.path.expanduser("~"), ".skm_python", name)

    return default_loc


def setup_text_logging(
    in_dir, loglevel, bname="logfile.log", append=False, logname=None
):
    """
    Pass logging file location to logging.

    Parameters
    ----------
    in_dir : str
        Directory to save log file to. Can be None to use bname directly.
    loglevel : str
        The name of the log level to use, e.g. "debug" or 10
    bname : str, optional, defaults to "logfile.log"
        The basename of the log file to save to.
    append: bool, optional, defaults to False
    logname : str, optional, defaults to None
        If provided, creates this logger. Otherwise uses the root logger.

    Returns
    -------
    None

    """
    numeric_level = convert_log_level(loglevel)

    if in_dir is None:
        fname = bname
    else:
        fname = os.path.join(in_dir, bname)
    if not append:
        if os.path.isfile(fname):
            open(fname, "w").close()

    root = logging.getLogger(logname)
    root.setLevel(numeric_level)
    file_handler = logging.FileHandler(fname)
    file_handler.setFormatter(
        logging.Formatter(
            fmt="%(levelname)s: %(asctime)s %(message)s",
            datefmt="%d/%m/%Y %I:%M:%S %p",
        )
    )
    root.addHandler(file_handler)
    # Encoding only supported in 3.9+
    # logging.basicConfig(
    #     filename=fname,
    #     # encoding="utf-8",
    #     level=numeric_level,
    #     format="%(levelname)s: %(asctime)s %(message)s",
    #     datefmt="%d/%m/%Y %I:%M:%S %p",
    # )
    mpl_logger = logging.getLogger("matplotlib")
    mpl_logger.setLevel(level=logging.WARNING)

def convert_log_level(loglevel):
    """Convert string or int log level to numeric level for logging module"""
    try:
        numeric_level = int(loglevel)
    except BaseException:
        numeric_level = getattr(logging, loglevel.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError(f"Invalid log level: {loglevel}")
    return numeric_level

def clear_handlers(logger):
    """Clear the handlers from the passed logger."""
    handlers = logger.handlers[:]
    for handler in handlers:
        handler.close()
        logger.removeHandler(handler)


class FileStdoutLogger:
    """A logger that prints to stdout and to a file."""

    def __init__(self, name="stdout_logger"):
        self.name = name
        self.logger = None
        self.create_logger()

    def init_logging(self):
        self.logger.setLevel(logging.INFO)
        out_loc = self.get_default_log_location()
        output_file_handler = logging.FileHandler(out_loc)
        stdout_handler = logging.StreamHandler(sys.stdout)

        self.logger.addHandler(output_file_handler)
        self.logger.addHandler(stdout_handler)

    def create_logger(self):
        if self.logger is None:
            self.logger = logging.getLogger(self.name)
        if len(self.get_handlers()) == 0:
            self.init_logging()

    def print(self, msg):
        self.logger.info(msg)

    def get_default_log_location(self):
        home = os.path.expanduser("~")
        out_loc = os.path.join(home, ".skm_python", f"{self.name}.log")
        os.makedirs(os.path.dirname(out_loc), exist_ok=True)

        return out_loc

    def read_log_file(self):
        loc = self.get_default_log_location()
        if os.path.exists(loc):
            with open(loc, "r") as f:
                out = f.read().strip()
            return out
        else:
            return "No log file exists."

    def clear_log_file(self):
        # handlers = self.logger.handlers[:]
        # for handler in handlers:
        #     handler.close()
        #     self.logger.removeHandler(handler)

        loc = self.get_default_log_location()
        if os.path.exists(loc):
            open(loc, "w").close()

    def get_handlers(self):
        return self.logger.handlers[:]


class FileLogger:
    """A logger that prints to a file."""

    def __init__(self, name="main", level="warning"):
        self.name = name
        self.logger = None
        self.level = level
        self.create_logger()

    def init_logging(self):
        out_loc = self.get_default_log_location()
        setup_text_logging(None, self.level, out_loc, append=True, logname=self.name)

    def create_logger(self):
        if self.logger is None:
            self.logger = logging.getLogger(self.name)
        if len(self.get_handlers()) == 0:
            self.init_logging()

    def info(self, msg, **kwargs):
        self.logger.info(msg, **kwargs)

    def warning(self, msg, **kwargs):
        self.logger.warning(msg, **kwargs)

    def error(self, msg, **kwargs):
        self.logger.error(msg, **kwargs)

    def critical(self, msg, **kwargs):
        self.logger.critical(msg, **kwargs)

    def debug(self, msg, **kwargs):
        self.logger.debug(msg, **kwargs)

    def set_level(self, loglevel):
        try:
            numeric_level = int(loglevel)
        except BaseException:
            numeric_level = getattr(logging, loglevel.upper(), None)
        if not isinstance(numeric_level, int):
            raise ValueError("Invalid log level: %s" % loglevel)

        self.logger.setLevel(numeric_level)
        filename = self.get_default_log_location()
        print("See {} for {} level logs".format(filename, loglevel))

    def get_default_log_location(self):
        home = os.path.expanduser("~")
        out_loc = os.path.join(home, ".skm_python", f"{self.name}.log")
        os.makedirs(os.path.dirname(out_loc), exist_ok=True)

        return out_loc

    def read_log_file(self):
        loc = self.get_default_log_location()
        if os.path.exists(loc):
            with open(loc, "r") as f:
                out = f.read().strip()
            return out
        else:
            return "No log file exists."

    def clear_log_file(self):
        self.clear_handlers()

        loc = self.get_default_log_location()
        if os.path.exists(loc):
            os.remove(loc)

    def clear_handlers(self):
        handlers = self.logger.handlers[:]
        for handler in handlers:
            handler.close()
            self.logger.removeHandler(handler)

    def get_handlers(self):
        return self.logger.handlers[:]


def print_memory_usage(as_string: bool=False) -> str:
    """
    Print memory usage information
    
    Parameters
    ----------
    as_string : bool, optional
        Just return the string representation, don't print.
        By default, False.
    
    Returns
    -------
    usage : str
        String about memory usage
    """
    str_ = f"RAM memory usage stats: {psutil.virtual_memory()}"
    if not as_string:
        print(str_)
    return str_

Functions

def clear_handlers(logger)

Clear the handlers from the passed logger.

Expand source code
def clear_handlers(logger):
    """Clear the handlers from the passed logger."""
    handlers = logger.handlers[:]
    for handler in handlers:
        handler.close()
        logger.removeHandler(handler)
def convert_log_level(loglevel)

Convert string or int log level to numeric level for logging module

Expand source code
def convert_log_level(loglevel):
    """Convert string or int log level to numeric level for logging module"""
    try:
        numeric_level = int(loglevel)
    except BaseException:
        numeric_level = getattr(logging, loglevel.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError(f"Invalid log level: {loglevel}")
    return numeric_level
def default_excepthook(exc_type, exc_value, exc_traceback)

Any uncaught exceptions will be logged from here.

Expand source code
def default_excepthook(exc_type, exc_value, exc_traceback):
    """Any uncaught exceptions will be logged from here."""
    default_loc = get_default_log_loc("uncaught_errors.txt")

    file_logger = logging.getLogger(__name__)
    file_logger.propagate = False
    handler = logging.FileHandler(default_loc)
    file_logger.addHandler(handler)

    # Don't catch CTRL+C exceptions
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return

    now = datetime.datetime.now()
    file_logger.critical(
        "\n----------Uncaught Exception at {}----------".format(now),
        exc_info=(exc_type, exc_value, exc_traceback),
    )

    print("\nA fatal error occurred in this Python program")
    print(
        "The error info was: {}".format(
            "".join(
                traceback.format_exception(exc_type, exc_value, exc_traceback)
            ).strip()
        )
    )
    print("Please report this to {} and provide the file {}".format("us", default_loc))

    sys.exit(-1)
def get_default_log_loc(name)
Expand source code
def get_default_log_loc(name):
    default_loc = os.path.join(os.path.expanduser("~"), ".skm_python", name)

    return default_loc
def log_exception(ex, more_info='', location=None)

Log an expection to file and additional info.

Parameters

ex : Exception
The python exception that occurred
more_info : str, optional
Additional string to log, default is ""
location : str, optional
Where to store the log, default is home/.skm_python/caught_errors.txt

Returns

None
 
Expand source code
def log_exception(ex, more_info="", location=None):
    """
    Log an expection to file and additional info.

    Parameters
    ----------
    ex : Exception
        The python exception that occurred
    more_info : str, optional
        Additional string to log, default is ""
    location : str, optional
        Where to store the log, default is
        home/.skm_python/caught_errors.txt

    Returns
    -------
    None

    """
    if location is None:
        default_loc = get_default_log_loc("caught_errors.txt")
    else:
        default_loc = location

    now = datetime.datetime.now()
    make_path_if_not_exists(default_loc)
    with open(default_loc, "a+") as f:
        f.write("\n----------Caught Exception at {}----------\n".format(now))
        traceback.print_exc(file=f)
    logging.error(
        "{} failed with caught exception.\nSee {} for more information.".format(
            more_info, default_loc
        ),
        exc_info=False,
    )
def override_excepthook(excepthook=None)

Change sys.excepthook behaviour.

Parameters

excepthook : function, optional
This is what sys.excepthook will be overridden by. See https://docs.python.org/3/library/sys.html#sys.excepthook for the function specification. If None is passed, uses the default excepthook in this module.

Returns

None
 
Expand source code
def override_excepthook(excepthook=None):
    """
    Change sys.excepthook behaviour.
    
    Parameters
    ----------
    excepthook : function, optional
        This is what sys.excepthook will be overridden by.
        See https://docs.python.org/3/library/sys.html#sys.excepthook
        for the function specification.
        If None is passed, uses the default excepthook in this module.
    
    Returns
    -------
    None

    """
    if excepthook is None:
        excepthook = default_excepthook
    sys.excepthook = excepthook
def print_memory_usage(as_string: bool = False) ‑> str

Print memory usage information

Parameters

as_string : bool, optional
Just return the string representation, don't print. By default, False.

Returns

usage : str
String about memory usage
Expand source code
def print_memory_usage(as_string: bool=False) -> str:
    """
    Print memory usage information
    
    Parameters
    ----------
    as_string : bool, optional
        Just return the string representation, don't print.
        By default, False.
    
    Returns
    -------
    usage : str
        String about memory usage
    """
    str_ = f"RAM memory usage stats: {psutil.virtual_memory()}"
    if not as_string:
        print(str_)
    return str_
def setup_text_logging(in_dir, loglevel, bname='logfile.log', append=False, logname=None)

Pass logging file location to logging.

Parameters

in_dir : str
Directory to save log file to. Can be None to use bname directly.
loglevel : str
The name of the log level to use, e.g. "debug" or 10
bname : str, optional, defaults to "logfile.log"
The basename of the log file to save to.
append : bool, optional, defaults to False
 
logname : str, optional, defaults to None
If provided, creates this logger. Otherwise uses the root logger.

Returns

None
 
Expand source code
def setup_text_logging(
    in_dir, loglevel, bname="logfile.log", append=False, logname=None
):
    """
    Pass logging file location to logging.

    Parameters
    ----------
    in_dir : str
        Directory to save log file to. Can be None to use bname directly.
    loglevel : str
        The name of the log level to use, e.g. "debug" or 10
    bname : str, optional, defaults to "logfile.log"
        The basename of the log file to save to.
    append: bool, optional, defaults to False
    logname : str, optional, defaults to None
        If provided, creates this logger. Otherwise uses the root logger.

    Returns
    -------
    None

    """
    numeric_level = convert_log_level(loglevel)

    if in_dir is None:
        fname = bname
    else:
        fname = os.path.join(in_dir, bname)
    if not append:
        if os.path.isfile(fname):
            open(fname, "w").close()

    root = logging.getLogger(logname)
    root.setLevel(numeric_level)
    file_handler = logging.FileHandler(fname)
    file_handler.setFormatter(
        logging.Formatter(
            fmt="%(levelname)s: %(asctime)s %(message)s",
            datefmt="%d/%m/%Y %I:%M:%S %p",
        )
    )
    root.addHandler(file_handler)
    # Encoding only supported in 3.9+
    # logging.basicConfig(
    #     filename=fname,
    #     # encoding="utf-8",
    #     level=numeric_level,
    #     format="%(levelname)s: %(asctime)s %(message)s",
    #     datefmt="%d/%m/%Y %I:%M:%S %p",
    # )
    mpl_logger = logging.getLogger("matplotlib")
    mpl_logger.setLevel(level=logging.WARNING)

Classes

class FileLogger (name='main', level='warning')

A logger that prints to a file.

Expand source code
class FileLogger:
    """A logger that prints to a file."""

    def __init__(self, name="main", level="warning"):
        self.name = name
        self.logger = None
        self.level = level
        self.create_logger()

    def init_logging(self):
        out_loc = self.get_default_log_location()
        setup_text_logging(None, self.level, out_loc, append=True, logname=self.name)

    def create_logger(self):
        if self.logger is None:
            self.logger = logging.getLogger(self.name)
        if len(self.get_handlers()) == 0:
            self.init_logging()

    def info(self, msg, **kwargs):
        self.logger.info(msg, **kwargs)

    def warning(self, msg, **kwargs):
        self.logger.warning(msg, **kwargs)

    def error(self, msg, **kwargs):
        self.logger.error(msg, **kwargs)

    def critical(self, msg, **kwargs):
        self.logger.critical(msg, **kwargs)

    def debug(self, msg, **kwargs):
        self.logger.debug(msg, **kwargs)

    def set_level(self, loglevel):
        try:
            numeric_level = int(loglevel)
        except BaseException:
            numeric_level = getattr(logging, loglevel.upper(), None)
        if not isinstance(numeric_level, int):
            raise ValueError("Invalid log level: %s" % loglevel)

        self.logger.setLevel(numeric_level)
        filename = self.get_default_log_location()
        print("See {} for {} level logs".format(filename, loglevel))

    def get_default_log_location(self):
        home = os.path.expanduser("~")
        out_loc = os.path.join(home, ".skm_python", f"{self.name}.log")
        os.makedirs(os.path.dirname(out_loc), exist_ok=True)

        return out_loc

    def read_log_file(self):
        loc = self.get_default_log_location()
        if os.path.exists(loc):
            with open(loc, "r") as f:
                out = f.read().strip()
            return out
        else:
            return "No log file exists."

    def clear_log_file(self):
        self.clear_handlers()

        loc = self.get_default_log_location()
        if os.path.exists(loc):
            os.remove(loc)

    def clear_handlers(self):
        handlers = self.logger.handlers[:]
        for handler in handlers:
            handler.close()
            self.logger.removeHandler(handler)

    def get_handlers(self):
        return self.logger.handlers[:]

Methods

def clear_handlers(self)
Expand source code
def clear_handlers(self):
    handlers = self.logger.handlers[:]
    for handler in handlers:
        handler.close()
        self.logger.removeHandler(handler)
def clear_log_file(self)
Expand source code
def clear_log_file(self):
    self.clear_handlers()

    loc = self.get_default_log_location()
    if os.path.exists(loc):
        os.remove(loc)
def create_logger(self)
Expand source code
def create_logger(self):
    if self.logger is None:
        self.logger = logging.getLogger(self.name)
    if len(self.get_handlers()) == 0:
        self.init_logging()
def critical(self, msg, **kwargs)
Expand source code
def critical(self, msg, **kwargs):
    self.logger.critical(msg, **kwargs)
def debug(self, msg, **kwargs)
Expand source code
def debug(self, msg, **kwargs):
    self.logger.debug(msg, **kwargs)
def error(self, msg, **kwargs)
Expand source code
def error(self, msg, **kwargs):
    self.logger.error(msg, **kwargs)
def get_default_log_location(self)
Expand source code
def get_default_log_location(self):
    home = os.path.expanduser("~")
    out_loc = os.path.join(home, ".skm_python", f"{self.name}.log")
    os.makedirs(os.path.dirname(out_loc), exist_ok=True)

    return out_loc
def get_handlers(self)
Expand source code
def get_handlers(self):
    return self.logger.handlers[:]
def info(self, msg, **kwargs)
Expand source code
def info(self, msg, **kwargs):
    self.logger.info(msg, **kwargs)
def init_logging(self)
Expand source code
def init_logging(self):
    out_loc = self.get_default_log_location()
    setup_text_logging(None, self.level, out_loc, append=True, logname=self.name)
def read_log_file(self)
Expand source code
def read_log_file(self):
    loc = self.get_default_log_location()
    if os.path.exists(loc):
        with open(loc, "r") as f:
            out = f.read().strip()
        return out
    else:
        return "No log file exists."
def set_level(self, loglevel)
Expand source code
def set_level(self, loglevel):
    try:
        numeric_level = int(loglevel)
    except BaseException:
        numeric_level = getattr(logging, loglevel.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError("Invalid log level: %s" % loglevel)

    self.logger.setLevel(numeric_level)
    filename = self.get_default_log_location()
    print("See {} for {} level logs".format(filename, loglevel))
def warning(self, msg, **kwargs)
Expand source code
def warning(self, msg, **kwargs):
    self.logger.warning(msg, **kwargs)
class FileStdoutLogger (name='stdout_logger')

A logger that prints to stdout and to a file.

Expand source code
class FileStdoutLogger:
    """A logger that prints to stdout and to a file."""

    def __init__(self, name="stdout_logger"):
        self.name = name
        self.logger = None
        self.create_logger()

    def init_logging(self):
        self.logger.setLevel(logging.INFO)
        out_loc = self.get_default_log_location()
        output_file_handler = logging.FileHandler(out_loc)
        stdout_handler = logging.StreamHandler(sys.stdout)

        self.logger.addHandler(output_file_handler)
        self.logger.addHandler(stdout_handler)

    def create_logger(self):
        if self.logger is None:
            self.logger = logging.getLogger(self.name)
        if len(self.get_handlers()) == 0:
            self.init_logging()

    def print(self, msg):
        self.logger.info(msg)

    def get_default_log_location(self):
        home = os.path.expanduser("~")
        out_loc = os.path.join(home, ".skm_python", f"{self.name}.log")
        os.makedirs(os.path.dirname(out_loc), exist_ok=True)

        return out_loc

    def read_log_file(self):
        loc = self.get_default_log_location()
        if os.path.exists(loc):
            with open(loc, "r") as f:
                out = f.read().strip()
            return out
        else:
            return "No log file exists."

    def clear_log_file(self):
        # handlers = self.logger.handlers[:]
        # for handler in handlers:
        #     handler.close()
        #     self.logger.removeHandler(handler)

        loc = self.get_default_log_location()
        if os.path.exists(loc):
            open(loc, "w").close()

    def get_handlers(self):
        return self.logger.handlers[:]

Methods

def clear_log_file(self)
Expand source code
def clear_log_file(self):
    # handlers = self.logger.handlers[:]
    # for handler in handlers:
    #     handler.close()
    #     self.logger.removeHandler(handler)

    loc = self.get_default_log_location()
    if os.path.exists(loc):
        open(loc, "w").close()
def create_logger(self)
Expand source code
def create_logger(self):
    if self.logger is None:
        self.logger = logging.getLogger(self.name)
    if len(self.get_handlers()) == 0:
        self.init_logging()
def get_default_log_location(self)
Expand source code
def get_default_log_location(self):
    home = os.path.expanduser("~")
    out_loc = os.path.join(home, ".skm_python", f"{self.name}.log")
    os.makedirs(os.path.dirname(out_loc), exist_ok=True)

    return out_loc
def get_handlers(self)
Expand source code
def get_handlers(self):
    return self.logger.handlers[:]
def init_logging(self)
Expand source code
def init_logging(self):
    self.logger.setLevel(logging.INFO)
    out_loc = self.get_default_log_location()
    output_file_handler = logging.FileHandler(out_loc)
    stdout_handler = logging.StreamHandler(sys.stdout)

    self.logger.addHandler(output_file_handler)
    self.logger.addHandler(stdout_handler)
def print(self, msg)
Expand source code
def print(self, msg):
    self.logger.info(msg)
def read_log_file(self)
Expand source code
def read_log_file(self):
    loc = self.get_default_log_location()
    if os.path.exists(loc):
        with open(loc, "r") as f:
            out = f.read().strip()
        return out
    else:
        return "No log file exists."