import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.core.config import settings from app.core.health import router as health_router from app.core.logging import request_id_var, setup_logging from app.core.middleware import RequestContextMiddleware from app.api.v1.router import api_router # 日志必须在任何模块开始产日志之前配置好,故放模块顶层而非 lifespan 内 setup_logging(level=settings.LOG_LEVEL, json_output=settings.LOG_JSON) logger = logging.getLogger("track.main") def _init_error_tracking() -> None: """可选错误追踪:未配置 DSN,或未安装 sentry-sdk 时静默跳过""" if not settings.SENTRY_DSN: return try: import sentry_sdk except ImportError: logger.warning( "已配置 SENTRY_DSN 但未安装 sentry-sdk,错误追踪未启用;" "需要时执行 pip install sentry-sdk" ) return sentry_sdk.init( dsn=settings.SENTRY_DSN, traces_sample_rate=settings.SENTRY_TRACES_SAMPLE_RATE, environment="production" if not settings.DEBUG else "development", release=settings.APP_VERSION, ) logger.info("错误追踪已启用 (Sentry)") @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期:启动时初始化连接,关闭时释放资源""" _init_error_tracking() logger.info( "服务启动", extra={ "extra_fields": { "version": settings.APP_VERSION, "debug": settings.DEBUG, "cors_origins": settings.CORS_ORIGINS_LIST, } }, ) yield logger.info("服务关闭") app = FastAPI( title="Track Production API", description="工厂生产流转管理系统 API", version=settings.APP_VERSION, lifespan=lifespan, ) # ---- CORS 跨域配置(从环境变量读取白名单) ---- app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS_LIST, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], # 暴露给浏览器 JS 读取:前端报错时才能把 request_id 一起带上便于对账 expose_headers=["X-Request-ID"], ) # Starlette 的 add_middleware 是「后添加者在外层」,故 RequestContextMiddleware # 最后注册 → 最先进入请求,才能覆盖包括 CORS 预检在内的全部请求并回写 X-Request-ID app.add_middleware(RequestContextMiddleware) @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: """兜底异常处理。 完整堆栈只进日志;响应体仅返回 request_id —— 既不把内部实现泄露给客户端, 又让用户报障时能凭这个 ID 在日志里精确定位到本次失败。 """ # 优先取 request.state(见 RequestContextMiddleware 的说明): # 本处理器由 ServerErrorMiddleware 调用,此时 contextvar 已被重置 request_id = getattr(request.state, "request_id", None) or request_id_var.get() logger.exception( "未处理异常: %s %s", request.method, request.url.path, extra={"extra_fields": {"method": request.method, "path": request.url.path}}, ) return JSONResponse( status_code=500, content={"detail": "服务器内部错误", "request_id": request_id}, ) # ---- 注册路由 ---- app.include_router(api_router, prefix="/api/v1") # 健康检查挂在根路径(/health*),运维探针不经过 /api/v1 app.include_router(health_router)