Files
2026-07-13 12:28:27 +08:00

147 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding: utf-8
import os
import logging
from pathlib import Path
from datetime import datetime
from logging.handlers import RotatingFileHandler
from rich.logging import RichHandler
class TruncatingFileHandler(RotatingFileHandler):
"""超过 maxBytes 后 truncate 文件,不保留任何备份文件"""
_TAIL_LINES = 10 # truncate 前保留的末尾行数
def doRollover(self):
# truncate 前先读旧文件末尾几行
tail = ''
try:
with open(self.baseFilename, 'r', encoding=self.encoding) as f:
lines = f.readlines()
tail = ''.join(lines[-self._TAIL_LINES:]).rstrip()
except Exception:
pass
if self.stream:
self.stream.close()
self.stream = None
# 用 'w' 模式重开,直接清空文件从头写
self.stream = open(self.baseFilename, 'w', encoding=self.encoding)
self.stream.write(f'--- Log truncated at {datetime.now()}\n')
if tail:
self.stream.write(f'--- Last {self._TAIL_LINES} lines of previous entries:\n{tail}\n\n')
self.stream.flush()
class Logger:
"""日志系统管理器"""
_loggers = {}
@classmethod
def setup(cls, name: str, log_dir: str = None, level: str = 'INFO', max_bytes: int = 10 * 1024 * 1024, log_filename: str = None):
"""
设置并返回一个日志记录器
Args:
name: 日志记录器名称(通常是 'server' 或 'client'
log_dir: 日志文件目录,默认为项目根目录下的 logs 文件夹
level: 日志级别,可选值:'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
max_bytes: 单个日志文件最大大小,默认 10MB
backup_count: 保留的日志文件数量,默认 5 个
log_filename: 自定义日志文件名前缀,如果不填则默认使用 name 或 'root'
Returns:
logging.Logger: 配置好的日志记录器
"""
# 设置日志级别
file_log_level = getattr(logging, level.upper(), logging.INFO)
console_log_level = logging.WARNING # 控制台强制只打印 WARNING 及以上
# 如果已经初始化过,更新级别并返回
if name in cls._loggers:
logger = cls._loggers[name]
logger.setLevel(min(file_log_level, console_log_level))
for handler in logger.handlers:
if isinstance(handler, RotatingFileHandler):
handler.setLevel(file_log_level)
elif isinstance(handler, logging.StreamHandler):
handler.setLevel(console_log_level)
return logger
# 创建日志记录器
logger = logging.getLogger(name if name else None)
logger.setLevel(min(file_log_level, console_log_level))
# 确保不会传播到 root logger
if name:
logger.propagate = False
# 确定日志目录
if log_dir is None:
from config_client import BASE_DIR
log_dir = os.path.join(BASE_DIR, 'logs')
# 创建日志目录
Path(log_dir).mkdir(parents=True, exist_ok=True)
# 1. 文件处理器(根据传入 level 记录)
file_name_prefix = log_filename or name or 'root'
log_file = os.path.join(log_dir, f'{file_name_prefix}_latest.log')
formatter = logging.Formatter(
fmt='%(asctime)s.%(msecs)03d %(levelname)-5s [%(filename)20s:%(lineno)-3d] %(message)s',
datefmt='%H:%M:%S'
)
file_handler = TruncatingFileHandler(
log_file,
maxBytes=max_bytes,
encoding='utf-8'
)
file_handler.setLevel(file_log_level)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# 2. 控制台处理器(固定 WARNING 及以上,使用 rich 渲染)
stream_handler = RichHandler(
level=console_log_level,
rich_tracebacks=True,
markup=True,
show_path=False
)
logger.addHandler(stream_handler)
# 缓存日志记录器
cls._loggers[name] = logger
return logger
@classmethod
def get_logger(cls, name: str):
"""
获取已创建的日志记录器,如果不存在则创建一个默认的
Args:
name: 日志记录器名称
Returns:
logging.Logger: 日志记录器
"""
if name not in cls._loggers:
# 如果 logger 还没有被初始化,先创建一个默认的(INFO 级别)
# 之后 core_client.py/core_server.py 会用正确的级别重新初始化
return cls.setup(name, level='INFO')
return cls._loggers[name]
# 便捷函数
def setup_logger(name: str, log_dir: str = None, level: str = 'INFO', **kwargs):
"""设置日志记录器的便捷函数"""
return Logger.setup(name, log_dir, level, **kwargs)
def get_logger(name: str):
"""获取日志记录器的便捷函数"""
return Logger.get_logger(name)