优化代码
This commit is contained in:
1 parent
53c771d965
commit
992aa29043
11 files changed
+204
-76
No files matched your search
@@ -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, "指标类型不能为空")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 类型有效)
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ const CompactResourceBar = ({ value, label, subtext, icon: Icon, color = "bg-cya
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center w-full h-5 gap-3 text-xs font-mono group/bar">
|
||||
<div className="flex items-center w-full h-5 gap-2 text-xs font-mono group/bar">
|
||||
{/* Icon & Label */}
|
||||
<div className={`flex items-center gap-2 w-14 flex-shrink-0 ${iconClass}`}>
|
||||
<div className={`flex items-center gap-2 w-10 flex-shrink-0 ${iconClass}`}>
|
||||
<Icon className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
<span className="text-[10px] font-bold tracking-wider opacity-80">{label}</span>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@ const CompactResourceBar = ({ value, label, subtext, icon: Icon, color = "bg-cya
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`font-medium text-xs ${isCritical ? 'text-rose-400' : 'text-slate-200'}`}>{value.toFixed(1)}%</div>
|
||||
<div className={`w-10 font-medium text-xs ${isCritical ? 'text-rose-400' : 'text-slate-200'}`}>{value.toFixed(1)}%</div>
|
||||
|
||||
{/* Value & Subtext */}
|
||||
<div className="w-28 text-right flex items-baseline justify-end gap-2 flex-shrink-0">
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-4">
|
||||
<CyberCard className="p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-xs font-bold font-mono uppercase tracking-widest text-cyan-500">资源快照</h3>
|
||||
</div>
|
||||
<SnapshotGrid cards={cards}/>
|
||||
</div>
|
||||
</CyberCard>
|
||||
);
|
||||
};
|
||||
@@ -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<number, any>();
|
||||
|
||||
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 (
|
||||
<ChartContainer title="监控响应时间" icon={Activity}>
|
||||
<ChartPlaceholder variant="dark"/>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartContainer title="监控响应时间" icon={Activity}>
|
||||
{chartData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
{monitorKeys.map((key, index) => (
|
||||
<linearGradient key={key} id={`monitorAreaGradient-${index}`} x1="0" y1="0" x2="0"
|
||||
y2="1">
|
||||
<stop offset="5%" stopColor={colors[index % colors.length]} stopOpacity={0.4}/>
|
||||
<stop offset="95%" stopColor={colors[index % colors.length]} stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid stroke="currentColor" strokeDasharray="4 4" className="stroke-cyan-900/30"/>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="currentColor"
|
||||
className="stroke-cyan-600"
|
||||
style={{fontSize: '12px'}}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="currentColor"
|
||||
className="stroke-cyan-600"
|
||||
style={{fontSize: '12px'}}
|
||||
tickFormatter={(value) => `${value}ms`}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip unit="ms" variant="dark"/>}/>
|
||||
{monitorKeys.length > 1 && (
|
||||
<Legend
|
||||
wrapperStyle={{fontSize: '12px'}}
|
||||
iconType="line"
|
||||
/>
|
||||
)}
|
||||
{monitorKeys.map((key, index) => (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
name={key}
|
||||
stroke={colors[index % colors.length]}
|
||||
strokeWidth={2}
|
||||
fill={`url(#monitorAreaGradient-${index})`}
|
||||
activeDot={{r: 3}}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<ChartPlaceholder variant="dark"/>
|
||||
)}
|
||||
</ChartContainer>
|
||||
);
|
||||
};
|
||||
@@ -6,3 +6,4 @@ export {DiskIOChart} from './DiskIOChart';
|
||||
export {NetworkConnectionChart} from './NetworkConnectionChart';
|
||||
export {GpuChart} from './GpuChart';
|
||||
export {TemperatureChart} from './TemperatureChart';
|
||||
export {MonitorChart} from './MonitorChart';
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
DiskIOChart,
|
||||
GpuChart,
|
||||
MemoryChart,
|
||||
MonitorChart,
|
||||
NetworkChart,
|
||||
NetworkConnectionChart,
|
||||
TemperatureChart,
|
||||
@@ -96,6 +97,11 @@ const ServerDetail = () => {
|
||||
<GpuChart agentId={id!} timeRange={timeRange}/>
|
||||
<TemperatureChart agentId={id!} timeRange={timeRange}/>
|
||||
</div>
|
||||
|
||||
{/* 监控指标:单列全宽 */}
|
||||
<div className="grid gap-4 sm:gap-5 lg:gap-6 grid-cols-1">
|
||||
<MonitorChart agentId={id!} timeRange={timeRange}/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -364,10 +364,12 @@ const ServerList = () => {
|
||||
</div>
|
||||
<div
|
||||
className={`text-[10px] font-mono flex items-center gap-1 ${server.expireTime && server.expireTime > 0 ? 'text-cyan-600' : 'text-emerald-500/60'}`}>
|
||||
<Calendar className="w-3 h-3"/>
|
||||
{server.expireTime > 0
|
||||
? `EXP: ${new Date(server.expireTime).toLocaleDateString('zh-CN')}`
|
||||
: ''
|
||||
|
||||
{server.expireTime > 0 &&
|
||||
<div className={'flex items-center gap-1'}>
|
||||
<Calendar className="w-3 h-3"/>
|
||||
<div>过期时间: {new Date(server.expireTime).toLocaleDateString('zh-CN')}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user