ForcePilot/backend/package/yuxi/channel/interfaces/rest/router/health.py
Kris 9a8a27bf36 feat(channel): 新增渠道网关模块完整实现
本次提交新增了完整的多渠道消息网关系统,包括:
1. 支持飞书、钉钉、Web、Hook 四种渠道的适配器与配置
2. 领域模型层:消息、会话、绑定、出箱等核心实体
3. 应用服务层:管道、中间件、DTO 与业务逻辑
4. 基础设施层:持久化、过滤器、队列等端口实现
5. 接口层:REST API、SSE、WebSocket 通信端点
6. 前端页面与路由配置,添加渠道管理菜单
7. 新增相关依赖包与 docker-compose 部署配置
2026-05-30 21:53:09 +08:00

129 lines
4.5 KiB
Python

from __future__ import annotations
import asyncio
import logging
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from sqlalchemy import select
from yuxi.channel.container import ChannelContainer, get_channel
logger = logging.getLogger(__name__)
router = APIRouter()
class HealthzResponse(BaseModel):
status: str = "ok"
startup_timeline: list[dict] | None = None
class ReadyzComponentStatus(BaseModel):
status: str
detail: str | None = None
class ReadyzResponse(BaseModel):
status: str
redis: ReadyzComponentStatus | None = None
postgres: ReadyzComponentStatus | None = None
workers: ReadyzComponentStatus | None = None
channels: ReadyzComponentStatus | None = None
ws_connections: ReadyzComponentStatus | None = None
@router.get("/healthz", response_model=HealthzResponse)
async def liveness_check(channel: ChannelContainer = Depends(get_channel)):
return HealthzResponse(
status="ok",
startup_timeline=channel.startup_tracer.to_dict() if channel else None,
)
@router.get("/readyz")
async def readiness_check(channel: ChannelContainer = Depends(get_channel)):
checks = {
"redis": await _check_redis(channel),
"postgres": await _check_postgres(),
"workers": await _check_workers(channel),
"channels": await _check_channels(channel),
"ws_connections": await _check_ws_connections(channel),
}
overall = "ready" if all(v.status == "ok" for v in checks.values()) else "not_ready"
status_code = 200 if overall == "ready" else 503
return JSONResponse(
status_code=status_code,
content=ReadyzResponse(status=overall, **checks).model_dump(),
)
async def _check_redis(container: ChannelContainer) -> ReadyzComponentStatus:
try:
if container and container.redis:
await container.redis.ping()
return ReadyzComponentStatus(status="ok")
except Exception as exc:
logger.warning("redis check failed: %s", exc)
return ReadyzComponentStatus(status="error", detail="connection refused")
async def _check_postgres() -> ReadyzComponentStatus:
try:
from yuxi.storage.postgres.manager import pg_manager
if not pg_manager._initialized:
return ReadyzComponentStatus(status="error", detail="not initialized")
async with pg_manager.get_async_session_context() as session:
await session.execute(select(1))
return ReadyzComponentStatus(status="ok")
except Exception as exc:
logger.warning("postgres check failed: %s", exc)
return ReadyzComponentStatus(status="error", detail="connection refused")
async def _check_workers(container: ChannelContainer) -> ReadyzComponentStatus:
try:
if container and container.worker_pool and container.worker_pool.is_running:
return ReadyzComponentStatus(status="ok")
except Exception as exc:
logger.warning("workers check failed: %s", exc)
return ReadyzComponentStatus(status="error", detail="no active consumers")
async def _check_channels(container: ChannelContainer) -> ReadyzComponentStatus:
try:
if container and container.adapters:
results = await asyncio.gather(
*[a.is_healthy() for a in container.adapters.values()],
return_exceptions=True,
)
unhealthy = [name for name, r in zip(container.adapters, results) if r is not True]
if not unhealthy:
return ReadyzComponentStatus(status="ok")
return ReadyzComponentStatus(
status="degraded",
detail=f"unhealthy channels: {', '.join(unhealthy)}",
)
except Exception as exc:
logger.warning("channels check failed: %s", exc)
return ReadyzComponentStatus(status="error", detail="no adapters")
async def _check_ws_connections(container: ChannelContainer) -> ReadyzComponentStatus:
if not container or not container.ws_manager:
return ReadyzComponentStatus(status="ok", detail="no ws connections")
status_map = container.ws_manager.connections_status
if not status_map:
return ReadyzComponentStatus(status="ok", detail="no ws connections registered")
disconnected = [ct for ct, ok in status_map.items() if not ok]
if not disconnected:
return ReadyzComponentStatus(status="ok")
return ReadyzComponentStatus(
status="degraded",
detail=f"disconnected: {', '.join(disconnected)}",
)