Files
FastapiAdmin/backend/app/core/logger.py
T
zhangtao 81d131c0a4 refactor(common): 替换分页服务方法及代码生成模块重构
- 将分页服务中方法名由get_page_obj统一替换为paginate
- 注意相关controller均调整调用方式,保证统一接口调用
- 代码生成模块数据库模型统一替换为GenTableModel和GenTableColumnModel
- 更改数据库类型及分页相关配置为settings.DATABASE_TYPE统一管理
- 重构代码生成模块查询参数,新增GenTableQueryParam和GenTableColumnQueryParam类支持更灵活查询
- 数据模型中Pydantic Schema类型统一调整为Schema后缀
- 优化异常处理,增加请求参数验证错误的友好提示映射
- 调整中间件及依赖以支持更严格的类型检查及更健壮的用户权限认证逻辑
- 微调日志打印格式,改进请求日志信息输出风格
2025-09-19 01:41:10 +08:00

321 lines
12 KiB
Python

# -*- coding: utf-8 -*-"""
import os
import time
from datetime import datetime, timedelta
import logging
from logging.handlers import TimedRotatingFileHandler
from typing import Optional, Dict, Any
from pathlib import Path
import typing
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 # type: ignore
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:
"""高性能日志处理器类,用于配置和管理日志"""
_instance: Optional['LoggerHandler'] = None
_lock = False # 线程安全锁
def __new__(cls):
"""线程安全的单例模式"""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# 双重检查锁,避免重复初始化
if not hasattr(self, '_initialized'):
self._initialized = True
self.logger = logging.getLogger(__name__)
self._configure_logger()
def _configure_logger(self) -> None:
"""优化后的日志配置方法"""
# 检查是否已经配置,避免重复工作
if self.logger.handlers:
return
# 预编译日志格式器(避免重复创建)
formatter = logging.Formatter(fmt=settings.LOGGER_FORMAT)
# 使用Path对象提升路径操作性能
log_dir = Path(settings.LOGGER_DIR)
if not log_dir.exists():
log_dir.mkdir(parents=True, exist_ok=True)
# 清除现有处理器
self.logger.handlers.clear()
self.logger.setLevel(settings.LOGGER_LEVEL)
# 处理器配置缓存,避免重复计算
handler_config = {
'when': settings.WHEN,
'interval': settings.INTERVAL,
'backupCount': settings.BACKUPCOUNT,
'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']
)
handler.setLevel(handler_info['level'])
handler.setFormatter(formatter)
self.logger.addHandler(handler)
# 配置控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(settings.LOGGER_LEVEL)
console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler)
# 配置全局异常处理
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):
"""支持上下文管理器协议"""
return self.logger
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
"""优化后的资源清理"""
try:
# 批量关闭文件处理器
file_handlers = [h for h in self.logger.handlers if isinstance(h, logging.FileHandler)]
for handler in file_handlers:
try:
handler.close()
self.logger.removeHandler(handler)
except Exception as e:
print(f"关闭日志处理器失败: {e}")
except Exception:
pass
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]:
"""
高性能获取日志文件信息
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()