313 lines
11 KiB
Python
313 lines
11 KiB
Python
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.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.run_sync(Base.metadata.create_all)
|
||
|
||
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(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/knowledge-bases")
|
||
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,
|
||
}
|
||
}
|
||
)
|