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