mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-02 17:06:36 +08:00
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:
@@ -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}`;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user