Thank you Hans for the reply. To date, I am not planning on using this logger in a code shared as library. I use it only in my application as a god-tier class that has customize behaviour. My wish is to be able to setup the logger once so that every time I need the logger, I simply call the logging.getLogger(name)
or logging.getLogger(name.subname)
to retrieve the instance of my logger (which should be a singleton AFAIK reading from the docs here and there).
Reading online, I ended up with the following design:
A custom logger class to add custom levels over the base logging.Logger
class as follows:
import logging
import platform
import sys
from enum import Enum
from colorama import Fore, Style
class CustomLogger(logging.Logger):
def __init__(
self,
name: str,
level: int = logging.NOTSET,
) -> None:
super().__init__(name, level)
# Add custom log levels
self._addCustomLevels()
def _addCustomLevels(self) -> None:
# Helper function to create a logging method for a specific log level
def make_log_method(level: CustomLevels):
def log_method(self, message, *args, **kwargs):
# NOTE: optimize by checking level before logging
# https://docs.python.org/3/howto/logging.html#optimization
if self.isEnabledFor(level.value):
self._log(level.value, message, args, **kwargs)
return log_method
# Register custom log levels and methods
for level in CustomLevels:
logging.addLevelName(level.value, level.name)
setattr(CustomLogger, level.name.lower(), make_log_method(level))
# not recommended (?): add the custom level to the logging namespace as well
# setattr(logging, level.name, level.value)
# base levels should be accessed within the logging namespace
# e.g. with logging.INFO, logging.DEBUG, etc.
# custom levels should be accessed withing the following Enum class
# e.g. with CustomLevels.QUERY, CustomLevels.PROFILE, etc.
class CustomLevels(Enum):
PROFILE = 19
QUERY = 21
EMAIL = 31
While the rest of the design is just a âcustomizationâ of the logger that can be accomplished by adding the desired behaviour trought formatters/handlers with a custom logic as follows:
def setup_logging(name: str | None = None, extra: dict | None = None):
# NOTE: this is important
# set the custom class as the default class managed by logging
logging.setLoggerClass(CustomLogger)
# NOTE: now use the logging APIs to configure the logger instance
# get the logger
logger = logging.getLogger(name)
# NOTE: this is needed because I don't know how to disable double records tracing from the root and my custom logger (I guess) or to make mine as the root
logger.propagate = False
# set the logger level
level = logging.DEBUG if debug_boolean else logging.INFO
logger.setLevel(level)
# Set up the console handler
setConsoleHandler(logger, datefmt=mydatefmt, verbose=myverbose)
# Set up SMTP handler for emails, if required
setCustomSMTPHandler(logger, mysmtpinfo)
# Set up file handler for logging to file, if required
filename = setFileConfigs(mycustomargs)
setFileHandler(logger, filename, verbose, datefmt)
# Optmize logging if not verbose
optimizeLogging(verbose)
return logger
For instance, the custom console handler is setup as follows:
def setConsoleHandler(logger: CustomLogger, datefmt: str | None = None, verbose: bool = False) -> None:
# Set the console handler with the custom formatter
console_handler = logging.StreamHandler(sys.stdout)
console_formatter = CustomColoredFormatter(datefmt, verbose)
# NOTE: add custom colored formatter only the console handler
console_handler.setFormatter(console_formatter)
console_handler.setLevel(logger.getEffectiveLevel())
logger.addHandler(console_handler)
class CustomFormatter(logging.Formatter):
def __init__(
self,
fmt: str | None = None,
datefmt: str | None = None,
verbose: bool = False,
):
self.verbose = verbose
# NOTE: colored console output support on Windows
if platform.system() == "Windows":
# https://github.com/tartley/colorama
# On Windows, enable ANSI color codes
from colorama import just_fix_windows_console
just_fix_windows_console()
# from colorama import init
# init()
# Setup the format info based on verbosity
if self.verbose:
# the syntax "-8" add space char to levelname if levelname length is less than 7
self.format_info = "[%(name)s - %(levelname)-8s%(asctime)s (P:%(process)d - T:%(thread)d)]: "
else:
self.format_info = "[%(levelname)-8s%(asctime)s]: "
self.format_message = "%(message)s"
fmt = self.format_info + self.format_message
# Call the base class constructor with the format and date format
# NOTE: use the default format style "%" of the constructor
super().__init__(fmt=fmt, datefmt=datefmt)
class CustomColoredFormatter(CustomFormatter):
# NOTE: the console formatter subclasses the file formatter and add colored logs
def __init__(
self,
datefmt: str | None = None,
verbose: bool = False
):
self._datefmt = datefmt
# NOTE: colored console output support on Windows
if platform.system() == "Windows":
# https://github.com/tartley/colorama
# On Windows, enable ANSI color codes
from colorama import just_fix_windows_console
just_fix_windows_console()
# Call the base class constructor with the format and date format
# NOTE: use the default format style "%" of the constructor
super().__init__(datefmt=datefmt, verbose=verbose)
def _make_message(self, ansi_code: str) -> str:
"""Takes the log string and formats it with color
Args:
ansi_code (str): ANSI foreground colored code
Returns:
str: the formatted colored text
"""
return ansi_code + self.format_info + Style.RESET_ALL + self.format_message
# mappings between log levels and ANSI colored codes
FORMATS = {
logging.DEBUG: Fore.MAGENTA,
CustomLevels.PROFILE.value: Fore.GREEN,
logging.INFO: Fore.WHITE,
CustomLevels.QUERY.value: Fore.CYAN,
logging.WARNING: Fore.YELLOW,
CustomLevels.EMAIL.value: Fore.YELLOW,
logging.ERROR: Fore.RED,
logging.CRITICAL: Fore.RED,
}
def format(self, record: logging.LogRecord) -> str:
color = self.FORMATS.get(record.levelno, Style.RESET_ALL)
log_fmt = self._make_message(color)
formatter = logging.Formatter(log_fmt, self._datefmt)
return formatter.format(record)
This worked nicely until I found out that to date I am not able to have a properly working logger by retrieving the logger in another file, for instance in other.py
:
import logging
logger = logging.getLogger("app")
def other():
logger.profile('This is a profile message from other.') # does not work, i.e. logger has no "profile" method
And the logger is setup in main.py
as follows:
import logging
from mypath import setup_logging
import other
if __name__ == "__main__":
logger = setup_logging(name="app", extra=myextra)
logger.profile('This is a profile message.') # works
I guess the solution is trivial but I am having an hard time understanding the missing piece. I suspect that somehow the configuration is not saved in the inner manager when saving the class as the default logger.