fix(monitoring): отображение всех метрик узла на графиках

Refs EventHub/EventHubFrontAdmin#24

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 17:27:43 +03:00
parent 8adf982587
commit da3fcbb6b0
+97 -8
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Card, Col, Row, Select, Tabs } from 'antd'; import { Card, Col, Row, Select, Table, Tabs } from 'antd';
import { import {
LineChart, LineChart,
Line, Line,
@@ -12,6 +12,7 @@ import {
} from 'recharts'; } from 'recharts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useMetricsStore, NodeMetric } from '../../store/metricsStore'; import { useMetricsStore, NodeMetric } from '../../store/metricsStore';
import { monitoringApi } from '../../api/monitoringApi';
const NODE_COLORS = ['#1677ff', '#52c41a', '#fa8c16', '#eb2f96', '#722ed1', '#13c2c2']; const NODE_COLORS = ['#1677ff', '#52c41a', '#fa8c16', '#eb2f96', '#722ed1', '#13c2c2'];
const TIME_RANGES = [ const TIME_RANGES = [
@@ -21,23 +22,35 @@ const TIME_RANGES = [
{ label: '1 час', value: 60 }, { label: '1 час', value: 60 },
]; ];
const bytesToMb = (value?: number) => (value ?? 0) / 1048576;
const formatTime = (ts: string) => dayjs(ts).format('HH:mm:ss'); const formatTime = (ts: string) => dayjs(ts).format('HH:mm:ss');
const buildChartData = (metrics: NodeMetric[]) => const buildChartData = (metrics: NodeMetric[]) =>
metrics.map((m) => ({ metrics.map((m) => ({
time: formatTime(m.timestamp), time: formatTime(m.timestamp),
cpu: m.cpu_utilization ?? 0, cpu: m.cpu_utilization ?? 0,
memoryMb: (m.memory_total ?? 0) / 1048576, memoryMb: bytesToMb(m.memory_total),
memoryProcessesMb: bytesToMb(m.memory_processes),
memoryAtomMb: bytesToMb(m.memory_atom),
memoryBinaryMb: bytesToMb(m.memory_binary),
memoryEtsMb: bytesToMb(m.memory_ets),
memoryAvailableMb: bytesToMb(m.memory_available),
ws: m.ws_connections ?? 0, ws: m.ws_connections ?? 0,
runQueue: m.run_queue ?? 0, runQueue: m.run_queue ?? 0,
processCount: m.process_count ?? 0,
activeSessions: m.active_sessions ?? 0,
ioIn: m.io_in ?? 0,
ioOut: m.io_out ?? 0,
mnesiaCommits: m.mnesia_commits ?? 0, mnesiaCommits: m.mnesia_commits ?? 0,
mnesiaFailures: m.mnesia_failures ?? 0, mnesiaFailures: m.mnesia_failures ?? 0,
tableSizes: m.table_sizes ?? {},
})); }));
const MetricChart: React.FC<{ data: ReturnType<typeof buildChartData>; lines: Array<{ key: string; color: string; name: string }> }> = ({ const MetricChart: React.FC<{
data, data: ReturnType<typeof buildChartData>;
lines, lines: Array<{ key: string; color: string; name: string }>;
}) => ( }> = ({ data, lines }) => (
<ResponsiveContainer width="100%" height={280}> <ResponsiveContainer width="100%" height={280}>
<LineChart data={data}> <LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
@@ -55,9 +68,38 @@ const MetricChart: React.FC<{ data: ReturnType<typeof buildChartData>; lines: Ar
const MonitoringPage: React.FC = () => { const MonitoringPage: React.FC = () => {
const allHistory = useMetricsStore((s) => s.allHistory); const allHistory = useMetricsStore((s) => s.allHistory);
const current = useMetricsStore((s) => s.current); const current = useMetricsStore((s) => s.current);
const setAllHistory = useMetricsStore((s) => s.setAllHistory);
const filterVisibleHistory = useMetricsStore((s) => s.filterVisibleHistory); const filterVisibleHistory = useMetricsStore((s) => s.filterVisibleHistory);
const visibleHistory = useMetricsStore((s) => s.visibleHistory); const visibleHistory = useMetricsStore((s) => s.visibleHistory);
const [minutes, setMinutes] = useState(15); const [minutes, setMinutes] = useState(15);
const [loadingHistory, setLoadingHistory] = useState(false);
useEffect(() => {
let cancelled = false;
const loadHistory = async () => {
setLoadingHistory(true);
try {
const to = dayjs().toISOString();
const from = dayjs().subtract(minutes, 'minute').toISOString();
const metrics = await monitoringApi.getNodeMetrics(from, to);
if (!cancelled && metrics.length > 0) {
setAllHistory(metrics);
}
} catch {
// live WS дополняет историю; REST недоступен — не блокируем страницу
} finally {
if (!cancelled) {
setLoadingHistory(false);
}
}
};
loadHistory();
return () => {
cancelled = true;
};
}, [minutes, setAllHistory]);
useEffect(() => { useEffect(() => {
filterVisibleHistory(minutes); filterVisibleHistory(minutes);
@@ -83,6 +125,7 @@ const MonitoringPage: React.FC = () => {
const tabItems = allNodes.map((node, idx) => { const tabItems = allNodes.map((node, idx) => {
const nodeMetrics = metricsByNode.get(node) || []; const nodeMetrics = metricsByNode.get(node) || [];
const chartData = buildChartData(nodeMetrics); const chartData = buildChartData(nodeMetrics);
const latestTableSizes = chartData.at(-1)?.tableSizes ?? {};
const color = NODE_COLORS[idx % NODE_COLORS.length]; const color = NODE_COLORS[idx % NODE_COLORS.length];
return { return {
@@ -96,7 +139,21 @@ const MonitoringPage: React.FC = () => {
data={chartData} data={chartData}
lines={[ lines={[
{ key: 'cpu', color, name: 'CPU %' }, { key: 'cpu', color, name: 'CPU %' },
{ key: 'memoryMb', color: '#1890ff', name: 'Память МБ' }, { key: 'memoryMb', color: '#1890ff', name: 'Память всего' },
{ key: 'memoryAvailableMb', color: '#13c2c2', name: 'Память доступно' },
]}
/>
</Card>
</Col>
<Col span={24}>
<Card title="Память по типам (МБ)">
<MetricChart
data={chartData}
lines={[
{ key: 'memoryProcessesMb', color: '#1677ff', name: 'Processes' },
{ key: 'memoryAtomMb', color: '#52c41a', name: 'Atom' },
{ key: 'memoryBinaryMb', color: '#fa8c16', name: 'Binary' },
{ key: 'memoryEtsMb', color: '#eb2f96', name: 'ETS' },
]} ]}
/> />
</Card> </Card>
@@ -108,6 +165,8 @@ const MonitoringPage: React.FC = () => {
lines={[ lines={[
{ key: 'ws', color: '#52c41a', name: 'WS' }, { key: 'ws', color: '#52c41a', name: 'WS' },
{ key: 'runQueue', color: '#fa8c16', name: 'Run queue' }, { key: 'runQueue', color: '#fa8c16', name: 'Run queue' },
{ key: 'processCount', color: '#722ed1', name: 'Processes' },
{ key: 'activeSessions', color: '#1677ff', name: 'Sessions' },
]} ]}
/> />
</Card> </Card>
@@ -123,6 +182,34 @@ const MonitoringPage: React.FC = () => {
/> />
</Card> </Card>
</Col> </Col>
<Col span={24}>
<Card title="I/O">
<MetricChart
data={chartData}
lines={[
{ key: 'ioIn', color: '#1677ff', name: 'IO in' },
{ key: 'ioOut', color: '#52c41a', name: 'IO out' },
]}
/>
</Card>
</Col>
<Col span={24}>
<Card title="Размеры таблиц Mnesia (последняя точка)">
<Table
size="small"
pagination={false}
rowKey="table"
dataSource={Object.entries(latestTableSizes).map(([table, size]) => ({
table,
size,
}))}
columns={[
{ title: 'Таблица', dataIndex: 'table', key: 'table' },
{ title: 'Записей', dataIndex: 'size', key: 'size' },
]}
/>
</Card>
</Col>
</Row> </Row>
), ),
}; };
@@ -141,7 +228,9 @@ const MonitoringPage: React.FC = () => {
</div> </div>
{allNodes.length === 0 ? ( {allNodes.length === 0 ? (
<Card>Ожидание метрик по WebSocket</Card> <Card loading={loadingHistory}>
{loadingHistory ? 'Загрузка метрик…' : 'Ожидание метрик по WebSocket…'}
</Card>
) : ( ) : (
<Tabs items={tabItems} /> <Tabs items={tabItems} />
)} )}