refactor(logger): 重构日志系统以提升性能和可维护性

- 使用自定义的TimedRotatingFileHandler实现更高效的日志轮换
- 新增日志文件自动清理功能,支持按天数保留日志
- 优化日志处理器初始化流程,增加线程安全保护
- 分离不同级别日志到单独文件(all.log和error.log)
- 添加全局异常捕获和日志文件信息统计功能
This commit is contained in:
zhangtao
2025-08-14 00:39:46 +08:00
parent 82840e410f
commit d89111e220
3 changed files with 299 additions and 52 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Any, Mapping, Optional from typing import Any, Mapping, Optional, Dict, Union
from fastapi import status from fastapi import status
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
from starlette.background import BackgroundTask from starlette.background import BackgroundTask
+12 -9
View File
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
from datetime import date
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, Dict, List, Optional, Union from typing import Any, ClassVar, Dict, List, Optional, Union
@@ -69,7 +68,7 @@ class Settings(BaseSettings):
# ================================================= # # ================================================= #
CORS_ORIGIN_ENABLE: bool = True # 是否启用跨域 CORS_ORIGIN_ENABLE: bool = True # 是否启用跨域
ALLOW_ORIGINS: List[str] = ["*"] # 允许的域名列表 ALLOW_ORIGINS: List[str] = ["*"] # 允许的域名列表
# ALLOW_ORIGINS: List[str] = ["http://localhost:5180", "http://127.0.0.1:5180", "http://0.0.0.1:5180","http://172.18.52.77:5180", "http://service.fastapiadmin.com"] # 允许的域名列表 # ALLOW_ORIGINS: List[str] = ["http://localhost:5180", "http://127.0.0.1:5180"] # 允许的域名列表
ALLOW_METHODS: List[str] = ["*"] # 允许的HTTP方法 ALLOW_METHODS: List[str] = ["*"] # 允许的HTTP方法
ALLOW_HEADERS: List[str] = ["*"] # 允许的请求头 ALLOW_HEADERS: List[str] = ["*"] # 允许的请求头
ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie
@@ -141,20 +140,24 @@ class Settings(BaseSettings):
# ================================================= # # ================================================= #
# ********************* 日志配置 ******************* # # ********************* 日志配置 ******************* #
# ================================================= # # ================================================= #
LOGGER_LEVEL: str # 日志级别 LOGGER_LEVEL: str # 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
LOGGER_NAME: str = date.today().strftime(r'%Y-%m-%d.log') # 日志文件名
LOGGER_DIR: Path = BASE_DIR.joinpath('logs') LOGGER_DIR: Path = BASE_DIR.joinpath('logs')
if not LOGGER_DIR.exists(): if not LOGGER_DIR.exists():
LOGGER_DIR.mkdir(parents=True, exist_ok=True) LOGGER_DIR.mkdir(parents=True, exist_ok=True)
LOGGER_FILEPATH: Path = LOGGER_DIR.joinpath(LOGGER_NAME) # 日志文件路径
BACKUPCOUNT: int = 10 # 日志文件备份数 BACKUPCOUNT: int = 10 # 日志文件备份数
WHEN: str = 'MIDNIGHT' # 日志分割时间 WHEN: str = 'MIDNIGHT' # 日志分割时间 (MIDNIGHT, H, D, W0-W6)
INTERVAL: int = 1 # 日志分割间隔 INTERVAL: int = 1 # 日志分割间隔
ENCODING: str = 'utf-8' # 日志编码 ENCODING: str = 'utf-8' # 日志编码
LOGGER_FORMAT: str = '%(asctime)s - %(levelname)s - [%(name)s:%(filename)s:%(funcName)s:%(lineno)d] %(message)s' # 日志格式 LOGGER_FORMAT: str = '%(asctime)s - %(levelname)8s - [%(name)s:%(filename)s:%(funcName)s:%(lineno)d] %(message)s' # 日志格式
OPERATION_LOG_RECORD: bool = True # 是否记录操作日志 OPERATION_LOG_RECORD: bool = True # 是否记录操作日志
OPERATION_RECORD_METHOD: List[str] = ["POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] # 需要记录的请求方法 OPERATION_RECORD_METHOD: List[str] = ["POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] # 需要记录的请求方法
IGNORE_OPERATION_FUNCTION: List[str] = ["get_captcha_for_login"] # 忽略记录的函数 IGNORE_OPERATION_FUNCTION: List[str] = ["get_captcha_for_login"] # 忽略记录的函数
LOG_RETENTION_DAYS: int = 30 # 日志保留天数,超过此天数的日志文件将被自动清理
# 日志文件说明:
# all.log - 包含所有级别日志(应用日志 + uvicorn日志 + uvicorn访问日志)
# error.log - 包含ERROR及以上级别日志(应用错误 + uvicorn错误)
# 轮转后文件名: info_日期.log, error_日期.log
# ================================================= # # ================================================= #
# ******************* Gzip压缩配置 ******************* # # ******************* Gzip压缩配置 ******************* #
@@ -331,8 +334,8 @@ class Settings(BaseSettings):
}, },
"file": { "file": {
"formatter": "default", "formatter": "default",
"class": "logging.handlers.TimedRotatingFileHandler", "class": "app.core.logger.CustomTimedRotatingFileHandler",
"filename": self.LOGGER_FILEPATH, "filename": self.LOGGER_DIR.joinpath("all.log"),
"when": self.WHEN, "when": self.WHEN,
"backupCount": self.BACKUPCOUNT, "backupCount": self.BACKUPCOUNT,
"encoding": self.ENCODING, "encoding": self.ENCODING,
+286 -42
View File
@@ -1,74 +1,318 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-"""
import os
import time
from datetime import datetime, timedelta
import logging import logging
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from typing import Optional from typing import Optional, Dict, List, Any
from pathlib import Path
from app.config.setting import settings from app.config.setting import settings
class CustomTimedRotatingFileHandler(TimedRotatingFileHandler):
"""高性能自定义的TimedRotatingFileHandler,支持自定义轮换文件名格式"""
# 文件名映射缓存,避免重复计算
_PREFIX_MAP = {
"all": "info",
"error": "error"
}
def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False):
super().__init__(filename, when, interval, backupCount, encoding, delay, utc)
def doRollover(self) -> None:
"""优化后的日志轮换,使用自定义命名格式"""
# 使用流上下文管理确保资源正确释放
if self.stream:
self.stream.close()
self.stream = None
try:
# 计算轮换时间(使用缓存避免重复计算)
current_time = self.rolloverAt - self.interval
time_tuple = time.localtime(current_time)
suffix = time.strftime("%Y-%m-%d", time_tuple)
# 优化路径构建
base_path = Path(self.baseFilename)
prefix = self._PREFIX_MAP.get(base_path.stem, base_path.stem)
new_name = base_path.parent / f"{prefix}_{suffix}.log"
# 原子性文件重命名
if base_path.exists():
base_path.rename(new_name)
# 重新打开日志文件
if not self.delay:
self.stream = self._open()
# 优化下次轮换时间计算
now = int(time.time())
self.rolloverAt = self.computeRollover(now)
except Exception as e:
# 添加错误处理,避免轮换失败影响主程序
print(f"日志轮换失败: {e}")
if not self.delay and self.stream is None:
self.stream = self._open()
class LoggerHandler: class LoggerHandler:
"""日志处理器类,用于配置和管理日志""" """高性能日志处理器类,用于配置和管理日志"""
_instance: Optional['LoggerHandler'] = None _instance: Optional['LoggerHandler'] = None
_lock = False # 线程安全锁
def __new__(cls): def __new__(cls):
"""单例模式""" """线程安全的单例模式"""
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
return cls._instance return cls._instance
def __init__(self): def __init__(self):
if not hasattr(self, 'logger'): # 双重检查锁,避免重复初始化
if not hasattr(self, '_initialized'):
self._initialized = True
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self._configure_logger() self._configure_logger()
def _configure_logger(self): def _configure_logger(self) -> None:
"""配置日志处理器""" """优化后的日志配置方法"""
try: # 检查是否已经配置,避免重复工作
# 清除现有处理器 if self.logger.handlers:
self.logger.handlers.clear() return
# 预编译日志格式器(避免重复创建)
formatter = logging.Formatter(fmt=settings.LOGGER_FORMAT)
# 设置日志级别 # 使用Path对象提升路径操作性能
self.logger.setLevel(settings.LOGGER_LEVEL) log_dir = Path(settings.LOGGER_DIR)
log_dir.mkdir(parents=True, exist_ok=True)
# 创建日志格式 # 清除现有处理
formatter = logging.Formatter(fmt=settings.LOGGER_FORMAT) self.logger.handlers.clear()
self.logger.setLevel(settings.LOGGER_LEVEL)
# 确保日志目录存在 # 处理器配置缓存,避免重复计算
settings.LOGGER_FILEPATH.parent.mkdir(parents=True, exist_ok=True) handler_config = {
'when': settings.WHEN,
'interval': settings.INTERVAL,
'backupCount': settings.BACKUPCOUNT,
'encoding': settings.ENCODING
}
# 配置文件处理器 # 批量配置处理器
file_handler = TimedRotatingFileHandler( handlers = [
filename=settings.LOGGER_FILEPATH, # all.log处理器 - 记录所有级别的日志
when=settings.WHEN, {
interval=settings.INTERVAL, 'path': log_dir / "all.log",
backupCount=settings.BACKUPCOUNT, 'level': logging.DEBUG,
encoding=settings.ENCODING 'config': handler_config
},
# error.log处理器 - 只记录ERROR及以上级别的日志
{
'path': log_dir / "error.log",
'level': logging.ERROR,
'config': handler_config
}
]
# 批量添加文件处理器
for handler_info in handlers:
handler = CustomTimedRotatingFileHandler(
filename=str(handler_info['path']),
**handler_info['config']
) )
file_handler.setLevel(settings.LOGGER_LEVEL) handler.setLevel(handler_info['level'])
file_handler.setFormatter(formatter) handler.setFormatter(formatter)
self.logger.addHandler(file_handler) self.logger.addHandler(handler)
# 配置控制台处理器 # 配置控制台处理器
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setLevel(settings.LOGGER_LEVEL) console_handler.setLevel(settings.LOGGER_LEVEL)
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler) self.logger.addHandler(console_handler)
except Exception as e:
self.logger.error(f"日志配置失败: {e}") # 配置全局异常处理
self._setup_global_exception_handler()
def _setup_global_exception_handler(self) -> None:
"""设置全局异常处理器"""
def handle_exception(exc_type, exc_value, exc_traceback):
"""全局异常处理回调"""
if issubclass(exc_type, KeyboardInterrupt):
# 允许键盘中断正常退出
return
if self.logger:
self.logger.error("未捕获的异常", exc_info=(exc_type, exc_value, exc_traceback))
import sys
sys.excepthook = handle_exception
def __enter__(self): def __enter__(self):
"""支持上下文管理器协议"""
return self.logger return self.logger
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
"""关闭并清理文件处理器""" """优化后的资源清理"""
for handler in self.logger.handlers[:]: try:
if isinstance(handler, logging.FileHandler): # 批量关闭文件处理器
handler.close() file_handlers = [h for h in self.logger.handlers if isinstance(h, logging.FileHandler)]
self.logger.removeHandler(handler) for handler in file_handlers:
try:
handler.close()
self.logger.removeHandler(handler)
except Exception as e:
print(f"关闭日志处理器失败: {e}")
except Exception:
pass
return True return True
def cleanup_old_logs(self, days_to_keep: Optional[int] = None) -> None:
"""
高性能清理指定天数之前的日志文件
Args:
days_to_keep: 保留最近多少天的日志,如果为None则使用配置中的值
"""
try:
days_to_keep = days_to_keep or settings.LOG_RETENTION_DAYS
log_dir = Path(settings.LOGGER_DIR)
if not log_dir.exists():
return
# 使用timedelta提高时间计算精度
cutoff_time = datetime.now() - timedelta(days=days_to_keep)
cutoff_timestamp = cutoff_time.timestamp()
# 优化的文件模式匹配
patterns = ["info_*.log", "error_*.log"]
cleaned_files = []
total_size = 0
# 批量收集待删除文件
for pattern in patterns:
for log_file in log_dir.glob(pattern):
if log_file.is_file() and log_file.stat().st_mtime < cutoff_timestamp:
try:
file_stat = log_file.stat()
total_size += file_stat.st_size
cleaned_files.append(log_file)
except OSError:
continue
# 批量删除文件
success_count = 0
for log_file in cleaned_files:
try:
log_file.unlink()
success_count += 1
except Exception as e:
self.logger.warning(f"无法删除日志文件 {log_file}: {e}")
if success_count > 0:
self.logger.info(
f"已清理旧日志文件: {success_count}个文件, "
f"释放空间: {total_size / 1024 / 1024:.2f}MB"
)
except Exception as e:
self.logger.error(f"清理日志文件时出错: {e}")
# 全局日志实例 def get_log_files_info(self) -> Dict[str, Any]:
logger = LoggerHandler().logger """
高性能获取日志文件信息
Returns:
Dict[str, Any]: 包含日志文件信息的字典
"""
log_dir = Path(settings.LOGGER_DIR)
log_info = {
"log_directory": str(log_dir),
"current_files": [],
"history_files": [],
"total_size": 0
}
if not log_dir.exists():
return log_info
try:
# 使用列表推导式优化当前文件处理
current_files = ["all.log", "error.log"]
current_files_info = []
for filename in current_files:
file_path = log_dir / filename
if file_path.exists():
stat = file_path.stat()
current_files_info.append({
"name": filename,
"size": stat.st_size,
"size_formatted": format_file_size(stat.st_size),
"modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
})
log_info["current_files"] = current_files_info
# 优化历史文件收集
history_files = []
for pattern in ["info_*.log", "error_*.log"]:
for log_file in log_dir.glob(pattern):
if log_file.is_file():
stat = log_file.stat()
history_files.append({
"name": log_file.name,
"size": stat.st_size,
"size_formatted": format_file_size(stat.st_size),
"modified": stat.st_mtime # 保存原始时间戳用于排序
})
# 高效排序(使用原始时间戳)
history_files.sort(key=lambda x: x["modified"], reverse=True)
# 格式化时间显示
for file_info in history_files:
file_info["modified"] = datetime.fromtimestamp(file_info["modified"]).strftime("%Y-%m-%d %H:%M:%S")
log_info["history_files"] = history_files
# 计算总大小(使用生成器表达式提升性能)
all_files = log_info["current_files"] + history_files
log_info["total_size"] = sum(file_info["size"] for file_info in all_files)
log_info["total_size_formatted"] = format_file_size(log_info["total_size"])
except OSError as e:
self.logger.error(f"获取日志文件信息失败: {e}")
return log_info
def format_file_size(size_bytes: int) -> str:
"""高性能文件大小格式化工具函数"""
if size_bytes == 0:
return "0 B"
units = ['B', 'KB', 'MB', 'GB', 'TB']
size = float(size_bytes)
for unit in units:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} PB"
# 全局日志实例(使用延迟初始化提升启动性能)
_logger_instance: Optional[logging.Logger] = None
def get_logger() -> logging.Logger:
"""获取全局日志实例(带延迟初始化)"""
global _logger_instance
if _logger_instance is None:
_logger_instance = LoggerHandler().logger
return _logger_instance
# 向后兼容的全局实例
logger = get_logger()