"""Tasks related to logging within scripts"""
import os
import sys
import logging
import datetime
from typing import Optional
def _get_log_filename(log_prefix: str = "", date_format: str = "%Y-%m-%d") -> str:
"""Generate standard format log-file names.
Args:
log_prefix: prefix for log filename
date_format: the date format to be used in the file name
Returns:
filename in format {log_prefix}_{current_date}
"""
# When log_prefix is populated, append an underscore and
# ignore any trailing underscore provided by the user
if log_prefix != "":
log_prefix = f"{log_prefix.rstrip('_')}_"
date = datetime.datetime.now().strftime(date_format)
log_filename = f"{log_prefix}{date}.log"
return log_filename
def _add_handler(logger: logging.Logger, log_directory: Optional[str] = None, **kwargs) -> None:
"""Adds a handler to a logger, either a logging.StreamHandler if log_directory is None
otherwise a logging.FileHandler piped to the directory specified.
Args:
logger: the logging.Logger class to which you would like to add a handler
log_directory: Optional log directory to pass. If not None a FileHandler is added,
otherwise a StreamHandler
**kwargs: Keyword arguments for the _get_log_filename
"""
if log_directory is None:
handler = logging.StreamHandler()
else:
if not os.path.exists(log_directory):
os.makedirs(log_directory)
handler = logging.FileHandler(os.path.join(log_directory, _get_log_filename(**kwargs)))
# for some reason you need to set both of these - setting to same value to avoid confusion
logger.setLevel(logging.INFO)
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(levelname)s <%(thread)d> [%(asctime)s] %(name)s <%(filename)s:%(lineno)d> %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
[docs]def create(
name: str,
*,
log_to_terminal: bool = True,
log_directory: Optional[str] = None,
log_prefix: str = "",
date_format: str = "%Y-%m-%d",
) -> logging.Logger:
"""Return logger object with pre-defined format.
Log file will be located under log_directory with file name
<log_prefix>_<date>.log, quashing extra separating underscores. Defaults to <date>.log.
For use in scripts only. To log in module files, use the standard library `logging` module with
a module-level logger and enable package logging. See
https://docs.python.org/3/howto/logging.html#advanced-logging-tutorial
>>> log = logging.getLogger(__name__)
Args:
name: This sets the name of your logger instance. It does not affect the file name.
To change the filename use log_prefix
log_to_terminal: Boolean indicating whether or not to log messages to the terminal.
log_directory: The directory to place log files inside
log_prefix: The string to prepend to the date in the log file name.
date_format: format string for date suffix on log file name
Returns:
Logger object
"""
if type(name) is not str:
raise TypeError
if name == "":
raise ValueError
logger = logging.getLogger(name)
if log_to_terminal:
_add_handler(logger, log_prefix=log_prefix, date_format=date_format)
if log_directory is not None:
_add_handler(
logger, log_directory=log_directory, log_prefix=log_prefix, date_format=date_format
)
# enable logging of uncaught exceptions
def log_exception(exc_type, exc_value, exc_traceback) -> None:
logger.error("Uncaught exception:", exc_info=(exc_type, exc_value, exc_traceback))
sys.excepthook = log_exception
return logger
[docs]def set_logging_level(logger_name: str, level: str) -> None:
"""A useful method for setting logging level for all a given logger and its handlers.
Args:
logger_name: the name of the logger for which to set the level
level: log level to use. The set available from core logging package is 'debug', 'info',
'warning', 'error'
"""
log = logging.getLogger(logger_name)
logging_level = logging.getLevelName(level.upper())
log.setLevel(logging_level)
for x in log.handlers:
x.setLevel(logging_level)
[docs]def enable_package_logging(
package_name: str,
*,
log_to_terminal: bool = True,
log_directory: Optional[str] = None,
level: Optional[str] = None,
log_prefix: str = "",
date_format: str = "%Y-%m-%d",
) -> None:
"""A helper function to enable package logging for any package following
python best practices for logging names (i.e. logger name == package.module.submodule).
Args:
package_name: the name of the package for which to enable logging
log_to_terminal: Boolean indicating whether or not to log messages to the terminal
log_directory: optional log directory which the package will write logs
level: optional level to specify, default is WARNING (inherited from base logging package)
log_prefix: Optional prefix for log files, if None will be blank string
date_format: Optional date format for log file
"""
package_logger = logging.getLogger(package_name)
_add_handler(package_logger, log_directory, log_prefix=log_prefix, date_format=date_format)
if log_to_terminal:
_add_handler(package_logger, log_prefix=log_prefix, date_format=date_format)
if level is not None:
set_logging_level(package_name, level)