From 992aa29043b56bea8f9eb7fba249eada7c989202 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Sun, 14 Dec 2025 22:44:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/agent_handler.go | 2 +- internal/service/metric_service.go | 79 +++++++---- internal/service/monitor_service.go | 7 - internal/vmclient/client.go | 31 ----- web/src/api/agent.ts | 2 +- web/src/components/CompactResourceBar.tsx | 6 +- web/src/components/server/SnapshotSection.tsx | 5 +- .../components/server/charts/MonitorChart.tsx | 131 ++++++++++++++++++ web/src/components/server/charts/index.ts | 1 + web/src/pages/Public/ServerDetail.tsx | 6 + web/src/pages/Public/ServerList.tsx | 10 +- 11 files changed, 204 insertions(+), 76 deletions(-) create mode 100644 web/src/components/server/charts/MonitorChart.tsx diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index 333df71..c92ae51 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -408,7 +408,7 @@ func (h *AgentHandler) GetMetrics(c echo.Context) error { // 验证指标类型 validTypes := map[string]bool{ "cpu": true, "memory": true, "disk": true, "network": true, "network_connection": true, - "disk_io": true, "gpu": true, "temperature": true, + "disk_io": true, "gpu": true, "temperature": true, "monitor": true, } if metricType == "" { return orz.NewError(400, "指标类型不能为空") diff --git a/internal/service/metric_service.go b/internal/service/metric_service.go index 3ec6216..4cb379b 100644 --- a/internal/service/metric_service.go +++ b/internal/service/metric_service.go @@ -23,6 +23,7 @@ type MetricService struct { logger *zap.Logger metricRepo *repo.MetricRepo agentRepo *repo.AgentRepo + monitorRepo *repo.MonitorRepo propertyService *PropertyService trafficService *TrafficService // 流量统计服务 vmClient *vmclient.VMClient @@ -38,6 +39,7 @@ func NewMetricService(logger *zap.Logger, db *gorm.DB, propertyService *Property logger: logger, metricRepo: repo.NewMetricRepo(db), agentRepo: repo.NewAgentRepo(db), + monitorRepo: repo.NewMonitorRepo(db), propertyService: propertyService, trafficService: trafficService, vmClient: vmClient, @@ -244,6 +246,45 @@ func (s *MetricService) GetMetrics(ctx context.Context, agentID, metricType stri series = append(series, convertedSeries...) } + // 如果是监控类型,添加监控任务名称到标签中 + if metricType == "monitor" && len(series) > 0 { + // 收集所有 monitor_id + monitorIdSet := make(map[string]struct{}) + for _, s := range series { + if monitorId, ok := s.Labels["monitor_id"]; ok { + monitorIdSet[monitorId] = struct{}{} + } + } + + // 查询 monitor 信息 + if len(monitorIdSet) > 0 { + monitorIds := make([]string, 0, len(monitorIdSet)) + for monitorId := range monitorIdSet { + monitorIds = append(monitorIds, monitorId) + } + + monitors, err := s.monitorRepo.FindByIdIn(ctx, monitorIds) + if err != nil { + s.logger.Error("查询 monitor 信息失败", zap.Error(err)) + } else { + // 构建 monitorId -> monitorName 映射 + monitorNameMap := make(map[string]string) + for _, monitor := range monitors { + monitorNameMap[monitor.ID] = monitor.Name + } + + // 在每个 series 的 labels 中添加 monitor_name + for i := range series { + if monitorId, ok := series[i].Labels["monitor_id"]; ok { + if monitorName, exists := monitorNameMap[monitorId]; exists { + series[i].Labels["monitor_name"] = monitorName + } + } + } + } + } + } + return &metric.GetMetricsResponse{ AgentID: agentID, Type: metricType, @@ -289,33 +330,7 @@ func (s *MetricService) DeleteAgentMetrics(ctx context.Context, agentID string) // 继续删除 VictoriaMetrics 中的数据 } - // 2. 删除 VictoriaMetrics 中的时间序列数据 - match := []string{fmt.Sprintf(`pika_.*{agent_id="%s"}`, agentID)} - if err := s.vmClient.DeleteSeries(ctx, match); err != nil { - s.logger.Error("删除 VictoriaMetrics 中的探针数据失败", - zap.String("agentID", agentID), - zap.Error(err)) - return err - } - - s.logger.Info("成功删除探针的所有指标数据", - zap.String("agentID", agentID)) - return nil -} - -// DeleteMonitorMetrics 删除指定监控任务的所有指标数据 -func (s *MetricService) DeleteMonitorMetrics(ctx context.Context, monitorID string) error { - // 删除 VictoriaMetrics 中的监控指标数据 - match := []string{fmt.Sprintf(`pika_monitor_.*{monitor_id="%s"}`, monitorID)} - if err := s.vmClient.DeleteSeries(ctx, match); err != nil { - s.logger.Error("删除 VictoriaMetrics 中的监控数据失败", - zap.String("monitorID", monitorID), - zap.Error(err)) - return err - } - - s.logger.Info("成功删除监控任务的所有指标数据", - zap.String("monitorID", monitorID)) + // 2. 不主动删除 VictoriaMetrics 中的时间序列数据,利用过期机制自动删除数据 return nil } @@ -430,6 +445,13 @@ func (s *MetricService) buildPromQLQueries(agentID, metricType string, interface Name: "temperature", Query: fmt.Sprintf(`pika_temperature_celsius{agent_id="%s"}`, agentID), }} + + case "monitor": + // 监控:响应时间(该探针参与的所有监控任务) + queries = []metric.QueryDefinition{{ + Name: "response_time", + Query: fmt.Sprintf(`pika_monitor_response_time_ms{agent_id="%s"}`, agentID), + }} } return queries @@ -492,6 +514,9 @@ func (s *MetricService) convertQueryResultToSeries(result *vmclient.QueryResult, delete(labels, "gpu_index") } + // 移除 target 标签(避免数据泄露) + delete(labels, "target") + allSeries = append(allSeries, metric.Series{ Name: finalName, Labels: labels, diff --git a/internal/service/monitor_service.go b/internal/service/monitor_service.go index f5c1e26..e3a0f89 100644 --- a/internal/service/monitor_service.go +++ b/internal/service/monitor_service.go @@ -187,13 +187,6 @@ func (s *MonitorService) DeleteMonitor(ctx context.Context, id string) error { if err := s.MonitorRepo.DeleteById(ctx, id); err != nil { return err } - - // 删除监控指标数据(从 VictoriaMetrics) - if err := s.metricService.DeleteMonitorMetrics(ctx, id); err != nil { - s.logger.Error("删除监控指标数据失败", zap.String("monitorId", id), zap.Error(err)) - return err - } - return nil }) diff --git a/internal/vmclient/client.go b/internal/vmclient/client.go index 8109848..c3896bc 100644 --- a/internal/vmclient/client.go +++ b/internal/vmclient/client.go @@ -220,37 +220,6 @@ func (c *VMClient) Query(ctx context.Context, query string) (*QueryResult, error return &result, nil } -// DeleteSeries 删除时间序列 -func (c *VMClient) DeleteSeries(ctx context.Context, match []string) error { - reqCtx, cancel := context.WithTimeout(ctx, c.writeTimeout) - defer cancel() - - params := url.Values{} - for _, m := range match { - params.Add("match[]", m) - } - - reqURL := fmt.Sprintf("%s/api/v1/admin/tsdb/delete_series?%s", c.baseURL, params.Encode()) - - req, err := http.NewRequestWithContext(reqCtx, "POST", reqURL, nil) - if err != nil { - return fmt.Errorf("create request failed: %w", err) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("delete series failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("delete series failed with status %d: %s", resp.StatusCode, string(body)) - } - - return nil -} - // ConvertToDataPoints 将查询结果转换为数据点列表 func ConvertToDataPoints(result *QueryResult) []DataPoint { if result == nil || len(result.Data.Result) == 0 { diff --git a/web/src/api/agent.ts b/web/src/api/agent.ts index d7802ed..76fe287 100644 --- a/web/src/api/agent.ts +++ b/web/src/api/agent.ts @@ -8,7 +8,7 @@ export interface ListAgentsResponse { export interface GetAgentMetricsRequest { agentId: string; - type: 'cpu' | 'memory' | 'disk' | 'network' | 'network_connection' | 'disk_io' | 'gpu' | 'temperature'; + type: 'cpu' | 'memory' | 'disk' | 'network' | 'network_connection' | 'disk_io' | 'gpu' | 'temperature' | 'monitor'; range?: string; // 时间范围,如 '15m', '1h', '1d' 等,从后端配置获取 interface?: string; // 网卡过滤参数(仅对 network 类型有效) } diff --git a/web/src/components/CompactResourceBar.tsx b/web/src/components/CompactResourceBar.tsx index 84540c7..c25e057 100644 --- a/web/src/components/CompactResourceBar.tsx +++ b/web/src/components/CompactResourceBar.tsx @@ -25,9 +25,9 @@ const CompactResourceBar = ({ value, label, subtext, icon: Icon, color = "bg-cya } return ( -
+
{/* Icon & Label */} -
+
{label}
@@ -60,7 +60,7 @@ const CompactResourceBar = ({ value, label, subtext, icon: Icon, color = "bg-cya
-
{value.toFixed(1)}%
+
{value.toFixed(1)}%
{/* Value & Subtext */}
diff --git a/web/src/components/server/SnapshotSection.tsx b/web/src/components/server/SnapshotSection.tsx index 6e3d254..12426a2 100644 --- a/web/src/components/server/SnapshotSection.tsx +++ b/web/src/components/server/SnapshotSection.tsx @@ -1,4 +1,5 @@ import {SnapshotGrid, type SnapshotCardData} from './SnapshotGrid'; +import CyberCard from "@/components/CyberCard.tsx"; interface SnapshotSectionProps { cards: SnapshotCardData[]; @@ -9,11 +10,11 @@ export const SnapshotSection = ({cards}: SnapshotSectionProps) => { return null; } return ( -
+

资源快照

-
+ ); }; diff --git a/web/src/components/server/charts/MonitorChart.tsx b/web/src/components/server/charts/MonitorChart.tsx new file mode 100644 index 0000000..0f7964d --- /dev/null +++ b/web/src/components/server/charts/MonitorChart.tsx @@ -0,0 +1,131 @@ +import {useMemo} from 'react'; +import {Activity} from 'lucide-react'; +import {Area, AreaChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {ChartPlaceholder, CustomTooltip} from '@/components/common'; +import {useMetricsQuery} from '@/hooks/server/queries'; +import {ChartContainer} from './ChartContainer'; + +interface MonitorChartProps { + agentId: string; + timeRange: string; +} + +/** + * 监控响应时间图表组件 + */ +export const MonitorChart = ({agentId, timeRange}: MonitorChartProps) => { + // 数据查询 + const {data: metricsResponse, isLoading} = useMetricsQuery({ + agentId, + type: 'monitor', + range: timeRange, + }); + + // 数据转换 - 支持多个监控任务 + const chartData = useMemo(() => { + const series = metricsResponse?.data.series || []; + if (series.length === 0) return []; + + // 收集所有时间戳 + const timestampMap = new Map(); + + series.forEach((s) => { + s.data.forEach((point) => { + if (!timestampMap.has(point.timestamp)) { + timestampMap.set(point.timestamp, { + time: new Date(point.timestamp).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + }), + timestamp: point.timestamp, + }); + } + + const dataPoint = timestampMap.get(point.timestamp); + // 使用 monitor_name 作为 key,如果没有则使用 monitor_id + const monitorKey = s.labels?.monitor_name || s.labels?.monitor_id || s.name; + dataPoint[monitorKey] = Number(point.value.toFixed(2)); + }); + }); + + // 转换为数组并排序 + return Array.from(timestampMap.values()).sort((a, b) => a.timestamp - b.timestamp); + }, [metricsResponse]); + + // 获取所有监控任务的列表(使用名称) + const monitorKeys = useMemo(() => { + const series = metricsResponse?.data.series || []; + return series.map(s => s.labels?.monitor_name || s.labels?.monitor_id || s.name); + }, [metricsResponse]); + + // 颜色配置 + const colors = ['#2563eb', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']; + + // 如果没有数据且不是加载中,不渲染组件 + if (!isLoading && chartData.length === 0) { + return null; + } + + // 渲染 + if (isLoading) { + return ( + + + + ); + } + + return ( + + {chartData.length > 0 ? ( + + + + {monitorKeys.map((key, index) => ( + + + + + ))} + + + + `${value}ms`} + /> + }/> + {monitorKeys.length > 1 && ( + + )} + {monitorKeys.map((key, index) => ( + + ))} + + + ) : ( + + )} + + ); +}; diff --git a/web/src/components/server/charts/index.ts b/web/src/components/server/charts/index.ts index aba0a52..d3d7b5c 100644 --- a/web/src/components/server/charts/index.ts +++ b/web/src/components/server/charts/index.ts @@ -6,3 +6,4 @@ export {DiskIOChart} from './DiskIOChart'; export {NetworkConnectionChart} from './NetworkConnectionChart'; export {GpuChart} from './GpuChart'; export {TemperatureChart} from './TemperatureChart'; +export {MonitorChart} from './MonitorChart'; diff --git a/web/src/pages/Public/ServerDetail.tsx b/web/src/pages/Public/ServerDetail.tsx index 97585a2..c622d01 100644 --- a/web/src/pages/Public/ServerDetail.tsx +++ b/web/src/pages/Public/ServerDetail.tsx @@ -13,6 +13,7 @@ import { DiskIOChart, GpuChart, MemoryChart, + MonitorChart, NetworkChart, NetworkConnectionChart, TemperatureChart, @@ -96,6 +97,11 @@ const ServerDetail = () => {
+ + {/* 监控指标:单列全宽 */} +
+ +
diff --git a/web/src/pages/Public/ServerList.tsx b/web/src/pages/Public/ServerList.tsx index 30e5904..21ad8e2 100644 --- a/web/src/pages/Public/ServerList.tsx +++ b/web/src/pages/Public/ServerList.tsx @@ -364,10 +364,12 @@ const ServerList = () => {
0 ? 'text-cyan-600' : 'text-emerald-500/60'}`}> - - {server.expireTime > 0 - ? `EXP: ${new Date(server.expireTime).toLocaleDateString('zh-CN')}` - : '' + + {server.expireTime > 0 && +
+ +
过期时间: {new Date(server.expireTime).toLocaleDateString('zh-CN')}
+
}