feat(monitoring): add node/tunnel metrics, service monitors, and health checks

- Add NodeMetric/TunnelMetric/ServiceMonitor models and repository methods
- Implement metrics ingestion service with per-minute bucket aggregation
- Add health checker for node connectivity monitoring
- Wire node metrics from WebSocket SystemInfo messages
- Add tunnel metrics ingestion from flow upload endpoint
- Create monitoring REST API endpoints for nodes, tunnels, services
- Implement service monitor CRUD and execution (TCP/ICMP checks)
- Add MonitorPermission for non-admin access control
- Create frontend monitor page with node/tunnel/service views
- Add tunnel metrics ingestion from agent flow reports
- Include schema migration for tunnel_metric unique index
- Fix tunnel entry port conflict validation to use transaction

Entire-Checkpoint: 030821a7c8e3
This commit is contained in:
sagitchu
2026-03-17 14:59:09 +08:00
parent 2e05df288b
commit 9de240f034
45 changed files with 8704 additions and 190 deletions
+5 -2
View File
@@ -73,9 +73,12 @@ const normalizeBatchFailure = (
};
};
const normalizeBatchFailureReason = (failure: BatchOperationFailure): string => {
const normalizeBatchFailureReason = (
failure: BatchOperationFailure,
): string => {
const name = typeof failure.name === "string" ? failure.name.trim() : "";
const reason = typeof failure.reason === "string" ? failure.reason.trim() : "";
const reason =
typeof failure.reason === "string" ? failure.reason.trim() : "";
if (name && reason) {
return `${name}: ${reason}`;
+92
View File
@@ -26,6 +26,16 @@ import type {
SpeedLimitMutationPayload,
UpdatePasswordPayload,
BackupImportPayload,
NodeMetricApiItem,
TunnelMetricApiItem,
ServiceMonitorApiItem,
ServiceMonitorResultApiItem,
ServiceMonitorLimitsApiData,
ServiceMonitorMutationPayload,
MonitorNodeApiItem,
MonitorTunnelApiItem,
MonitorPermissionApiItem,
MonitorAccessApiData,
} from "./types";
import axios from "axios";
@@ -372,3 +382,85 @@ export const getAnnouncement = () =>
Network.get<AnnouncementData>("/announcement/get");
export const updateAnnouncement = (data: AnnouncementData) =>
Network.post("/announcement/update", data);
export const getNodeMetrics = (
nodeId: number,
start?: number,
end?: number,
) => {
const params: Record<string, string> = {};
if (start) params.start = String(start);
if (end) params.end = String(end);
return Network.get<NodeMetricApiItem[]>(
`/monitor/nodes/${nodeId}/metrics`,
params,
);
};
export const getNodeMetricsLatest = (nodeId: number) =>
Network.get<NodeMetricApiItem>(`/monitor/nodes/${nodeId}/metrics/latest`);
export const getTunnelMetrics = (
tunnelId: number,
start?: number,
end?: number,
) => {
const params: Record<string, string> = {};
if (start) params.start = String(start);
if (end) params.end = String(end);
return Network.get<TunnelMetricApiItem[]>(
`/monitor/tunnels/${tunnelId}/metrics`,
params,
);
};
export const getMonitorTunnels = () =>
Network.get<MonitorTunnelApiItem[]>("/monitor/tunnels");
export const getServiceMonitorList = () =>
Network.get<ServiceMonitorApiItem[]>("/monitor/services");
export const getServiceMonitorLimits = () =>
Network.get<ServiceMonitorLimitsApiData>("/monitor/services/limits");
export const createServiceMonitor = (data: ServiceMonitorMutationPayload) =>
Network.post<ServiceMonitorApiItem>("/monitor/services/create", data);
export const updateServiceMonitor = (data: ServiceMonitorMutationPayload) =>
Network.post<ServiceMonitorApiItem>("/monitor/services/update", data);
export const deleteServiceMonitor = (id: number) =>
Network.post("/monitor/services/delete", { id });
export const getServiceMonitorResults = (monitorId: number, limit = 100) =>
Network.get<ServiceMonitorResultApiItem[]>(
`/monitor/services/${monitorId}/results`,
{ limit: String(limit) },
);
export const getServiceMonitorLatestResults = () =>
Network.get<ServiceMonitorResultApiItem[]>(
"/monitor/services/latest-results",
);
export const runServiceMonitor = (id: number) =>
Network.post<ServiceMonitorResultApiItem>("/monitor/services/run", { id });
export const getMonitorNodes = () =>
Network.get<MonitorNodeApiItem[]>("/monitor/nodes");
export const getMonitorAccess = () =>
Network.get<MonitorAccessApiData>("/monitor/access");
export const getMonitorPermissionList = () =>
Network.get<MonitorPermissionApiItem[]>("/monitor/permission/list");
export const assignMonitorPermission = (userId: number) =>
Network.post("/monitor/permission/assign", { userId });
export const removeMonitorPermission = (userId: number) =>
Network.post("/monitor/permission/remove", { userId });
+101
View File
@@ -340,3 +340,104 @@ export interface BackupImportPayload {
types: string[];
[key: string]: unknown;
}
export interface NodeMetricApiItem {
id: number;
nodeId: number;
timestamp: number;
cpuUsage: number;
memoryUsage: number;
diskUsage: number;
netInBytes: number;
netOutBytes: number;
netInSpeed: number;
netOutSpeed: number;
load1: number;
load5: number;
load15: number;
tcpConns: number;
udpConns: number;
}
export interface TunnelMetricApiItem {
id: number;
tunnelId: number;
nodeId: number;
timestamp: number;
bytesIn: number;
bytesOut: number;
connections: number;
errors: number;
avgLatencyMs: number;
}
export interface ServiceMonitorApiItem {
id: number;
name: string;
// Keep as string for forward-compatibility.
type: string;
target: string;
intervalSec: number;
timeoutSec: number;
nodeId: number;
enabled: number;
createdTime: number;
updatedTime: number;
}
export interface ServiceMonitorResultApiItem {
id: number;
monitorId: number;
timestamp: number;
success: number;
latencyMs: number;
statusCode: number;
errorMessage: string;
}
export interface ServiceMonitorMutationPayload {
id?: number;
name: string;
type: "tcp" | "icmp";
target: string;
intervalSec?: number;
timeoutSec?: number;
nodeId?: number;
enabled?: number;
}
export interface ServiceMonitorLimitsApiData {
checkerScanIntervalSec: number;
minIntervalSec: number;
defaultIntervalSec: number;
minTimeoutSec: number;
defaultTimeoutSec: number;
maxTimeoutSec: number;
}
export interface MonitorNodeApiItem {
id: number;
inx: number;
name: string;
status: number;
updatedTime: number;
}
export interface MonitorTunnelApiItem {
id: number;
inx: number;
name: string;
status: number;
updatedTime: number;
}
export interface MonitorPermissionApiItem {
id: number;
userId: number;
createdTime: number;
}
export interface MonitorAccessApiData {
allowed: boolean;
reason?: string;
}