feat(platform): add ops capacity handoff

This commit is contained in:
lingniu
2026-07-04 20:37:20 +08:00
parent c0b99e9a3a
commit 87fa0f524c
2 changed files with 58 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { IconCopy } from '@douyinfe/semi-icons';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from '../api/client'; import { api } from '../api/client';
import type { CapacityMetrics, LinkHealth, OpsHealth } from '../api/types'; import type { CapacityMetrics, LinkHealth, OpsHealth } from '../api/types';
@@ -226,6 +227,44 @@ function capacityRisk(finding: string): CapacityRisk {
}; };
} }
function opsCapacityHandoffText(health: OpsHealth | null) {
const metrics = getCapacityMetrics(health);
const risks = (health?.capacityFindings ?? []).map(capacityRisk);
const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok');
const runtime = health?.runtime;
return [
'【车辆数据中台运维容量交接】',
`运行版本:${runtime?.platformRelease || '-'}`,
`Kafka Lag${formatNumber(metrics.kafkaLag)}`,
`活跃连接:${formatNumber(metrics.activeConnections)}`,
`Redis 在线 Key${formatNumber(health?.redisOnlineKeys)}`,
`NATS桥接consumer pending ${formatNumber(metrics.bridgeConsumerPending)} / ack pending ${formatNumber(metrics.bridgeAckPending)} / batch pending ${formatNumber(metrics.bridgeBatchPendingMessages)}`,
`快速写入consumer pending ${formatNumber(metrics.fastWriterConsumerPending)} / ack pending ${formatNumber(metrics.fastWriterAckPending)} / batch pending ${formatNumber(metrics.fastWriterBatchPending)}`,
`历史写入batch pending ${formatNumber(metrics.historyBatchPending)} / rows pending ${formatNumber(metrics.historyRowsPending)}`,
`存储状态TDengine ${statusText(health?.tdengineWritable)} / MySQL ${statusText(health?.mysqlWritable)}`,
`地图配置Web JS ${runtime?.amapWebJsConfigured ? '已配置' : '未配置'} / API ${runtime?.amapApiConfigured ? '已配置' : '未配置'} / ${amapSecurityText(health)}`,
`异常链路:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}(${item.detail || '-'})`).join('') : '正常'}`,
'',
'容量发现:',
...(risks.length > 0 ? risks.map((risk, index) => `${index + 1}. [${risk.category}] ${risk.finding} -> ${risk.action}`) : ['暂无容量风险']),
'',
'处置顺序:',
'1. 先看 Kafka Lag 与 NATS ACK pending确认消息是否卡在桥接或下游消费',
'2. 再看快速写入 pending确认 Redis/TDengine 实时链路是否影响在线和实时数据',
'3. 再看历史写入 pending确认轨迹、RAW、统计是否滞后',
'4. 最后核对 MySQL、TDengine、Redis 和地图配置,确认中台查询体验是否受影响'
].join('\n');
}
async function copyText(value: string, label: string) {
try {
await navigator.clipboard.writeText(value);
Toast.success(`已复制${label}`);
} catch {
Toast.error(`复制${label}失败`);
}
}
export function OpsQuality() { export function OpsQuality() {
const [health, setHealth] = useState<OpsHealth | null>(null); const [health, setHealth] = useState<OpsHealth | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -252,7 +291,12 @@ export function OpsQuality() {
<PageHeader <PageHeader
title="运维质量" title="运维质量"
description="集中查看网关、消息队列、缓存、时序库、关系库、地图配置和运行版本状态" description="集中查看网关、消息队列、缓存、时序库、关系库、地图配置和运行版本状态"
actions={<Button size="small" loading={loading} onClick={load}></Button>} actions={(
<Space>
<Button size="small" icon={<IconCopy />} aria-label="复制容量交接" disabled={!health} onClick={() => copyText(opsCapacityHandoffText(health), '运维容量交接')}></Button>
<Button size="small" loading={loading} onClick={load}></Button>
</Space>
)}
/> />
<div className="vp-kpi-grid"> <div className="vp-kpi-grid">

View File

@@ -3497,6 +3497,11 @@ test('renders notification rules as a standalone operations page', async () => {
test('renders ops quality as a standalone runtime health page', async () => { test('renders ops quality as a standalone runtime health page', async () => {
window.history.replaceState(null, '', '/#/ops-quality'); window.history.replaceState(null, '', '/#/ops-quality');
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText }
});
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input); const path = String(input);
if (path.includes('/api/ops/health')) { if (path.includes('/api/ops/health')) {
@@ -3579,6 +3584,14 @@ test('renders ops quality as a standalone runtime health page', async () => {
expect(screen.getAllByText('NATS 桥接待消费').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('NATS 桥接待消费').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('桥接处置顺序')).toBeInTheDocument(); expect(screen.getByText('桥接处置顺序')).toBeInTheDocument();
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined);
fireEvent.click(screen.getByRole('button', { name: '复制容量交接' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台运维容量交接】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本platform-ops-test'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('Kafka Lag42'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('NATS桥接consumer pending 1,482,047 / ack pending 4,000'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('存储状态TDengine 正常 / MySQL 异常'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 先看 Kafka Lag 与 NATS ACK pending'));
}); });
test('copies notification text from quality priority queue', async () => { test('copies notification text from quality priority queue', async () => {