feat(platform): expose structured capacity metrics
This commit is contained in:
@@ -351,6 +351,7 @@ export interface OpsHealth {
|
||||
linkHealth: LinkHealth[];
|
||||
kafkaLag: number | null;
|
||||
activeConnections: number | null;
|
||||
capacityMetrics?: CapacityMetrics;
|
||||
capacityFindings: string[];
|
||||
redisOnlineKeys: number | null;
|
||||
tdengineWritable: boolean;
|
||||
@@ -358,6 +359,19 @@ export interface OpsHealth {
|
||||
runtime: RuntimeInfo;
|
||||
}
|
||||
|
||||
export interface CapacityMetrics {
|
||||
activeConnections: number;
|
||||
kafkaLag: number;
|
||||
bridgeConsumerPending: number;
|
||||
bridgeAckPending: number;
|
||||
bridgeBatchPendingMessages: number;
|
||||
fastWriterConsumerPending: number;
|
||||
fastWriterAckPending: number;
|
||||
fastWriterBatchPending: number;
|
||||
historyBatchPending: number;
|
||||
historyRowsPending: number;
|
||||
}
|
||||
|
||||
export interface RuntimeInfo {
|
||||
requestTimeoutMs: number;
|
||||
amapWebJsConfigured?: boolean;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { LinkHealth, OpsHealth } from '../api/types';
|
||||
import type { CapacityMetrics, LinkHealth, OpsHealth } from '../api/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
|
||||
const statusColor: Record<string, 'green' | 'orange' | 'red' | 'grey'> = {
|
||||
@@ -47,6 +47,112 @@ type CapacityRisk = {
|
||||
action: string;
|
||||
};
|
||||
|
||||
type CapacityMetricRow = {
|
||||
key: keyof CapacityMetrics;
|
||||
name: string;
|
||||
value: number;
|
||||
threshold: string;
|
||||
status: 'ok' | 'warning' | 'error';
|
||||
detail: string;
|
||||
};
|
||||
|
||||
const emptyCapacityMetrics: CapacityMetrics = {
|
||||
activeConnections: 0,
|
||||
kafkaLag: 0,
|
||||
bridgeConsumerPending: 0,
|
||||
bridgeAckPending: 0,
|
||||
bridgeBatchPendingMessages: 0,
|
||||
fastWriterConsumerPending: 0,
|
||||
fastWriterAckPending: 0,
|
||||
fastWriterBatchPending: 0,
|
||||
historyBatchPending: 0,
|
||||
historyRowsPending: 0
|
||||
};
|
||||
|
||||
function getCapacityMetrics(health: OpsHealth | null): CapacityMetrics {
|
||||
return {
|
||||
...emptyCapacityMetrics,
|
||||
...(health?.capacityMetrics ?? {}),
|
||||
activeConnections: health?.capacityMetrics?.activeConnections ?? health?.activeConnections ?? 0,
|
||||
kafkaLag: health?.capacityMetrics?.kafkaLag ?? health?.kafkaLag ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
function metricStatus(value: number, warningAt: number, errorAt?: number): 'ok' | 'warning' | 'error' {
|
||||
if (errorAt != null && value > errorAt) return 'error';
|
||||
return value > warningAt ? 'warning' : 'ok';
|
||||
}
|
||||
|
||||
function capacityMetricRows(health: OpsHealth | null): CapacityMetricRow[] {
|
||||
const metrics = getCapacityMetrics(health);
|
||||
return [
|
||||
{
|
||||
key: 'bridgeConsumerPending',
|
||||
name: 'NATS 桥接待消费',
|
||||
value: metrics.bridgeConsumerPending,
|
||||
threshold: '> 10,000 预警',
|
||||
status: metricStatus(metrics.bridgeConsumerPending, 10000),
|
||||
detail: 'RAW/FIELDS 从 NATS 进入 Kafka 前的待消费量。'
|
||||
},
|
||||
{
|
||||
key: 'bridgeAckPending',
|
||||
name: 'NATS 桥接 ACK',
|
||||
value: metrics.bridgeAckPending,
|
||||
threshold: '> 100 严重',
|
||||
status: metricStatus(metrics.bridgeAckPending, 0, 100),
|
||||
detail: '桥接消费者已拉取但未确认的消息,持续不为 0 需要优先排查。'
|
||||
},
|
||||
{
|
||||
key: 'fastWriterConsumerPending',
|
||||
name: '快速写入待消费',
|
||||
value: metrics.fastWriterConsumerPending,
|
||||
threshold: '> 10,000 预警',
|
||||
status: metricStatus(metrics.fastWriterConsumerPending, 10000),
|
||||
detail: '实时数据写 TDengine、Redis 前的 NATS 待消费量。'
|
||||
},
|
||||
{
|
||||
key: 'fastWriterAckPending',
|
||||
name: '快速写入 ACK',
|
||||
value: metrics.fastWriterAckPending,
|
||||
threshold: '> 10 严重',
|
||||
status: metricStatus(metrics.fastWriterAckPending, 0, 10),
|
||||
detail: '快速写入消费者已拉取但未确认的消息。'
|
||||
},
|
||||
{
|
||||
key: 'historyBatchPending',
|
||||
name: '历史批次待写',
|
||||
value: metrics.historyBatchPending,
|
||||
threshold: '> 0 预警',
|
||||
status: metricStatus(metrics.historyBatchPending, 0),
|
||||
detail: '历史落库批处理内存队列,正常应快速归零。'
|
||||
},
|
||||
{
|
||||
key: 'historyRowsPending',
|
||||
name: '历史行待写',
|
||||
value: metrics.historyRowsPending,
|
||||
threshold: '> 0 预警',
|
||||
status: metricStatus(metrics.historyRowsPending, 0),
|
||||
detail: '历史落库批处理待写行数,用于判断 TDengine/MySQL 写入是否滞后。'
|
||||
},
|
||||
{
|
||||
key: 'kafkaLag',
|
||||
name: 'Kafka 消费 Lag',
|
||||
value: metrics.kafkaLag,
|
||||
threshold: '> 0 预警',
|
||||
status: metricStatus(metrics.kafkaLag, 0),
|
||||
detail: '后续统计和历史消费链路的 Kafka 积压。'
|
||||
},
|
||||
{
|
||||
key: 'activeConnections',
|
||||
name: '网关活跃连接',
|
||||
value: metrics.activeConnections,
|
||||
threshold: '> 100,000 预警',
|
||||
status: metricStatus(metrics.activeConnections, 100000),
|
||||
detail: '32960、808、MQTT 当前活跃连接总量。'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function firstNumber(value: string) {
|
||||
const match = value.match(/[\d,.]+/);
|
||||
return match ? match[0] : '-';
|
||||
@@ -138,6 +244,7 @@ export function OpsQuality() {
|
||||
|
||||
const runtime = health?.runtime;
|
||||
const capacityRisks = (health?.capacityFindings ?? []).map(capacityRisk);
|
||||
const capacityRows = capacityMetricRows(health);
|
||||
const bridgeRisks = capacityRisks.filter((item) => item.category === 'NATS -> Kafka');
|
||||
|
||||
return (
|
||||
@@ -196,6 +303,31 @@ export function OpsQuality() {
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card bordered title="消息链路积压" loading={loading} style={{ marginTop: 16 }}>
|
||||
<Table<CapacityMetricRow>
|
||||
pagination={false}
|
||||
dataSource={capacityRows}
|
||||
rowKey={(row?: CapacityMetricRow) => row?.key ?? ''}
|
||||
columns={[
|
||||
{ title: '指标', dataIndex: 'name' },
|
||||
{
|
||||
title: '当前值',
|
||||
width: 140,
|
||||
render: (_: unknown, row: CapacityMetricRow) => (
|
||||
<Typography.Text strong>{formatNumber(row.value)}</Typography.Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 120,
|
||||
render: (_: unknown, row: CapacityMetricRow) => <Tag color={statusColor[row.status]}>{row.status}</Tag>
|
||||
},
|
||||
{ title: '阈值', width: 150, dataIndex: 'threshold' },
|
||||
{ title: '说明', dataIndex: 'detail' }
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{capacityRisks.length ? (
|
||||
<Card bordered title="容量与风险发现" style={{ marginTop: 16 }}>
|
||||
<div className="vp-risk-grid">
|
||||
|
||||
@@ -3374,6 +3374,18 @@ test('renders ops quality as a standalone runtime health page', async () => {
|
||||
],
|
||||
kafkaLag: 42,
|
||||
activeConnections: 1234,
|
||||
capacityMetrics: {
|
||||
activeConnections: 1234,
|
||||
kafkaLag: 42,
|
||||
bridgeConsumerPending: 1482047,
|
||||
bridgeAckPending: 4000,
|
||||
bridgeBatchPendingMessages: 12,
|
||||
fastWriterConsumerPending: 8,
|
||||
fastWriterAckPending: 0,
|
||||
fastWriterBatchPending: 0,
|
||||
historyBatchPending: 2,
|
||||
historyRowsPending: 320
|
||||
},
|
||||
capacityFindings: [
|
||||
'Kafka lag 42',
|
||||
'bridge ack pending 4000 exceeds 100',
|
||||
@@ -3413,7 +3425,7 @@ test('renders ops quality as a standalone runtime health page', async () => {
|
||||
expect(await screen.findByRole('heading', { name: '运维质量' })).toBeInTheDocument();
|
||||
expect(screen.getByText('platform-ops-test')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('42').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('1,234')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('1,234').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('368')).toBeInTheDocument();
|
||||
expect(screen.getByText('TDengine 写入')).toBeInTheDocument();
|
||||
expect(screen.getByText('MySQL 写入')).toBeInTheDocument();
|
||||
@@ -3421,9 +3433,14 @@ test('renders ops quality as a standalone runtime health page', async () => {
|
||||
expect(screen.getByText('/_AMapService')).toBeInTheDocument();
|
||||
expect(screen.getByText('安全码未暴露')).toBeInTheDocument();
|
||||
expect(screen.getByText('gb32960-gateway')).toBeInTheDocument();
|
||||
expect(screen.getByText('消息链路积压')).toBeInTheDocument();
|
||||
expect(screen.getByText('快速写入待消费')).toBeInTheDocument();
|
||||
expect(screen.getByText('历史行待写')).toBeInTheDocument();
|
||||
expect(screen.getByText('1,482,047')).toBeInTheDocument();
|
||||
expect(screen.getByText('4,000')).toBeInTheDocument();
|
||||
expect(screen.getByText('Kafka lag 42')).toBeInTheDocument();
|
||||
expect(screen.getByText('NATS 桥接 ACK 未确认')).toBeInTheDocument();
|
||||
expect(screen.getByText('NATS 桥接待消费')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('NATS 桥接待消费').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('桥接处置顺序')).toBeInTheDocument();
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user