feat(monitor): NATS 集群 Raft 副本健康 + 基建 ping 延迟(监测组完善)

此前 /status 把 NATS 当一盏二元灯(连不上就 fatal 故恒真),看不出 3 节点集群里
哪个节点掉了、JetStream 持久流的 Raft 副本是否还齐(计费/状态/评测流不丢的关键)。
DB/Redis/MinIO 也只二元 ping、无延迟。

- bus.ClusterStatus:连的节点名 + 集群发现节点数(nc.Servers) + RTT(nc.RTT) + 6 条关键
  持久流(tasks/status/usage/eval/ingest/approvals)的 Raft 副本健康(leader + healthy/total,
  单节点部署记 1/1;某节点掉队 → healthy<total 记降级)。
- /status 新增 nats 集群对象 + NATS 灯改为'连接且无副本降级才绿'、detail 显示'N 节点·连 X·
  流副本齐全/降级'、latency=RTT;PG/Redis/MinIO 加 ping 往返耗时。
- admin StatusPage:新增 NATS 集群面板(节点/RTT/连接 + 各流 leader/副本健康/消息数表),
  基建行显示延迟。

sundynix-shared/gateway build+vet+test 绿;admin tsc 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-21 16:15:22 +08:00
parent a42f30239f
commit 9b153871eb
5 changed files with 217 additions and 14 deletions
+16
View File
@@ -670,11 +670,27 @@ export interface ToolGroup {
up: boolean;
tools: ToolInfo[] | null;
}
export interface NatsStreamHealth {
name: string;
leader: string;
replicas_healthy: number;
replicas_total: number;
messages: number;
}
export interface NatsClusterStatus {
connected: boolean;
connected_to: string;
known_servers: number;
rtt_ms: number;
streams: NatsStreamHealth[] | null;
degraded: number;
}
export interface SystemStatus {
checked_at: string;
infra: StatusItem[];
services: StatusItem[];
tools: ToolGroup[];
nats?: NatsClusterStatus;
}
export async function getStatus(): Promise<SystemStatus> {
+65
View File
@@ -330,6 +330,9 @@ export function StatusPage() {
)}
</div>
<div className="flex items-center gap-3">
{item.up && item.latency_ms != null && item.latency_ms > 0 && (
<span className="font-mono text-[10px] text-slate-400 tabular-nums">{item.latency_ms}ms</span>
)}
<code className="text-[10px] text-slate-400 font-mono bg-slate-100 px-1.5 py-0.5 rounded">
:{meta?.port || "—"}
</code>
@@ -344,6 +347,68 @@ export function StatusPage() {
</section>
</div>
{/* NATS 集群 · JetStream Raft 副本健康 */}
{data.nats && (
<section className="rounded-2xl border border-gray-200 bg-white p-5">
<div className="border-b border-slate-100 pb-3.5 mb-4 flex flex-wrap items-center justify-between gap-3">
<div>
<h3 className="text-sm font-bold text-slate-800 flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-indigo-500"></span>
NATS ·
</h3>
<p className="text-[10px] text-slate-400 mt-0.5">JetStream Raft //</p>
</div>
<div className="flex items-center gap-5 text-xs">
{[
{ k: "节点", v: String(data.nats.known_servers) },
{ k: "RTT", v: `${data.nats.rtt_ms}ms` },
{ k: "连接", v: data.nats.connected_to || "—" },
].map((m) => (
<div key={m.k} className="text-right">
<div className="text-[9px] uppercase text-slate-400 tracking-wider font-semibold">{m.k}</div>
<div className="font-bold text-slate-700 font-mono truncate max-w-[140px]" title={m.v}>{m.v}</div>
</div>
))}
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-left text-[10px] uppercase tracking-wide text-slate-400">
<th className="pb-2 font-medium"></th>
<th className="pb-2 font-medium">Leader</th>
<th className="pb-2 font-medium text-right"></th>
<th className="pb-2 font-medium text-right"></th>
</tr>
</thead>
<tbody>
{(data.nats.streams ?? []).map((s) => {
const ok = s.replicas_healthy >= s.replicas_total;
return (
<tr key={s.name} className="border-t border-slate-50">
<td className="py-2 font-mono text-slate-700">{s.name}</td>
<td className="py-2 font-mono text-[10px] text-slate-500 truncate max-w-[160px]" title={s.leader}>{s.leader || "—"}</td>
<td className="py-2 text-right">
<span className={`font-bold font-mono ${ok ? "text-emerald-600" : "text-amber-600"}`}>
{s.replicas_healthy}/{s.replicas_total}
</span>
{!ok && <span className="ml-1 text-[9px] text-amber-600"></span>}
</td>
<td className="py-2 text-right font-mono text-slate-500 tabular-nums">{s.messages.toLocaleString()}</td>
</tr>
);
})}
{(data.nats.streams ?? []).length === 0 && (
<tr>
<td colSpan={4} className="py-4 text-center text-[10px] text-slate-400"></td>
</tr>
)}
</tbody>
</table>
</div>
</section>
)}
{/* MCP 工具注册箱 */}
<section className="rounded-2xl border border-gray-200 bg-white p-6 space-y-4">
<div className="border-b border-slate-100 pb-3 flex flex-wrap items-center justify-between gap-3">