Program/mooc/core/logger.py
?..濡.. 8451ad034c 1.统一CRUD操作
2.完成登录部分接口
3.暂时挂载本地图片链接作为头像存储
2025-03-04 20:36:52 +08:00

201 lines
7.4 KiB
Python
Raw 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.

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}")