feat: add Prometheus system monitoring
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { request, withQuery } from '../core/httpClient';
|
||||
import type { InfrastructureMonitoringOverview, InfrastructureMonitoringRange } from '../types';
|
||||
|
||||
export const adminInfrastructureMonitoringApi = {
|
||||
getInfrastructureMonitoringOverview: (range: InfrastructureMonitoringRange) =>
|
||||
request<InfrastructureMonitoringOverview>(withQuery('/admin/infrastructure-monitoring/overview', { range })),
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { adminOperationsApi } from './admin/operations.api';
|
||||
import { adminGovernanceApi } from './admin/governance.api';
|
||||
import { adminFilesApi } from './admin/files.api';
|
||||
import { adminSignatureRetirementApi } from './admin/signature-retirement.api';
|
||||
import { adminInfrastructureMonitoringApi } from './admin/infrastructure-monitoring.api';
|
||||
|
||||
export const adminApi = {
|
||||
...adminIdentityApi,
|
||||
@@ -18,4 +19,5 @@ export const adminApi = {
|
||||
...adminGovernanceApi,
|
||||
...adminFilesApi,
|
||||
...adminSignatureRetirementApi,
|
||||
...adminInfrastructureMonitoringApi,
|
||||
};
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './channels-reports';
|
||||
export * from './operations';
|
||||
export * from './governance';
|
||||
export * from './signature-retirement';
|
||||
export * from './infrastructure-monitoring';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
export type InfrastructureMonitoringRange = '1h' | '24h' | '7d';
|
||||
|
||||
export type InfrastructureMetricPoint = {
|
||||
timestamp: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type InfrastructureServiceStatus = {
|
||||
key: string;
|
||||
name: string;
|
||||
unit: string;
|
||||
status: 'healthy' | 'unhealthy' | 'unknown';
|
||||
};
|
||||
|
||||
export type InfrastructureAlert = {
|
||||
fingerprint: string;
|
||||
name: string;
|
||||
severity: 'info' | 'warning' | 'critical';
|
||||
status: string;
|
||||
startedAt: string;
|
||||
summary: string;
|
||||
description?: string;
|
||||
currentValue?: string;
|
||||
threshold?: string;
|
||||
service?: string;
|
||||
instance?: string;
|
||||
};
|
||||
|
||||
export type InfrastructureMonitoringOverview = {
|
||||
available: boolean;
|
||||
range: InfrastructureMonitoringRange;
|
||||
collectedAt: string;
|
||||
lastSampleAt: string | null;
|
||||
error?: string;
|
||||
summary: {
|
||||
overallStatus: 'healthy' | 'warning' | 'critical' | 'unknown';
|
||||
serviceTotal: number;
|
||||
serviceHealthy: number;
|
||||
warningAlerts: number;
|
||||
criticalAlerts: number;
|
||||
activeAlerts: number;
|
||||
};
|
||||
metrics: {
|
||||
cpuUsagePercent: number | null;
|
||||
memoryUsagePercent: number | null;
|
||||
memoryTotalBytes: number | null;
|
||||
memoryAvailableBytes: number | null;
|
||||
diskUsagePercent: number | null;
|
||||
diskTotalBytes: number | null;
|
||||
diskAvailableBytes: number | null;
|
||||
networkReceiveBytesPerSecond: number | null;
|
||||
networkTransmitBytesPerSecond: number | null;
|
||||
load1: number | null;
|
||||
uptimeSeconds: number | null;
|
||||
};
|
||||
trends: {
|
||||
cpuUsagePercent: InfrastructureMetricPoint[];
|
||||
memoryUsagePercent: InfrastructureMetricPoint[];
|
||||
diskUsagePercent: InfrastructureMetricPoint[];
|
||||
networkReceiveBytesPerSecond: InfrastructureMetricPoint[];
|
||||
networkTransmitBytesPerSecond: InfrastructureMetricPoint[];
|
||||
};
|
||||
services: InfrastructureServiceStatus[];
|
||||
alerts: InfrastructureAlert[];
|
||||
};
|
||||
@@ -0,0 +1,309 @@
|
||||
.admin-system-monitoring-page {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.system-monitoring-heading {
|
||||
align-items: flex-end;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.system-monitoring-title-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.system-monitoring-title-row h1 {
|
||||
color: var(--color-text-strong);
|
||||
font-size: 24px;
|
||||
line-height: 1.25;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.system-monitoring-title-row p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.system-monitoring-controls,
|
||||
.system-monitoring-range {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.system-monitoring-controls {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.system-monitoring-range {
|
||||
background: var(--color-surface-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.system-monitoring-range button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.system-monitoring-range button:hover {
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.system-monitoring-range button:focus-visible {
|
||||
box-shadow: var(--focus-ring);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.system-monitoring-range button.is-active {
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-xs);
|
||||
color: var(--color-selected);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.system-monitoring-unavailable {
|
||||
align-items: flex-start;
|
||||
background: var(--color-danger-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--color-danger) 24%, transparent);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-danger);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.system-monitoring-unavailable div {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.system-monitoring-unavailable span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.system-monitoring-health {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
grid-template-columns: minmax(220px, 1.35fr) repeat(4, minmax(120px, 1fr));
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.system-monitoring-health__mark {
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
height: 48px;
|
||||
justify-content: center;
|
||||
margin-right: 13px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.system-monitoring-health__mark.is-healthy { background: var(--color-success-soft); color: var(--color-success); }
|
||||
.system-monitoring-health__mark.is-warning { background: var(--color-warning-soft); color: var(--color-warning); }
|
||||
.system-monitoring-health__mark.is-critical { background: var(--color-danger-soft); color: var(--color-danger); }
|
||||
.system-monitoring-health__mark.is-unknown { background: var(--color-surface-muted); color: var(--color-text-muted); }
|
||||
|
||||
.system-monitoring-health__copy {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr;
|
||||
grid-template-rows: repeat(3, auto);
|
||||
}
|
||||
|
||||
.system-monitoring-health__copy .system-monitoring-health__mark { grid-row: 1 / 4; }
|
||||
.system-monitoring-health__copy > span,
|
||||
.system-monitoring-health__fact > span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.system-monitoring-health__copy > strong { color: var(--color-text-strong); font-size: 17px; }
|
||||
.system-monitoring-health__copy > small,
|
||||
.system-monitoring-health__fact > small { color: var(--color-text-subtle); font-size: 11px; }
|
||||
|
||||
.system-monitoring-health__fact {
|
||||
border-left: 1px solid var(--color-border);
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.system-monitoring-health__fact strong {
|
||||
color: var(--color-text-strong);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.system-monitoring-metrics {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.system-monitoring-metric {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.system-monitoring-metric__icon {
|
||||
align-items: center;
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
flex: 0 0 40px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.system-monitoring-metric__icon.is-blue { background: var(--color-info-soft); color: var(--color-info); }
|
||||
.system-monitoring-metric__icon.is-violet { background: #f5f3ff; color: #7c3aed; }
|
||||
.system-monitoring-metric__icon.is-amber { background: var(--color-warning-soft); color: var(--color-warning); }
|
||||
.system-monitoring-metric__icon.is-green { background: var(--color-success-soft); color: #0f766e; }
|
||||
|
||||
.system-monitoring-metric > div:last-child {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.system-monitoring-metric span,
|
||||
.system-monitoring-metric small { color: var(--color-text-muted); font-size: 12px; }
|
||||
.system-monitoring-metric strong { color: var(--color-text-strong); font-size: 23px; line-height: 1.25; }
|
||||
.system-monitoring-metric small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.system-monitoring-main-grid {
|
||||
align-items: start;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
}
|
||||
|
||||
.system-monitoring-chart-stack {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.system-monitoring-chart-card,
|
||||
.system-monitoring-services,
|
||||
.system-monitoring-alerts { padding: 18px; }
|
||||
|
||||
.system-monitoring-chart-card header,
|
||||
.system-monitoring-services > header,
|
||||
.system-monitoring-alerts > header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.system-monitoring-chart-card header div,
|
||||
.system-monitoring-services > header div,
|
||||
.system-monitoring-alerts > header div {
|
||||
align-items: center;
|
||||
color: var(--color-text-strong);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.system-monitoring-chart-card header > span { color: var(--color-text); font-weight: var(--font-weight-semibold); }
|
||||
|
||||
.system-monitoring-chart-empty {
|
||||
align-items: center;
|
||||
color: var(--color-text-subtle);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
height: 230px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.system-monitoring-service-list { display: grid; }
|
||||
|
||||
.system-monitoring-service {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
grid-template-columns: 9px 1fr auto;
|
||||
padding: 13px 0;
|
||||
}
|
||||
|
||||
.system-monitoring-service:last-child { border-bottom: 0; }
|
||||
.system-monitoring-service__dot { background: var(--color-text-subtle); border-radius: 50%; height: 8px; width: 8px; }
|
||||
.system-monitoring-service__dot.is-healthy { background: var(--color-success); box-shadow: 0 0 0 3px var(--color-success-soft); }
|
||||
.system-monitoring-service__dot.is-unhealthy { background: var(--color-danger); box-shadow: 0 0 0 3px var(--color-danger-soft); }
|
||||
.system-monitoring-service__dot.is-unknown { background: var(--color-text-subtle); box-shadow: 0 0 0 3px var(--color-surface-muted); }
|
||||
.system-monitoring-service div { display: grid; gap: 1px; min-width: 0; }
|
||||
.system-monitoring-service strong { color: var(--color-text); font-size: 13px; }
|
||||
.system-monitoring-service small { color: var(--color-text-subtle); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.system-monitoring-service > span:last-child { color: var(--color-text-muted); font-size: 12px; }
|
||||
|
||||
.system-monitoring-collector-note {
|
||||
align-items: flex-start;
|
||||
background: var(--color-bg-subtle);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
gap: 8px;
|
||||
line-height: 1.55;
|
||||
margin-top: 14px;
|
||||
padding: 11px;
|
||||
}
|
||||
|
||||
.system-monitoring-collector-note svg { flex: 0 0 auto; margin-top: 1px; }
|
||||
|
||||
.system-monitoring-alerts { overflow: hidden; }
|
||||
.system-monitoring-alerts > header > span { align-items: center; color: var(--color-text-muted); display: flex; font-size: 12px; gap: 5px; }
|
||||
.system-monitoring-alert-copy { display: grid; gap: 2px; }
|
||||
.system-monitoring-alert-copy strong { color: var(--color-text-strong); }
|
||||
.system-monitoring-alert-copy span { color: var(--color-text-muted); font-size: 12px; }
|
||||
|
||||
.is-spinning { animation: system-monitoring-spin 0.9s linear infinite; }
|
||||
@keyframes system-monitoring-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.system-monitoring-health { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 18px; }
|
||||
.system-monitoring-health__copy { grid-column: span 2; }
|
||||
.system-monitoring-health__fact:nth-last-child(-n + 2) { border-left: 0; padding-left: 0; }
|
||||
.system-monitoring-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.system-monitoring-main-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.system-monitoring-services { order: -1; }
|
||||
.system-monitoring-service-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.system-monitoring-service { border-bottom: 0; border-right: 1px solid var(--color-border); padding: 11px 12px; }
|
||||
.system-monitoring-service:nth-child(3n) { border-right: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.system-monitoring-heading { align-items: stretch; flex-direction: column; }
|
||||
.system-monitoring-controls { align-items: stretch; flex-direction: column; }
|
||||
.system-monitoring-range { display: grid; grid-template-columns: repeat(3, 1fr); }
|
||||
.system-monitoring-title-row { align-items: flex-start; justify-content: space-between; }
|
||||
.system-monitoring-health { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.system-monitoring-health__copy { grid-column: 1 / -1; }
|
||||
.system-monitoring-health__fact { border-left: 0; border-top: 1px solid var(--color-border); padding: 12px 0 0; }
|
||||
.system-monitoring-metrics,
|
||||
.system-monitoring-chart-stack,
|
||||
.system-monitoring-service-list { grid-template-columns: minmax(0, 1fr); }
|
||||
.system-monitoring-service { border-bottom: 1px solid var(--color-border); border-right: 0; padding: 13px 0; }
|
||||
.system-monitoring-chart-card { padding: 14px 10px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.is-spinning { animation: none; }
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
Cpu,
|
||||
Database,
|
||||
HardDrive,
|
||||
MemoryStick,
|
||||
Network,
|
||||
RefreshCw,
|
||||
Server,
|
||||
ShieldAlert,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
adminApi,
|
||||
type InfrastructureAlert,
|
||||
type InfrastructureMetricPoint,
|
||||
type InfrastructureMonitoringOverview,
|
||||
type InfrastructureMonitoringRange,
|
||||
} from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import './AdminSystemMonitoringPage.css';
|
||||
|
||||
const RANGE_OPTIONS: Array<{ value: InfrastructureMonitoringRange; label: string }> = [
|
||||
{ value: '1h', label: '近1小时' },
|
||||
{ value: '24h', label: '近24小时' },
|
||||
{ value: '7d', label: '近7天' },
|
||||
];
|
||||
|
||||
const STATUS_COPY = {
|
||||
healthy: { label: '运行正常', tone: 'success' as const },
|
||||
warning: { label: '需要关注', tone: 'warning' as const },
|
||||
critical: { label: '严重告警', tone: 'danger' as const },
|
||||
unknown: { label: '状态未知', tone: 'neutral' as const },
|
||||
};
|
||||
|
||||
function formatPercent(value: number | null) {
|
||||
return value === null ? '—' : `${value.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function formatBytes(value: number | null) {
|
||||
if (value === null) return '—';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let amount = value;
|
||||
let index = 0;
|
||||
while (amount >= 1024 && index < units.length - 1) {
|
||||
amount /= 1024;
|
||||
index += 1;
|
||||
}
|
||||
return `${amount.toFixed(index >= 3 ? 1 : 0)} ${units[index]}`;
|
||||
}
|
||||
|
||||
function formatRate(value: number | null) {
|
||||
return value === null ? '—' : `${formatBytes(value)}/s`;
|
||||
}
|
||||
|
||||
function totalNetworkRate(receive: number | null | undefined, transmit: number | null | undefined) {
|
||||
if (receive === null || receive === undefined || transmit === null || transmit === undefined) return null;
|
||||
return receive + transmit;
|
||||
}
|
||||
|
||||
function formatUptime(value: number | null) {
|
||||
if (value === null) return '—';
|
||||
const days = Math.floor(value / 86400);
|
||||
const hours = Math.floor((value % 86400) / 3600);
|
||||
return days > 0 ? `${days}天 ${hours}小时` : `${hours}小时`;
|
||||
}
|
||||
|
||||
function formatTime(value: string | null) {
|
||||
if (!value) return '暂无采样';
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
function formatDuration(startedAt: string) {
|
||||
const milliseconds = Date.now() - Date.parse(startedAt);
|
||||
if (!Number.isFinite(milliseconds) || milliseconds < 0) return '—';
|
||||
const minutes = Math.floor(milliseconds / 60_000);
|
||||
if (minutes < 60) return `${Math.max(minutes, 1)}分钟`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return hours < 24 ? `${hours}小时 ${minutes % 60}分钟` : `${Math.floor(hours / 24)}天 ${hours % 24}小时`;
|
||||
}
|
||||
|
||||
function timeLabels(points: InfrastructureMetricPoint[], range: InfrastructureMonitoringRange) {
|
||||
return points.map((point) => new Intl.DateTimeFormat('zh-CN', range === '7d'
|
||||
? { month: '2-digit', day: '2-digit', hour: '2-digit', hour12: false }
|
||||
: { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
.format(new Date(point.timestamp)));
|
||||
}
|
||||
|
||||
function makeTrendOption(params: {
|
||||
range: InfrastructureMonitoringRange;
|
||||
series: Array<{ name: string; points: InfrastructureMetricPoint[]; color: string }>;
|
||||
suffix: string;
|
||||
maximum?: number;
|
||||
}): EChartsOption {
|
||||
const first = params.series[0]?.points ?? [];
|
||||
return {
|
||||
animationDuration: 280,
|
||||
color: params.series.map((item) => item.color),
|
||||
grid: { left: 8, right: 16, top: 34, bottom: 4, containLabel: true },
|
||||
legend: params.series.length > 1 ? { top: 0, right: 0, textStyle: { color: '#6b7280', fontSize: 12 } } : undefined,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
valueFormatter: (value) => `${Number(value).toFixed(1)}${params.suffix}`,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: timeLabels(first, params.range),
|
||||
axisLine: { lineStyle: { color: '#e5e7eb' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#9ca3af', hideOverlap: true, margin: 12 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', min: 0, max: params.maximum,
|
||||
axisLabel: { color: '#9ca3af', formatter: `{value}${params.suffix}` },
|
||||
splitLine: { lineStyle: { color: '#eef0f3' } },
|
||||
},
|
||||
series: params.series.map((item) => ({
|
||||
name: item.name,
|
||||
data: item.points.map((point) => point.value),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { width: 2.5 },
|
||||
areaStyle: { opacity: 0.07 },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function severityTag(severity: InfrastructureAlert['severity']) {
|
||||
if (severity === 'critical') return <Tag tone="danger">严重</Tag>;
|
||||
if (severity === 'warning') return <Tag tone="warning">警告</Tag>;
|
||||
return <Tag tone="info">提示</Tag>;
|
||||
}
|
||||
|
||||
const alertColumns: Array<TableColumn<InfrastructureAlert>> = [
|
||||
{ key: 'severity', title: '级别', width: '82px', render: (record) => severityTag(record.severity) },
|
||||
{
|
||||
key: 'alert', title: '告警', width: '280px', render: (record) => (
|
||||
<div className="system-monitoring-alert-copy"><strong>{record.name}</strong><span>{record.summary}</span></div>
|
||||
),
|
||||
},
|
||||
{ key: 'service', title: '服务 / 实例', width: '190px', render: (record) => record.service || record.instance || '主机资源' },
|
||||
{ key: 'value', title: '当前值 / 阈值', width: '150px', render: (record) => `${record.currentValue || '—'} / ${record.threshold || '—'}` },
|
||||
{ key: 'startedAt', title: '开始时间', width: '150px', render: (record) => formatTime(record.startedAt) },
|
||||
{ key: 'duration', title: '持续时间', width: '120px', render: (record) => formatDuration(record.startedAt) },
|
||||
];
|
||||
|
||||
export function AdminSystemMonitoringPage() {
|
||||
const [range, setRange] = useState<InfrastructureMonitoringRange>('24h');
|
||||
const [overview, setOverview] = useState<InfrastructureMonitoringOverview | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const requestSequence = useRef(0);
|
||||
const pendingRequests = useRef(0);
|
||||
|
||||
const loadData = useCallback(async (supersede = false) => {
|
||||
if (!supersede && pendingRequests.current > 0) return;
|
||||
pendingRequests.current += 1;
|
||||
const sequence = ++requestSequence.current;
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await adminApi.getInfrastructureMonitoringOverview(range);
|
||||
if (sequence !== requestSequence.current) return;
|
||||
setOverview(result);
|
||||
setError(result.available ? '' : result.error || '监控数据当前不可用');
|
||||
} catch (reason) {
|
||||
if (sequence !== requestSequence.current) return;
|
||||
setOverview(null);
|
||||
setError(reason instanceof Error ? reason.message : '监控数据加载失败');
|
||||
} finally {
|
||||
if (sequence === requestSequence.current) setLoading(false);
|
||||
pendingRequests.current -= 1;
|
||||
}
|
||||
}, [range]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadData(true);
|
||||
const intervalId = window.setInterval(() => {
|
||||
if (document.visibilityState === 'visible') void loadData();
|
||||
}, 30_000);
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === 'visible') void loadData();
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
requestSequence.current += 1;
|
||||
window.clearInterval(intervalId);
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
};
|
||||
}, [loadData]);
|
||||
|
||||
const status = STATUS_COPY[overview?.summary.overallStatus ?? 'unknown'];
|
||||
const cpuOption = useMemo(() => makeTrendOption({
|
||||
range, maximum: 100, suffix: '%', series: [{ name: 'CPU', points: overview?.trends.cpuUsagePercent ?? [], color: '#2563eb' }],
|
||||
}), [overview?.trends.cpuUsagePercent, range]);
|
||||
const memoryOption = useMemo(() => makeTrendOption({
|
||||
range, maximum: 100, suffix: '%', series: [{ name: '内存', points: overview?.trends.memoryUsagePercent ?? [], color: '#7c3aed' }],
|
||||
}), [overview?.trends.memoryUsagePercent, range]);
|
||||
const diskOption = useMemo(() => makeTrendOption({
|
||||
range, maximum: 100, suffix: '%', series: [{ name: '根磁盘', points: overview?.trends.diskUsagePercent ?? [], color: '#d97706' }],
|
||||
}), [overview?.trends.diskUsagePercent, range]);
|
||||
const networkOption = useMemo(() => makeTrendOption({
|
||||
range, suffix: ' B/s', series: [
|
||||
{ name: '接收', points: overview?.trends.networkReceiveBytesPerSecond ?? [], color: '#0f766e' },
|
||||
{ name: '发送', points: overview?.trends.networkTransmitBytesPerSecond ?? [], color: '#2563eb' },
|
||||
],
|
||||
}), [overview?.trends.networkReceiveBytesPerSecond, overview?.trends.networkTransmitBytesPerSecond, range]);
|
||||
|
||||
const metrics = overview?.metrics;
|
||||
const serviceHealthy = overview?.summary.serviceHealthy ?? 0;
|
||||
const serviceTotal = overview?.summary.serviceTotal ?? 6;
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-system-monitoring-page">
|
||||
<div className="page-heading system-monitoring-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['系统管理', '系统监控']} />
|
||||
<div className="system-monitoring-title-row">
|
||||
<div>
|
||||
<h1>系统监控</h1>
|
||||
<p>服务器资源、核心服务与活动告警</p>
|
||||
</div>
|
||||
<Tag tone={status.tone}>{status.label}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div className="system-monitoring-controls">
|
||||
<div className="system-monitoring-range" aria-label="监控时间范围" role="group">
|
||||
{RANGE_OPTIONS.map((option) => (
|
||||
<button
|
||||
aria-pressed={range === option.value}
|
||||
className={range === option.value ? 'is-active' : ''}
|
||||
key={option.value}
|
||||
onClick={() => setRange(option.value)}
|
||||
type="button"
|
||||
>{option.label}</button>
|
||||
))}
|
||||
</div>
|
||||
<Button disabled={loading} icon={<RefreshCw className={loading ? 'is-spinning' : ''} size={16} />} onClick={() => void loadData()} variant="ghost">
|
||||
{loading ? '刷新中' : '刷新'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="system-monitoring-unavailable" role="alert">
|
||||
<ShieldAlert size={20} />
|
||||
<div><strong>监控数据不可用</strong><span>{error}。页面不会展示历史缓存值。</span></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="system-monitoring-health surface">
|
||||
<div className="system-monitoring-health__copy">
|
||||
<div className={`system-monitoring-health__mark is-${overview?.summary.overallStatus ?? 'unknown'}`}>
|
||||
{overview?.summary.overallStatus === 'healthy' ? <CheckCircle2 size={24} /> : <AlertTriangle size={24} />}
|
||||
</div>
|
||||
<span>平台基础设施</span>
|
||||
<strong>{status.label}</strong>
|
||||
<small>最新采样 {formatTime(overview?.lastSampleAt ?? null)}</small>
|
||||
</div>
|
||||
<div className="system-monitoring-health__fact"><span>核心服务</span><strong>{serviceHealthy}/{serviceTotal}</strong><small>正常运行</small></div>
|
||||
<div className="system-monitoring-health__fact"><span>活动告警</span><strong>{overview?.summary.activeAlerts ?? 0}</strong><small>{overview?.summary.criticalAlerts ?? 0} 严重 · {overview?.summary.warningAlerts ?? 0} 警告</small></div>
|
||||
<div className="system-monitoring-health__fact"><span>系统负载</span><strong>{metrics?.load1 === null || metrics?.load1 === undefined ? '—' : metrics.load1.toFixed(2)}</strong><small>最近1分钟</small></div>
|
||||
<div className="system-monitoring-health__fact"><span>持续运行</span><strong>{formatUptime(metrics?.uptimeSeconds ?? null)}</strong><small>主机启动后</small></div>
|
||||
</div>
|
||||
|
||||
<div className="system-monitoring-metrics">
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-blue"><Cpu size={19} /></div><div><span>CPU 使用率</span><strong>{formatPercent(metrics?.cpuUsagePercent ?? null)}</strong><small>5分钟平均</small></div></article>
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-violet"><MemoryStick size={19} /></div><div><span>内存使用率</span><strong>{formatPercent(metrics?.memoryUsagePercent ?? null)}</strong><small>{formatBytes(metrics?.memoryAvailableBytes ?? null)} 可用 / {formatBytes(metrics?.memoryTotalBytes ?? null)}</small></div></article>
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-amber"><HardDrive size={19} /></div><div><span>根磁盘使用率</span><strong>{formatPercent(metrics?.diskUsagePercent ?? null)}</strong><small>{formatBytes(metrics?.diskAvailableBytes ?? null)} 可用 / {formatBytes(metrics?.diskTotalBytes ?? null)}</small></div></article>
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-green"><Network size={19} /></div><div><span>网络吞吐</span><strong>{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}</strong><small>接收 {formatRate(metrics?.networkReceiveBytesPerSecond ?? null)} · 发送 {formatRate(metrics?.networkTransmitBytesPerSecond ?? null)}</small></div></article>
|
||||
</div>
|
||||
|
||||
<div className="system-monitoring-main-grid">
|
||||
<div className="system-monitoring-chart-stack">
|
||||
<article className="surface system-monitoring-chart-card"><header><div><Cpu size={17} /><strong>CPU 趋势</strong></div><span>{formatPercent(metrics?.cpuUsagePercent ?? null)}</span></header>{overview?.trends.cpuUsagePercent.length ? <Chart height={230} option={cpuOption} /> : <EmptyChart />}</article>
|
||||
<article className="surface system-monitoring-chart-card"><header><div><MemoryStick size={17} /><strong>内存趋势</strong></div><span>{formatPercent(metrics?.memoryUsagePercent ?? null)}</span></header>{overview?.trends.memoryUsagePercent.length ? <Chart height={230} option={memoryOption} /> : <EmptyChart />}</article>
|
||||
<article className="surface system-monitoring-chart-card"><header><div><HardDrive size={17} /><strong>磁盘趋势</strong></div><span>{formatPercent(metrics?.diskUsagePercent ?? null)}</span></header>{overview?.trends.diskUsagePercent.length ? <Chart height={230} option={diskOption} /> : <EmptyChart />}</article>
|
||||
<article className="surface system-monitoring-chart-card"><header><div><Activity size={17} /><strong>网络趋势</strong></div><span>{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}</span></header>{overview?.trends.networkReceiveBytesPerSecond.length ? <Chart height={230} option={networkOption} /> : <EmptyChart />}</article>
|
||||
</div>
|
||||
|
||||
<aside className="surface system-monitoring-services">
|
||||
<header><div><Server size={18} /><strong>核心服务</strong></div><Tag tone={serviceHealthy === serviceTotal && overview?.available ? 'success' : 'neutral'}>{serviceHealthy}/{serviceTotal} 正常</Tag></header>
|
||||
<div className="system-monitoring-service-list">
|
||||
{(overview?.services ?? []).map((service) => (
|
||||
<div className="system-monitoring-service" key={service.key}>
|
||||
<span className={`system-monitoring-service__dot is-${service.status}`} />
|
||||
<div><strong>{service.name}</strong><small>{service.unit}</small></div>
|
||||
<span>{service.status === 'healthy' ? '正常' : service.status === 'unhealthy' ? '异常' : '未知'}</span>
|
||||
</div>
|
||||
))}
|
||||
{!overview?.services.length ? [
|
||||
['api', 'API服务'], ['gateway', 'Gateway服务'], ['postgresql', 'PostgreSQL'], ['redis', 'Redis'], ['minio', 'MinIO'], ['nginx', 'Nginx'],
|
||||
].map(([key, name]) => <div className="system-monitoring-service" key={key}><span className="system-monitoring-service__dot is-unknown" /><div><strong>{name}</strong><small>等待真实采集</small></div><span>未知</span></div>) : null}
|
||||
</div>
|
||||
<div className="system-monitoring-collector-note"><Database size={16} /><span>指标由 Prometheus 采集,业务数据库不写入高频时序数据。</span></div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<section className="surface system-monitoring-alerts">
|
||||
<header><div><AlertTriangle size={18} /><strong>活动告警</strong><Tag tone={overview?.summary.activeAlerts ? 'warning' : 'success'}>{overview?.summary.activeAlerts ?? 0}</Tag></div><span><Clock3 size={14} /> 刷新于 {formatTime(overview?.collectedAt ?? null)}</span></header>
|
||||
<Table columns={alertColumns} data={overview?.alerts ?? []} emptyText={overview?.available ? '当前没有活动告警' : '监控不可用,无法读取活动告警'} pagination={false} rowKey="fingerprint" />
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyChart() {
|
||||
return <div className="system-monitoring-chart-empty"><Activity size={22} /><span>暂无真实趋势指标</span></div>;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
ReceiptText,
|
||||
ClipboardList,
|
||||
Send,
|
||||
ServerCog,
|
||||
ScanSearch,
|
||||
Settings,
|
||||
Shield,
|
||||
@@ -202,6 +203,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
{ label: '报备字段库', to: '/admin/drainage-fields', icon: Hash },
|
||||
{ label: '引流识别规则', to: '/admin/drainage-detection-rules', icon: ScanSearch },
|
||||
{ label: '系统日志', to: '/admin/system-logs', icon: FileText },
|
||||
{ label: '系统监控', to: '/admin/system-monitoring', icon: ServerCog },
|
||||
],
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -38,6 +38,7 @@ import { AdminSignatureAuditPage } from '@/apps/admin/AdminSignatureAuditPage';
|
||||
import { AdminSignatureRetirementPage } from '@/apps/admin/AdminSignatureRetirementPage';
|
||||
import { AdminDrainageAuditPage } from '@/apps/admin/AdminDrainageAuditPage';
|
||||
import { AdminSystemLogsPage } from '@/apps/admin/AdminSystemLogsPage';
|
||||
import { AdminSystemMonitoringPage } from '@/apps/admin/system-monitoring/AdminSystemMonitoringPage';
|
||||
import { AdminTemplateAuditPage } from '@/apps/admin/AdminTemplateAuditPage';
|
||||
import { AdminUsersPage } from '@/apps/admin/AdminUsersPage';
|
||||
import { AdminEnterpriseAuditPage } from '@/apps/admin/AdminEnterpriseAuditPage';
|
||||
@@ -142,6 +143,7 @@ export function AppRoutes() {
|
||||
<Route path="drainage-fields" element={<AdminDrainageFieldsPage />} />
|
||||
<Route path="drainage-detection-rules" element={<AdminDrainageDetectionRulesPage />} />
|
||||
<Route path="system-logs" element={<AdminSystemLogsPage />} />
|
||||
<Route path="system-monitoring" element={<AdminSystemMonitoringPage />} />
|
||||
<Route path="*" element={<PagePlaceholder />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
Reference in New Issue
Block a user