本次提交新增了完整的多渠道消息网关系统,包括: 1. 支持飞书、钉钉、Web、Hook 四种渠道的适配器与配置 2. 领域模型层:消息、会话、绑定、出箱等核心实体 3. 应用服务层:管道、中间件、DTO 与业务逻辑 4. 基础设施层:持久化、过滤器、队列等端口实现 5. 接口层:REST API、SSE、WebSocket 通信端点 6. 前端页面与路由配置,添加渠道管理菜单 7. 新增相关依赖包与 docker-compose 部署配置
129 lines
4.5 KiB
Python
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)}",
|
|
)
|