import logging
import logging.handlers
import os
import threading
import coloredlogs
from bioat.exceptions import BioatInvalidParameterError
__all__ = ["LoggerManager"]
_logger_cache = {}
_logger_lock = threading.Lock()
[docs]
class LoggerManager:
LOG_FORMAT = "%(asctime)s.%(msecs)03d - [%(name)s] - %(filename)s[line:%(lineno)4d] - %(levelname)+8s: %(message)s"
DEFAULT_LEVEL = os.getenv("BIOAT_LOG_LEVEL", "INFO").upper()
[docs]
def __init__(
self,
log_level: str = DEFAULT_LEVEL,
mod_name: str = "bioat.logger",
cls_name: str | None = None,
func_name: str | None = None,
):
"""Initialize the LoggerManager with default log level and module name.
Args:
log_level (str, optional): Default logger level. Defaults to "ERROR".
mod_name (str, optional): Module name. Defaults to "fbt".
cls_name (str, optional): The name of the class for which the logger is created.
Defaults to None.
func_name (str, optional): The name of the function for which the logger is created.
Defaults to None.
"""
self.log_level = self._get_log_level(log_level)
self.mod_name = mod_name
self.cls_name = cls_name
self.func_name = func_name
self.logger = self._get_or_create_logger()
# self.logger.propagate = False # TODO
[docs]
@staticmethod
def get_logger(name: str, level: str = DEFAULT_LEVEL) -> logging.Logger:
"""直接返回 logger 实例,用于标准 logging 接口."""
return LoggerManager(mod_name=name, log_level=level).logger
def _get_log_level(self, log_level: str) -> int:
if log_level.upper() not in logging._nameToLevel:
msg = (
f"Invalid log level: {log_level}. "
"Choose from CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET."
)
raise BioatInvalidParameterError(
msg,
)
return logging._nameToLevel[log_level.upper()]
def _get_logger_name(self) -> str:
parts = [self.mod_name]
if self.cls_name:
parts.append(self.cls_name)
if self.func_name:
parts.append(self.func_name)
return ".".join(parts)
def _get_or_create_logger(self) -> logging.Logger:
name = self._get_logger_name()
with _logger_lock:
if name in _logger_cache:
return _logger_cache[name]
logger = logging.getLogger(name)
logger.setLevel(self.log_level)
logger.propagate = False
if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
coloredlogs.install(
fmt=self.LOG_FORMAT,
level=self.log_level,
logger=logger,
field_styles={
"asctime": {"color": "yellow", "bold": True},
"name": {"color": "blue", "bold": True},
"filename": {"color": "cyan"},
"lineno": {"color": "green", "bold": True},
"levelname": {"color": "magenta", "bold": True},
},
level_styles={
"critical": {"color": "red", "bold": True},
"error": {"color": "red"},
"warning": {"color": "yellow"},
"info": {"color": "green"},
"debug": {"color": "blue"},
},
)
_logger_cache[name] = logger
return logger
[docs]
def set_names(self, cls_name: str | None = None, func_name: str | None = None):
"""重新设置类名和函数名,会重新绑定 logger."""
self.cls_name = cls_name
self.func_name = func_name
# 会根据新的 name 自动获取缓存或新建 logger(线程安全)
# 可能只是应用了同名 logger 的配置,并不会实例化新的 logger而是指向同名旧 logger
# 也可能 logger 会被新的 logger 实例替换
self.logger = self._get_or_create_logger()
[docs]
def set_level(self, log_level: str):
"""更新日志等级."""
self.log_level = self._get_log_level(log_level)
self.logger.setLevel(self.log_level)
[docs]
def mute(self):
"""将日志等级设置为 NOTSET."""
self.set_level(log_level="NOTSET") # NOTSET is higher than CRITICAL
[docs]
def set_file(
self,
file: str,
mode: str = "a",
max_bytes: int = 10 * 1024 * 1024,
backup_count: int = 5,
):
"""设置普通文件日志."""
if mode not in {"a", "w"}:
msg = "Mode must be 'a' or 'w'"
raise BioatInvalidParameterError(msg)
os.makedirs(os.path.dirname(file), exist_ok=True)
self._remove_handler_type(logging.FileHandler)
if mode == "w" or max_bytes <= 0:
handler = logging.FileHandler(file, mode=mode)
else:
handler = logging.handlers.RotatingFileHandler(
file,
mode=mode,
maxBytes=max_bytes,
backupCount=backup_count,
)
handler.setFormatter(logging.Formatter(self.LOG_FORMAT))
self.logger.addHandler(handler)
def _remove_handler_type(self, handler_type):
"""内部方法:移除已有的特定 handler 类型."""
self.logger.handlers = [
h for h in self.logger.handlers if not isinstance(h, handler_type)
]
[docs]
def add_stream_handler(self):
"""添加 console handler(不会重复添加)."""
if not any(isinstance(h, logging.StreamHandler) for h in self.logger.handlers):
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter(self.LOG_FORMAT))
self.logger.addHandler(stream_handler)
if __name__ == "__main__":
import os
import tempfile
print("=== LoggerManager 内部测试开始 ===")
# 测试 logger 创建与等级
lm = LoggerManager(mod_name="test.module", log_level="DEBUG")
assert isinstance(lm.logger, logging.Logger)
assert lm.logger.level == logging.DEBUG
print("✅ logger 创建与等级 OK")
# 测试 set_names
lm.set_names(cls_name="TestClass", func_name="test_func")
assert "TestClass" in lm.logger.name
assert "test_func" in lm.logger.name
print("✅ set_names OK:", lm.logger.name)
# 测试 set_level
lm.set_level("INFO")
assert lm.logger.level == logging.INFO
lm.logger.info("✅This is an info message.")
lm.logger.debug("❌This debug message should not appear.")
lm.logger.warning("✅This is a warning message.")
lm.logger.error("✅This is an error message.")
lm.logger.critical("✅This is a critical message.")
lm.set_level("DEBUG")
assert lm.logger.level == logging.DEBUG
lm.logger.debug("✅This debug message should now appear.")
lm.logger.info("✅This is another info message after level change.")
print("✅ set_level OK")
# 测试非法等级
try:
lm.set_level("FAKE_LEVEL")
except BioatInvalidParameterError as e:
print("✅ 错误等级捕获 OK:", type(e), e)
else:
msg = "❌ 错误等级未正确捕获"
raise AssertionError(msg)
# 测试文件日志
with tempfile.TemporaryDirectory() as tmpdir:
file_path = os.path.join(tmpdir, "test.log")
lm.set_file(file_path, mode="w")
lm.logger.warning("test file log entry")
with open(file_path) as f:
content = f.read()
assert "test file log entry" in content
print("✅ 文件日志写入 OK:", file_path)
# 测试 add_stream_handler 不重复
# 仅统计 StreamHandler 类型的 handler 数量
def count_stream_handlers(logger):
return sum(1 for h in logger.handlers if isinstance(h, logging.StreamHandler))
count_before = count_stream_handlers(lm.logger)
lm.add_stream_handler()
lm.add_stream_handler()
count_after = count_stream_handlers(lm.logger)
# 至多只能添加一个额外的 StreamHandler(如果 coloredlogs 没装)
assert count_after == count_before, "Unexpected number of StreamHandlers"
print(
"✅ StreamHandler 不重复添加 OK:",
f"StreamHandlers: before={count_before}, after={count_after}",
)
# 静态方式测试
static_logger = LoggerManager.get_logger("static.test", level="WARNING")
assert isinstance(static_logger, logging.Logger)
assert "static.test" in static_logger.name
print("✅ get_logger 静态方法 OK:", static_logger.name)
lm = LoggerManager(mod_name="bioat.lib.align", log_level="DEBUG")
lm.logger.info("Hello Logger!")
# 修改类名函数名
lm.set_names(cls_name="MyClass", func_name="run")
lm.logger.debug("With class+func name.")
# 静态方式快速获取 logger
logger = LoggerManager.get_logger("bioat.io", "INFO")
logger.info("Hello from static logger")
# 初始化 LoggerManager
lm = LoggerManager(log_level="debug")
print("🎉 所有 LoggerManager 内部测试通过!")
print("_logger_cache:", _logger_cache) # 打印缓存的 logger 名称