import logging from contextlib import asynccontextmanager from datetime import datetime, timezone from fastapi import FastAPI, HTTPException, Request, Depends from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse, Response from prometheus_client import generate_latest, CONTENT_TYPE_LATEST from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text # 必须在其他模块 import 之前初始化 JSON 日志 from app.logging_config import setup_logging setup_logging() from fastapi.middleware.cors import CORSMiddleware from app.api.admin import router as admin_router from app.api.content import router as content_router from app.api.contents import router as contents_router from app.api.clients import router as clients_router from app.api.organization import router as organization_router from app.api.agents import router as agents_router from app.api.knowledge import router as knowledge_router from app.api.distribution import router as distribution_router from app.api.analytics import router as analytics_router from app.api.lifecycle import router as lifecycle_router from app.api.auth import router as auth_router from app.api.citations import router as citations_router from app.api.queries import router as queries_router from app.api.reports import router as reports_router from app.api.subscriptions import router as subscription_router from app.api.alerts import router as alerts_router from app.api.dashboard import router as dashboard_router from app.api.brands import router as brands_router from app.api.diagnosis import router as diagnosis_router from app.api.onboarding import router as onboarding_router from app.api.platforms import router as platforms_router from app.api.platform_rules import router as platform_rules_router from app.api.image import router as image_router from app.api.knowledge_graph import router as knowledge_graph_router from app.api.ai_engines import router as ai_engines_router from app.api.detection import router as detection_router from app.api.api_keys import router as api_keys_router from app.api.usage import router as usage_router from app.config import settings from app.database import engine, Base from app.schemas.common import ErrorResponse, ErrorCode from app.middleware.rate_limit import RateLimitMiddleware from app.middleware.logging_middleware import RequestLoggingMiddleware from app.middleware.request_id import RequestIdMiddleware from app.middleware.metrics import MetricsMiddleware from app.monitoring.middleware import MonitoringMiddleware from app.database import get_db from app.workers.scheduler import query_scheduler @asynccontextmanager async def lifespan(app: FastAPI): import app.models import app.monitoring async with engine.begin() as conn: await conn.execute(text("SELECT 1")) query_scheduler.start() yield await query_scheduler.shutdown() app = FastAPI( title="GEO Platform API", version="1.0.0", lifespan=lifespan, ) @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: """统一 HTTP 异常响应格式。""" code = ErrorCode.from_status(exc.status_code) return JSONResponse( status_code=exc.status_code, content=ErrorResponse( detail=str(exc.detail), code=code, ).model_dump(mode="json"), ) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse: """统一参数校验异常响应格式。""" return JSONResponse( status_code=422, content=ErrorResponse( detail="请求参数校验失败", code=ErrorCode.VALIDATION_ERROR, extra={"errors": exc.errors()}, ).model_dump(mode="json"), ) @app.exception_handler(Exception) async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: """兜底异常处理器,避免内部错误泄漏给客户端。""" logging.getLogger(__name__).exception("Unhandled exception: %s", exc) return JSONResponse( status_code=500, content=ErrorResponse( detail="服务器内部错误,请稍后重试", code=ErrorCode.INTERNAL_ERROR, ).model_dump(mode="json"), ) _allow_origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",") if origin.strip()] if not _allow_origins: _allow_origins = ["http://localhost:3000"] app.add_middleware( CORSMiddleware, allow_origins=_allow_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 安全响应头 @app.middleware("http") async def add_security_headers(request, call_next): response = await call_next(request) response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" return response # 中间件注册顺序(FastAPI 后进先出,最后注册的最先执行) # 执行链:RequestId → Metrics → RateLimit → RequestLogging → CORS → SecurityHeaders app.add_middleware(RequestLoggingMiddleware) app.add_middleware(RateLimitMiddleware) app.add_middleware(MetricsMiddleware) app.add_middleware(MonitoringMiddleware) app.add_middleware(RequestIdMiddleware) app.include_router(auth_router, prefix="/api/v1/auth", tags=["认证"]) app.include_router(queries_router, prefix="/api/v1/queries", tags=["查询词"]) app.include_router(citations_router, prefix="/api/v1/citations", tags=["引用数据"]) app.include_router(reports_router, prefix="/api/v1/reports", tags=["报告"]) app.include_router(subscription_router) app.include_router(admin_router) app.include_router(agents_router, prefix="/api/v1/agents", tags=["Agent管理"]) app.include_router(lifecycle_router, prefix="/api/v1/lifecycle", tags=["lifecycle"]) app.include_router(knowledge_router, prefix="/api/v1/knowledge", tags=["知识库"]) app.include_router(content_router, prefix="/api/v1/content", tags=["内容生产"]) app.include_router(contents_router, prefix="/api/v1/contents", tags=["内容管理"]) app.include_router(organization_router) app.include_router(clients_router, prefix="/api/v1/clients", tags=["客户管理"]) app.include_router(distribution_router, prefix="/api/v1/distribution", tags=["内容分发"]) app.include_router(analytics_router, prefix="/api/v1/analytics", tags=["监测优化"]) app.include_router(alerts_router, prefix="/api/v1/alerts", tags=["告警通知"]) app.include_router(dashboard_router, prefix="/api/v1/dashboard", tags=["仪表盘"]) app.include_router(brands_router, prefix="/api/v1/brands", tags=["品牌管理"]) app.include_router(diagnosis_router, prefix="/api/v1/diagnosis", tags=["诊断服务"]) app.include_router(onboarding_router, prefix="/api/v1") app.include_router(platforms_router, prefix="/api/v1") app.include_router(platform_rules_router) app.include_router(image_router, prefix="/api/v1") app.include_router(knowledge_graph_router, prefix="/api/v1") app.include_router(ai_engines_router, prefix="/api/v1/ai-engines", tags=["AI引擎查询"]) app.include_router(detection_router, prefix="/api/v1/detection", tags=["定时检测任务"]) app.include_router(api_keys_router, prefix="/api/v1/api-keys", tags=["API Key管理"]) app.include_router(usage_router, prefix="/api/v1/usage", tags=["用量追踪"]) @app.get("/health", tags=["可观测性"]) async def health_check(): """存活检查(Liveness):服务进程是否运行正常。不依赖外部服务。""" return { "status": "healthy", "timestamp": datetime.now(timezone.utc).isoformat(), } @app.get("/ready", tags=["可观测性"]) async def readiness_check(db: AsyncSession = Depends(get_db)): """就绪检查(Readiness):依赖服务(DB / Redis)是否就绪。 供 Kubernetes readinessProbe / Docker healthcheck 使用。 不需要认证。 """ import redis.asyncio as aioredis # type: ignore from app.config import settings as _settings # --- 检查数据库 --- try: await db.execute(text("SELECT 1")) db_ok = True except Exception: db_ok = False # --- 检查 Redis --- redis_ok = False try: redis_client = aioredis.from_url(_settings.REDIS_URL, socket_connect_timeout=2) await redis_client.ping() await redis_client.aclose() redis_ok = True except Exception: pass all_ok = db_ok and redis_ok return JSONResponse( status_code=200 if all_ok else 503, content={ "status": "ready" if all_ok else "not_ready", "checks": { "database": "ok" if db_ok else "error", "redis": "ok" if redis_ok else "error", }, "timestamp": datetime.now(timezone.utc).isoformat(), }, ) @app.get("/metrics", tags=["可观测性"]) async def metrics(): """Prometheus指标端点""" return Response( content=generate_latest(), media_type=CONTENT_TYPE_LATEST ) # ---- 详细健康检查端点 ---- from app.services.health_checker import HealthChecker from app.services.app_state import app_state @app.get("/health/detailed", tags=["可观测性"]) async def detailed_health( db: AsyncSession = Depends(get_db), ): """ 详细健康检查 返回所有依赖组件的健康状态: - database: 数据库连接 - redis: Redis缓存 - llm_providers: LLM服务提供商 - storage: 文件存储 状态: - healthy: 所有组件正常 - degraded: 部分组件异常,但仍可服务 - unhealthy: 核心组件异常 """ checker = HealthChecker(db, settings.REDIS_URL) health_result = await checker.check_all() # 添加应用信息 health_result["app"] = app_state.get_info() return health_result @app.get("/health/liveness", tags=["可观测性"]) async def liveness(): """ 存活探针 用于Kubernetes livenessProbe 只要应用进程存活就返回200 """ return {"status": "alive"} @app.get("/health/readiness", tags=["可观测性"]) async def readiness(db: AsyncSession = Depends(get_db)): """ 就绪探针 用于Kubernetes readinessProbe 检查核心依赖是否就绪 """ checker = HealthChecker(db, settings.REDIS_URL) # 只检查核心依赖:数据库和Redis db_result = await checker.check_database() redis_result = await checker.check_redis() if db_result.healthy and redis_result.healthy: return { "status": "ready", "checks": { "database": db_result.healthy, "redis": redis_result.healthy, } } else: raise HTTPException( status_code=503, detail={ "status": "not_ready", "checks": { "database": db_result.healthy, "redis": redis_result.healthy, } } )