"""
Absfuyu: Logger
---------------
Custom Logger Module
Version: 5.1.0
Date updated: 10/03/2025 (dd/mm/yyyy)
Usage:
------
>>> from absfuyu.logger import logger, LogLevel
>>> logger.setLevel(LogLevel.DEBUG)
>>> logger.debug("This logs!")
"""
# Module level
# ---------------------------------------------------------------------------
__all__ = [
# logger
"logger",
"compress_for_log",
# log level
"LogLevel",
]
# Library
# ---------------------------------------------------------------------------
import logging
import math
from logging.handlers import RotatingFileHandler as _RFH
from logging.handlers import TimedRotatingFileHandler as _TRFH
from pathlib import Path
from typing import Any, Optional, Union
# Setup
# ---------------------------------------------------------------------------
[docs]
class LogLevel:
"""
``logging``'s log level wrapper + custom log level
"""
TRACE: int = logging.DEBUG - 5
DEBUG: int = logging.DEBUG
INFO: int = logging.INFO
WARNING: int = logging.WARNING
ERROR: int = logging.ERROR
CRITICAL: int = logging.CRITICAL
EXTREME: int = logging.CRITICAL + 10
class _LogFormat:
"""Some log format styles"""
FULL = "[%(asctime)s] [%(process)-d] [%(module)s] [%(name)s] [%(funcName)s] [%(levelname)-s] %(message)s" # Time|ProcessID|Module|Name|Function|LogType|Message
SHORT = "[%(module)s] [%(name)s] [%(funcName)s] [%(levelname)-s] %(message)s" # Module|Name|Function|LogType|Message
CONSOLE = "%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s" # Time|LogType|Function|LineNumber|Message
FILE = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s" # Time|LogType|FileName|Function|LineNumber|Message
# Create a custom logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.WARNING)
# Create handlers
## Console log handler
console_handler = logging.StreamHandler()
console_handler.setLevel(LogLevel.TRACE) # Minimum log level
console_handler.setFormatter(
logging.Formatter(_LogFormat.CONSOLE, datefmt="%Y-%m-%d %H:%M:%S")
)
# logger.addHandler(console_handler)
logger.addHandler(logging.NullHandler())
# Functions
# ---------------------------------------------------------------------------
def _compress_list_for_print(iterable: list, max_visible: Optional[int] = 5) -> str:
"""
Compress the list to be more log-readable
iterable: list
max_visible: Maximum items can be printed on screen (Minimum: 3)
"""
if max_visible is None or max_visible <= 2:
max_visible = 5
if len(iterable) <= max_visible:
return str(iterable)
else:
# logger.debug(f"Max vis: {max_visible}")
if max_visible % 2 == 0:
cut_idx_1 = math.floor(max_visible / 2) - 1
cut_idx_2 = math.floor(max_visible / 2)
else:
cut_idx_1 = cut_idx_2 = math.floor(max_visible / 2)
# logger.debug(f"Cut pos: {(cut_idx_1, cut_idx_2)}")
# temp = [iterable[:cut_idx_1], ["..."], iterable[len(iterable)-cut_idx_2:]]
# out = list(chain.from_iterable(temp))
# out = [*iterable[:cut_idx_1], "...", *iterable[len(iterable)-cut_idx_2:]] # Version 2
out = f"{str(iterable[:cut_idx_1])[:-1]}, ..., {str(iterable[len(iterable) - cut_idx_2 :])[1:]}" # Version 3
# logger.debug(out)
return f"{out} [Len: {len(iterable)}]"
def _compress_string_for_print(text: str, max_visible: Optional[int] = 120) -> str:
"""
Compress the string to be more log-readable
text: str
max_visible: Maximum text can be printed on screen (Minimum: 5)
"""
if max_visible is None or max_visible <= 5:
max_visible = 120
text = text.replace("\n", " ") # Remove new line
# logger.debug(text)
if len(text) <= max_visible:
return str(text)
else:
cut_idx = math.floor((max_visible - 3) / 2)
temp = f"{text[:cut_idx]}...{text[len(text) - cut_idx :]}"
return f"{temp} [Len: {len(text)}]"
[docs]
def compress_for_log(object_: Any, max_visible: Optional[int] = None) -> str:
"""
Compress the object to be more log-readable
:param object_: Object
:param max_visible: Maximum objects can be printed on screen
:returns: Compressed log output
:rtype: str
"""
if isinstance(object_, list):
return _compress_list_for_print(object_, max_visible)
elif isinstance(object_, (set, tuple)):
return _compress_list_for_print(list(object_), max_visible)
elif isinstance(object_, dict):
temp = [{k: v} for k, v in object_.items()]
return _compress_list_for_print(temp, max_visible)
elif isinstance(object_, str):
return _compress_string_for_print(object_, max_visible)
else:
try:
return _compress_string_for_print(str(object_), max_visible)
except Exception:
return object_ # type: ignore
# Class
# ---------------------------------------------------------------------------
class _CustomLogger:
"""
Custom logger [W.I.P]
Create a custom logger
*Useable but maybe unstable*
"""
def __init__(
self,
name: str,
cwd: Union[str, Path] = ".",
log_format: Optional[str] = None,
*,
save_log_file: bool = False,
separated_error_file: bool = False,
timed_log: bool = False,
date_log_format: Optional[str] = None,
error_log_size: int = 1_000_000, # 1 MB
) -> None:
"""
:param name: Custom logger name
:param cwd: Current working directory
:param log_format: Log format
:param save_log_file: Save logs to log file (default: False)
:param separated_error_file: Save error logs into a separated file (Default: False)
:param timed_log: Split log file every day. Requirement: `save_log_file = True` (Default: False)
:param date_log_format: Date format in log
:param error_log_size: Error log file max size (Default: 1 MB)
"""
self._cwd = Path(cwd)
self.log_folder = self._cwd.joinpath("logs")
self.log_folder.mkdir(
exist_ok=True, parents=True
) # Does not throw exception when folder existed
self.name = name
self.log_file = self.log_folder.joinpath(f"{name}.log")
# Create a custom logger
try:
self.logger = logging.getLogger(self.name)
except Exception:
try:
self.logger = logging.getLogger(__name__)
except Exception:
self.logger = logging.getLogger()
self.logger.setLevel(logging.DEBUG)
if date_log_format is None:
_date_format = "%Y-%m-%d %H:%M:%S"
else:
_date_format = date_log_format
## Console log handler
if log_format is None:
# Time|LogType|Function|LineNumber|Message
_log_format = (
"%(asctime)s [%(levelname)5s] %(funcName)s:%(lineno)3d: %(message)s"
)
else:
_log_format = log_format
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG) # Minimum log level
_console_log_format = _log_format # Create formatters and add it to handlers
_console_formatter = logging.Formatter(
_console_log_format, datefmt=_date_format
)
console_handler.setFormatter(_console_formatter)
self._console_handler = console_handler
self.logger.addHandler(self._console_handler) # Add handlers to the logger
## Log file handler
if save_log_file:
if log_format is None:
# Time|LogType|FileName|Function|LineNumber|Message
_log_format = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s"
else:
_log_format = log_format
file_handler = logging.FileHandler(
self.log_file, mode="a", encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
_file_log_format = _log_format
_file_formatter = logging.Formatter(_file_log_format, datefmt=_date_format)
file_handler.setFormatter(_file_formatter)
self._file_handler = file_handler
self.logger.addHandler(self._file_handler)
if timed_log:
## Time handler (split log every day)
time_handler = _TRFH(
self.log_folder.joinpath(f"{self.name}_timed.log"),
when="midnight",
interval=1,
encoding="utf-8",
)
time_handler.setLevel(logging.DEBUG)
time_handler.setFormatter(_file_formatter)
self._time_handler = time_handler
self.logger.addHandler(self._time_handler)
# | Value | Type of interval |
# |:--------:|:---------------------:|
# | S | Seconds |
# | M | Minutes |
# | H | Hours |
# | D | Days |
# | W | Week day (0=Monday) |
# | midnight | Roll over at midnight |
## Error and above log handler
if separated_error_file:
if log_format is None:
# Time|LogType|FileName|Function|LineNumber|Message
_log_format = "%(asctime)s [%(levelname)5s] %(filename)s:%(funcName)s:%(lineno)3d: %(message)s"
else:
_log_format = log_format
error_handler = _RFH(
self.log_folder.joinpath(f"{self.name}_error.log"),
maxBytes=error_log_size,
backupCount=1,
encoding="utf-8",
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(_log_format) # type: ignore
self._error_handler = error_handler
self.logger.addHandler(self._error_handler)
def __str__(self) -> str:
return f"{self.__class__.__name__}({self.name})"
def __repr__(self) -> str:
return self.__str__()
@staticmethod
def _add_logging_level(
level_name: str, level_num: int, method_name: Optional[str] = None
):
"""
Comprehensively adds a new logging level to the `logging` module and the
currently configured logging class.
`level_name` becomes an attribute of the `logging` module with the value
`level_num`. `method_name` becomes a convenience method for both `logging`
itself and the class returned by `logging.getLoggerClass()` (usually just
`logging.Logger`). If `method_name` is not specified, `level_name.lower()` is
used.
To avoid accidental clobberings of existing attributes, this method will
raise an `AttributeError` if the level name is already an attribute of the
`logging` module or if the method name is already present
"""
# Original code: https://stackoverflow.com/a/35804945/1691778
if not method_name:
method_name = level_name.lower()
if hasattr(logging, level_name):
raise AttributeError(f"{level_name} already defined in logging module")
if hasattr(logging, method_name):
raise AttributeError(f"{method_name} already defined in logging module")
if hasattr(logging.getLoggerClass(), method_name):
raise AttributeError(f"{method_name} already defined in logger class")
# This method was inspired by the answers to Stack Overflow post
# http://stackoverflow.com/q/2183233/2988730, especially
# http://stackoverflow.com/a/13638084/2988730
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(level_num):
self._log(level_num, message, args, **kwargs)
def logToRoot(message, *args, **kwargs):
logging.log(level_num, message, *args, **kwargs)
logging.addLevelName(level_num, level_name)
setattr(logging, level_name, level_num)
setattr(logging.getLoggerClass(), method_name, logForLevel)
setattr(logging, method_name, logToRoot)
def add_log_level(self, level_name: str, level_num: int):
__class__._add_logging_level(level_name, level_num) # type: ignore
if level_num < logging.DEBUG:
self._console_handler.setLevel(level_num)
self.logger.setLevel(level_num)