201 lines
7.4 KiB
Python
201 lines
7.4 KiB
Python
![]() |
import os
|
|||
|
import logging
|
|||
|
import sys
|
|||
|
from datetime import datetime
|
|||
|
from pathlib import Path
|
|||
|
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
|||
|
import json
|
|||
|
from typing import Dict, Any, Optional
|
|||
|
import colorlog # 导入colorlog库
|
|||
|
|
|||
|
from fastapi import FastAPI, Request, Response
|
|||
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|||
|
from starlette.responses import JSONResponse
|
|||
|
|
|||
|
from mooc.core.config import settings
|
|||
|
|
|||
|
# 创建日志目录
|
|||
|
log_dir = Path(settings.LOG_DIR)
|
|||
|
if not log_dir.exists():
|
|||
|
log_dir.mkdir(parents=True)
|
|||
|
|
|||
|
# 日志格式
|
|||
|
LOGGING_FORMAT = '%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s'
|
|||
|
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
|
|||
|
|
|||
|
# ANSI颜色代码
|
|||
|
COLORS = {
|
|||
|
'DEBUG': '\033[36m', # 青色
|
|||
|
'INFO': '\033[32m', # 绿色
|
|||
|
'WARNING': '\033[33m', # 黄色
|
|||
|
'ERROR': '\033[31m', # 红色
|
|||
|
'CRITICAL': '\033[41m\033[37m', # 白字红底
|
|||
|
'RESET': '\033[0m' # 重置
|
|||
|
}
|
|||
|
|
|||
|
class ColoredFormatter(logging.Formatter):
|
|||
|
def format(self, record):
|
|||
|
levelname = record.levelname
|
|||
|
if levelname in COLORS:
|
|||
|
record.levelname = f"{COLORS[levelname]}{levelname}{COLORS['RESET']}"
|
|||
|
return super().format(record)
|
|||
|
class SQLFilter(logging.Filter):
|
|||
|
"""过滤掉 SQLAlchemy 查询日志"""
|
|||
|
def filter(self, record):
|
|||
|
# 如果是 sqlalchemy.engine 的日志且包含 SELECT 等查询语句,则过滤掉
|
|||
|
if record.name.startswith('sqlalchemy.engine'):
|
|||
|
msg = record.getMessage()
|
|||
|
return not any(keyword in msg for keyword in ['SELECT', 'INSERT', 'UPDATE', 'DELETE'])
|
|||
|
return True
|
|||
|
# 获取根日志记录器
|
|||
|
root_logger = logging.getLogger()
|
|||
|
root_logger.setLevel(settings.LOG_LEVEL)
|
|||
|
|
|||
|
# 清除现有处理器
|
|||
|
if root_logger.handlers:
|
|||
|
root_logger.handlers.clear()
|
|||
|
|
|||
|
# 添加彩色控制台处理器
|
|||
|
console_handler = logging.StreamHandler(sys.stdout)
|
|||
|
color_formatter = ColoredFormatter(LOGGING_FORMAT, DATE_FORMAT)
|
|||
|
console_handler.setFormatter(color_formatter)
|
|||
|
console_handler.setLevel(settings.LOG_LEVEL)
|
|||
|
root_logger.addHandler(console_handler)
|
|||
|
|
|||
|
# 添加文件处理器 - 按大小轮转
|
|||
|
file_handler = RotatingFileHandler(
|
|||
|
filename=os.path.join(settings.LOG_DIR, f"{settings.LOG_NAME}.log"),
|
|||
|
maxBytes=settings.LOG_MAX_SIZE,
|
|||
|
backupCount=settings.LOG_BACKUP_COUNT,
|
|||
|
encoding='utf-8'
|
|||
|
)
|
|||
|
file_handler.setFormatter(logging.Formatter(LOGGING_FORMAT, DATE_FORMAT))
|
|||
|
file_handler.setLevel(settings.LOG_LEVEL)
|
|||
|
root_logger.addHandler(file_handler)
|
|||
|
|
|||
|
# 添加文件处理器 - 错误日志单独存储
|
|||
|
error_file_handler = RotatingFileHandler(
|
|||
|
filename=os.path.join(settings.LOG_DIR, f"{settings.LOG_NAME}_error.log"),
|
|||
|
maxBytes=settings.LOG_MAX_SIZE,
|
|||
|
backupCount=settings.LOG_BACKUP_COUNT,
|
|||
|
encoding='utf-8'
|
|||
|
)
|
|||
|
error_file_handler.setFormatter(logging.Formatter(LOGGING_FORMAT, DATE_FORMAT))
|
|||
|
error_file_handler.setLevel(logging.ERROR)
|
|||
|
root_logger.addHandler(error_file_handler)
|
|||
|
|
|||
|
# 如果是生产环境,添加每日轮转的文件处理器
|
|||
|
if settings.ENVIRONMENT == "production":
|
|||
|
daily_handler = TimedRotatingFileHandler(
|
|||
|
filename=os.path.join(settings.LOG_DIR, f"{settings.LOG_NAME}_daily.log"),
|
|||
|
when='midnight',
|
|||
|
interval=1,
|
|||
|
backupCount=30,
|
|||
|
encoding='utf-8'
|
|||
|
)
|
|||
|
daily_handler.setFormatter(logging.Formatter(LOGGING_FORMAT, DATE_FORMAT))
|
|||
|
daily_handler.setLevel(settings.LOG_LEVEL)
|
|||
|
root_logger.addHandler(daily_handler)
|
|||
|
|
|||
|
# 为第三方库设置更高的日志级别,减少日志噪音
|
|||
|
for logger_name in ['uvicorn', 'uvicorn.error', 'uvicorn.access', 'sqlalchemy','httpcore','httpx','urllib3','python_multipart',"sqlalchemy.engine"]:
|
|||
|
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
|||
|
|
|||
|
# 创建获取日志记录器的函数
|
|||
|
def get_logger(name: str) -> logging.Logger:
|
|||
|
"""获取指定名称的日志记录器"""
|
|||
|
return logging.getLogger(name)
|
|||
|
|
|||
|
# 请求/响应日志记录中间件
|
|||
|
class LoggingMiddleware(BaseHTTPMiddleware):
|
|||
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|||
|
# 生成请求ID
|
|||
|
request_id = f"{datetime.now().strftime('%Y%m%dT%H%M%S')}-{os.urandom(3).hex()}"
|
|||
|
|
|||
|
# 记录请求信息
|
|||
|
logger = get_logger("api.request")
|
|||
|
|
|||
|
# 提取并安全处理请求体
|
|||
|
try:
|
|||
|
body = await request.body()
|
|||
|
body_str = body.decode('utf-8') if body else ""
|
|||
|
# 对敏感信息进行脱敏
|
|||
|
if 'password' in body_str or 'token' in body_str:
|
|||
|
try:
|
|||
|
body_json = json.loads(body_str)
|
|||
|
if 'password' in body_json:
|
|||
|
body_json['password'] = '*****'
|
|||
|
if 'token' in body_json:
|
|||
|
body_json['token'] = '*****'
|
|||
|
body_str = json.dumps(body_json)
|
|||
|
except:
|
|||
|
# 如果不是JSON格式,简单替换敏感信息
|
|||
|
body_str = body_str.replace('"password":"', '"password":"*****')
|
|||
|
body_str = body_str.replace('"token":"', '"token":"*****')
|
|||
|
except Exception as e:
|
|||
|
body_str = f"[Error reading body: {str(e)}]"
|
|||
|
|
|||
|
# 记录请求日志
|
|||
|
request_log = {
|
|||
|
"request_id": request_id,
|
|||
|
"method": request.method,
|
|||
|
"url": str(request.url),
|
|||
|
"headers": dict(request.headers),
|
|||
|
"client_ip": request.client.host if request.client else None,
|
|||
|
"body": body_str[:1000] if body_str else None, # 限制大小
|
|||
|
}
|
|||
|
|
|||
|
logger.info(f"Request: {json.dumps(request_log)}")
|
|||
|
|
|||
|
# 将请求ID传递给请求状态
|
|||
|
request.state.request_id = request_id
|
|||
|
|
|||
|
try:
|
|||
|
# 处理请求
|
|||
|
response = await call_next(request)
|
|||
|
|
|||
|
# 记录响应信息
|
|||
|
response_log = {
|
|||
|
"request_id": request_id,
|
|||
|
"status_code": response.status_code,
|
|||
|
"headers": dict(response.headers),
|
|||
|
"processing_time": None # 目前未实现处理时间计算
|
|||
|
}
|
|||
|
|
|||
|
logger.info(f"Response: {json.dumps(response_log)}")
|
|||
|
|
|||
|
return response
|
|||
|
except Exception as e:
|
|||
|
# 记录未捕获的异常
|
|||
|
error_logger = get_logger("api.error")
|
|||
|
error_logger.exception(f"Request {request_id} failed with unhandled exception: {str(e)}")
|
|||
|
|
|||
|
# 返回错误响应
|
|||
|
return JSONResponse(
|
|||
|
status_code=500,
|
|||
|
content={
|
|||
|
"code": 500,
|
|||
|
"msg": "Internal server error",
|
|||
|
"request_id": request_id,
|
|||
|
"data": None
|
|||
|
}
|
|||
|
)
|
|||
|
|
|||
|
# 初始化日志系统
|
|||
|
def setup_logging(app: FastAPI) -> None:
|
|||
|
"""初始化应用的日志系统"""
|
|||
|
# 添加日志中间件
|
|||
|
app.add_middleware(LoggingMiddleware)
|
|||
|
|
|||
|
# 设置 SQLAlchemy 日志级别
|
|||
|
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
|||
|
|
|||
|
# 添加 SQL 过滤器
|
|||
|
sql_filter = SQLFilter()
|
|||
|
for handler in logging.getLogger().handlers:
|
|||
|
handler.addFilter(sql_filter)
|
|||
|
|
|||
|
# 记录应用启动信息
|
|||
|
logger = get_logger("app")
|
|||
|
logger.info(f"Application {app.title} v{app.version} starting in {settings.ENVIRONMENT} environment")
|
|||
|
logger.info(f"Log level: {settings.LOG_LEVEL}")
|