mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 12:52:26 +00:00
refactor(logger): 重构日志系统以提升性能和可维护性
- 使用自定义的TimedRotatingFileHandler实现更高效的日志轮换 - 新增日志文件自动清理功能,支持按天数保留日志 - 优化日志处理器初始化流程,增加线程安全保护 - 分离不同级别日志到单独文件(all.log和error.log) - 添加全局异常捕获和日志文件信息统计功能
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+285
-41
@@ -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
|
||||||
|
|
||||||
# 设置日志级别
|
# 预编译日志格式器(避免重复创建)
|
||||||
self.logger.setLevel(settings.LOGGER_LEVEL)
|
formatter = logging.Formatter(fmt=settings.LOGGER_FORMAT)
|
||||||
|
|
||||||
# 创建日志格式器
|
# 使用Path对象提升路径操作性能
|
||||||
formatter = logging.Formatter(fmt=settings.LOGGER_FORMAT)
|
log_dir = Path(settings.LOGGER_DIR)
|
||||||
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# 确保日志目录存在
|
# 清除现有处理器
|
||||||
settings.LOGGER_FILEPATH.parent.mkdir(parents=True, exist_ok=True)
|
self.logger.handlers.clear()
|
||||||
|
self.logger.setLevel(settings.LOGGER_LEVEL)
|
||||||
|
|
||||||
# 配置文件处理器
|
# 处理器配置缓存,避免重复计算
|
||||||
file_handler = TimedRotatingFileHandler(
|
handler_config = {
|
||||||
filename=settings.LOGGER_FILEPATH,
|
'when': settings.WHEN,
|
||||||
when=settings.WHEN,
|
'interval': settings.INTERVAL,
|
||||||
interval=settings.INTERVAL,
|
'backupCount': settings.BACKUPCOUNT,
|
||||||
backupCount=settings.BACKUPCOUNT,
|
'encoding': settings.ENCODING
|
||||||
encoding=settings.ENCODING
|
}
|
||||||
|
|
||||||
|
# 批量配置处理器
|
||||||
|
handlers = [
|
||||||
|
# all.log处理器 - 记录所有级别的日志
|
||||||
|
{
|
||||||
|
'path': log_dir / "all.log",
|
||||||
|
'level': logging.DEBUG,
|
||||||
|
'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:
|
||||||
logger = LoggerHandler().logger
|
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]:
|
||||||
|
"""
|
||||||
|
高性能获取日志文件信息
|
||||||
|
|
||||||
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user