feat: Gateway health monitoring (Hermes CLI, API, frontend)
- Hermes CLI wrapper using subprocess.run for gateway status - Gateway API: status list (with filter), events (placeholder), start/stop/restart with audit logging - Frontend GatewayPage: timeline, filter bar, status table with 10s polling, operation modals - usePolling hook for periodic data refresh Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""Gateway API — 状态、事件、启停操作。"""
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import require_auth, require_role
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.user import User
|
||||
from app.services import hermes_cli
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/gateway", tags=["gateway"])
|
||||
|
||||
# ── 占位事件数据(TODO: 从 hermes 日志或系统日志解析) ──
|
||||
|
||||
PLACEHOLDER_EVENTS = [
|
||||
{"time": "2026-07-22T18:45", "profile": "monitor", "type": "heartbeat_timeout",
|
||||
"detail": "连续 3 次心跳无响应"},
|
||||
{"time": "2026-07-22T14:20", "profile": "feishu-gw", "type": "feishu_disconnect",
|
||||
"detail": "WebSocket 断开 8 分钟"},
|
||||
{"time": "2026-07-22T10:30", "profile": "gateway-monitor", "type": "restart",
|
||||
"detail": "Gateway 重启"},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_gateway_status(
|
||||
status: str | None = Query(None, pattern="^(online|offline|warn)$"),
|
||||
_user: User = Depends(require_auth),
|
||||
):
|
||||
"""返回全部 Profile 的 Gateway 状态列表,支持 ?status= 筛选。"""
|
||||
gateways = hermes_cli.gateway_status_all()
|
||||
for gw in gateways:
|
||||
gw.setdefault("downtime_minutes", 0)
|
||||
gw.setdefault("restarts_today", 0)
|
||||
gw.setdefault("last_event", "")
|
||||
gw.setdefault("profile_type", "worker")
|
||||
if status:
|
||||
gateways = [g for g in gateways if g.get("status") == status]
|
||||
return gateways
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def get_gateway_events(
|
||||
_user: User = Depends(require_auth),
|
||||
):
|
||||
"""返回最近 24h 异常事件。"""
|
||||
# TODO: 从 hermes CLI 日志或系统日志解析真实事件
|
||||
return PLACEHOLDER_EVENTS
|
||||
|
||||
|
||||
async def _operate_gateway(
|
||||
profile: str,
|
||||
action: str,
|
||||
user: User,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
"""执行 hermes gateway {action} --profile {profile} 并写入审计日志。"""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[settings.hermes_cli_path, "gateway", action, "--profile", profile],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
success = proc.returncode == 0
|
||||
detail = (proc.stdout or proc.stderr).strip()
|
||||
except Exception as e:
|
||||
success = False
|
||||
detail = str(e)
|
||||
|
||||
audit = AuditLog(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
action=f"gateway:{action}",
|
||||
target=f"profile:{profile}",
|
||||
detail=detail,
|
||||
)
|
||||
db.add(audit)
|
||||
await db.commit()
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=f"Gateway {action} 失败: {detail}")
|
||||
|
||||
return {"success": True, "action": action, "profile": profile, "detail": detail}
|
||||
|
||||
|
||||
@router.post("/{profile}/start")
|
||||
async def start_gateway(
|
||||
profile: str,
|
||||
user: User = Depends(require_role("owner", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""启动 Gateway。"""
|
||||
return await _operate_gateway(profile, "start", user, db)
|
||||
|
||||
|
||||
@router.post("/{profile}/stop")
|
||||
async def stop_gateway(
|
||||
profile: str,
|
||||
user: User = Depends(require_role("owner", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""停止 Gateway。"""
|
||||
return await _operate_gateway(profile, "stop", user, db)
|
||||
|
||||
|
||||
@router.post("/{profile}/restart")
|
||||
async def restart_gateway(
|
||||
profile: str,
|
||||
user: User = Depends(require_role("owner", "operator")),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""重启 Gateway。"""
|
||||
return await _operate_gateway(profile, "restart", user, db)
|
||||
+2
-1
@@ -4,7 +4,7 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.database import engine, Base
|
||||
from app.api import auth, users, audit, dashboard
|
||||
from app.api import auth, users, audit, dashboard, gateway
|
||||
from app.middleware.audit import AuditMiddleware
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ app.include_router(auth.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(audit.router)
|
||||
app.include_router(dashboard.router, prefix="/api/v1")
|
||||
app.include_router(gateway.router, prefix="/api/v1")
|
||||
|
||||
|
||||
@app.get("/api/v1/health")
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Hermes CLI 封装 — 通过 subprocess 调用 hermes 命令。"""
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _run_hermes(*args: str, timeout: int = 15) -> subprocess.CompletedProcess:
|
||||
"""执行 hermes CLI 命令,返回 CompletedProcess。"""
|
||||
return subprocess.run(
|
||||
[settings.hermes_cli_path, *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
def gateway_status_all() -> list[dict]:
|
||||
"""执行 `hermes gateway status`,返回解析后的 Gateway 状态列表。
|
||||
|
||||
出错时返回空列表并记录告警日志。
|
||||
"""
|
||||
try:
|
||||
proc = _run_hermes("gateway", "status")
|
||||
if proc.returncode != 0:
|
||||
logger.warning("hermes gateway status 返回非零: %s", proc.stderr.strip())
|
||||
return []
|
||||
data = json.loads(proc.stdout)
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return data.get("gateways", [])
|
||||
except FileNotFoundError:
|
||||
logger.warning("Hermes CLI 未找到: %s", settings.hermes_cli_path)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning("解析 hermes gateway status 输出失败: %s", e)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("hermes gateway status 执行超时")
|
||||
except Exception as e:
|
||||
logger.warning("调用 hermes gateway status 异常: %s", e)
|
||||
return []
|
||||
Reference in New Issue
Block a user