优化数据指标的存储和查询

This commit is contained in:
dushixiang committed 2025-11-30 20:37:41 +08:00
1 parent b137cc9864
commit 16dc0fab99
9 files changed
+171 -51

No files matched your search

+3
View File
@@ -195,6 +195,9 @@ func setupApi(app *orz.App, components *AppComponents) {
// Logo(公开访问)- 用于公共页面只获取 Logo
publicApiWithOptionalAuth.GET("/logo", components.PropertyHandler.GetLogo)
// 指标配置(公开访问)- 用于获取时间范围选项等配置
publicApiWithOptionalAuth.GET("/metrics-config", components.PropertyHandler.GetMetricsConfig)
}
// WebSocket 路由(探针连接)
+6
View File
@@ -150,6 +150,12 @@ func (h *PropertyHandler) GetLogo(c echo.Context) error {
return c.Blob(http.StatusOK, contentType, imageData)
}
// GetMetricsConfig 获取指标配置(公开访问)
func (h *PropertyHandler) GetMetricsConfig(c echo.Context) error {
config := h.service.GetMetricsConfig(c.Request().Context())
return c.JSON(http.StatusOK, config)
}
// TestNotificationChannel 测试通知渠道(从数据库读取配置)
func (h *PropertyHandler) TestNotificationChannel(c echo.Context) error {
channelType := c.Param("type")
+9 -2
View File
@@ -49,8 +49,15 @@ type SystemConfig struct {
DefaultView string `json:"defaultView"` // 默认视图 grid | list
}
// TimeRangeOption 时间范围选项
type TimeRangeOption struct {
Label string `json:"label"` // 显示标签,如 "15分钟"
Value string `json:"value"` // 值,如 "15m"
}
// MetricsConfig 指标数据配置
type MetricsConfig struct {
RetentionHours int `json:"retentionHours"` // 原始数据保留小时数(默认168小时=7天)
MaxQueryPoints int `json:"maxQueryPoints"` // 查询返回的最大数据点数(默认720)
RetentionHours int `json:"retentionHours"` // 原始数据保留小时数(默认168小时=7天)
MaxQueryPoints int `json:"maxQueryPoints"` // 查询返回的最大数据点数(默认300)
TimeRangeOptions []TimeRangeOption `json:"timeRangeOptions"` // 前端时间范围选项
}
+49 -14
View File
@@ -15,7 +15,7 @@ import (
const (
defaultMetricsRetentionHours = 24 * 7 // 默认保留 7 天
defaultMaxQueryPoints = 720 // 默认最多返回 720 个点
defaultMaxQueryPoints = 300 // 默认最多返回 300 个点(优化前端渲染性能)
)
var allowedIntervals = []int{
@@ -26,6 +26,17 @@ var allowedIntervals = []int{
3600, 7200, 14400,
}
// 聚合任务支持的 bucket 列表(升序)
// 与前端 timeRangeOptions 对齐,优化查询效率
var aggregationBuckets = []int{
60, // 1 分钟 - 适用于 15分钟、30分钟时间范围
300, // 5 分钟 - 适用于 1小时、3小时时间范围
900, // 15 分钟 - 适用于 6小时、12小时时间范围
1800, // 30 分钟 - 适用于 1天时间范围
3600, // 1 小时 - 适用于 3天时间范围
7200, // 2 小时 - 适用于 7天时间范围
}
// MetricService 指标服务
type MetricService struct {
logger *zap.Logger
@@ -390,18 +401,17 @@ func (s *MetricService) GetMetrics(ctx context.Context, agentID, metricType stri
var bucketSeconds int
useAgg := false
if aggCapable[metricType] {
if interval >= 3600 {
bucketSeconds = 3600
useAgg = true
} else if interval >= 300 {
bucketSeconds = 300
useAgg = true
} else if interval >= 60 {
bucketSeconds = 60
useAgg = true
}
bucketSeconds = chooseAggregationBucket(interval)
useAgg = bucketSeconds > 0
}
// 将时间范围对齐到最终使用的 bucket,避免不同时间框架出现桶数量偏差
bucketMs := int64(interval * 1000)
if useAgg {
bucketMs = int64(bucketSeconds * 1000)
}
start, end = alignTimeRangeToBucket(start, end, bucketMs)
switch metricType {
case "cpu":
if useAgg {
@@ -563,6 +573,33 @@ func maxInt(a, b int) int {
return b
}
// alignTimeRangeToBucket 将时间范围对齐到桶边界,确保不同时间框架的桶数一致
func alignTimeRangeToBucket(start, end int64, bucketMs int64) (int64, int64) {
if bucketMs <= 0 {
return start, end
}
alignedStart := (start / bucketMs) * bucketMs
endBucket := ((end - 1) / bucketMs) * bucketMs
alignedEnd := endBucket + bucketMs - 1
if alignedEnd < alignedStart {
alignedEnd = alignedStart
}
return alignedStart, alignedEnd
}
// chooseAggregationBucket 根据请求的 interval 选择最合适的聚合 bucket(取不小于 interval 的最小桶)
func chooseAggregationBucket(interval int) int {
if interval <= 0 {
return 0
}
for _, bucket := range aggregationBuckets {
if interval <= bucket {
return bucket
}
}
return aggregationBuckets[len(aggregationBuckets)-1]
}
// getMetricsConfig 获取指标配置
func (s *MetricService) getMetricsConfig(ctx context.Context) models.MetricsConfig {
cfg := models.MetricsConfig{
@@ -606,10 +643,8 @@ func (s *MetricService) StartAggregationTask(ctx context.Context) {
func (s *MetricService) runAggregation(ctx context.Context) {
cfg := s.getMetricsConfig(ctx)
retention := time.Duration(cfg.RetentionHours) * time.Hour
// 标准 bucket:1m、5m、1h
buckets := []int{60, 300, 3600}
for _, bucket := range buckets {
for _, bucket := range aggregationBuckets {
s.aggregateMetric(ctx, "cpu", bucket, retention, s.metricRepo.AggregateCPUToAgg)
s.aggregateMetric(ctx, "memory", bucket, retention, s.metricRepo.AggregateMemoryToAgg)
s.aggregateMetric(ctx, "disk", bucket, retention, s.metricRepo.AggregateDiskToAgg)
+12 -1
View File
@@ -173,7 +173,18 @@ func (s *PropertyService) InitializeDefaultConfigs(ctx context.Context) error {
Name: "指标数据配置",
Value: models.MetricsConfig{
RetentionHours: 168, // 默认7天
MaxQueryPoints: 720, // 默认720个点
MaxQueryPoints: 300, // 默认300个点(优化前端渲染性能)
TimeRangeOptions: []models.TimeRangeOption{
{Label: "15分钟", Value: "15m"},
{Label: "30分钟", Value: "30m"},
{Label: "1小时", Value: "1h"},
{Label: "3小时", Value: "3h"},
{Label: "6小时", Value: "6h"},
{Label: "12小时", Value: "12h"},
{Label: "1天", Value: "1d"},
{Label: "3天", Value: "3d"},
{Label: "7天", Value: "7d"},
},
},
},
}
+1 -1
View File
@@ -9,7 +9,7 @@ export interface ListAgentsResponse {
export interface GetAgentMetricsRequest {
agentId: string;
type: 'cpu' | 'memory' | 'disk' | 'network' | 'network_connection' | 'disk_io' | 'gpu' | 'temperature';
range?: '1m' | '5m' | '15m' | '30m' | '1h' | '3h' | '6h' | '12h' | '1d' | '24h' | '3d' | '7d' | '30d';
range?: string; // 时间范围,如 '15m', '1h', '1d' 等,从后端配置获取
interface?: string; // 网卡过滤参数(仅对 network 类型有效)
}
+16 -4
View File
@@ -27,16 +27,28 @@ export const saveProperty = async <T>(propertyId: string, name: string, value: T
const PROPERTY_ID_METRICS_CONFIG = 'metrics_config';
export interface MetricsConfig {
retentionHours: number; // 数据保留时长(小时)
maxQueryPoints: number; // 最大查询点数
export interface TimeRangeOption {
label: string; // 显示标签,如 "15分钟"
value: string; // 值,如 "15m"
}
// 获取指标配置
export interface MetricsConfig {
retentionHours: number; // 数据保留时长(小时)
maxQueryPoints: number; // 最大查询点数
timeRangeOptions: TimeRangeOption[]; // 时间范围选项
}
// 获取指标配置(管理后台使用,需认证)
export const getMetricsConfig = async (): Promise<MetricsConfig> => {
return getProperty<MetricsConfig>(PROPERTY_ID_METRICS_CONFIG);
};
// 获取指标配置(公开访问,用于前端获取时间范围选项等)
export const getMetricsConfigPublic = async (): Promise<MetricsConfig> => {
const response = await get<MetricsConfig>('/metrics-config');
return response.data;
};
// 保存指标配置
export const saveMetricsConfig = async (config: MetricsConfig): Promise<void> => {
return saveProperty(PROPERTY_ID_METRICS_CONFIG, '指标配置', config);
+28 -9
View File
@@ -16,6 +16,7 @@ import {
import type {TooltipProps} from 'recharts';
import {Area, AreaChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis,} from 'recharts';
import {type AggregatedMonitorMetric, getMonitorHistory, getMonitorStatsById} from '@/api/monitor.ts';
import { getMetricsConfigPublic, type TimeRangeOption } from '@/api/property.ts';
import type {MonitorStats} from '@/types';
const formatTime = (ms: number): string => {
@@ -300,19 +301,37 @@ const AGENT_COLORS = [
'#14b8a6', // teal
];
const timeRangeOptions = [
{label: '15分钟', value: '15m'},
{label: '30分钟', value: '30m'},
{label: '1小时', value: '1h'},
] as const;
type TimeRange = typeof timeRangeOptions[number]['value'];
const MonitorDetail = () => {
const navigate = useNavigate();
const {id} = useParams<{ id: string }>();
const [selectedAgent, setSelectedAgent] = useState<string>('all');
const [timeRange, setTimeRange] = useState<TimeRange>('15m');
const [timeRange, setTimeRange] = useState<string>('15m');
const [timeRangeOptions, setTimeRangeOptions] = useState<TimeRangeOption[]>([]);
// 从后端获取时间范围选项
useEffect(() => {
getMetricsConfigPublic()
.then(config => {
if (config.timeRangeOptions && config.timeRangeOptions.length > 0) {
setTimeRangeOptions(config.timeRangeOptions);
} else {
// 如果后端没有配置,使用默认值
setTimeRangeOptions([
{label: '15分钟', value: '15m'},
{label: '30分钟', value: '30m'},
{label: '1小时', value: '1h'},
]);
}
})
.catch(() => {
// 如果请求失败,使用默认值
setTimeRangeOptions([
{label: '15分钟', value: '15m'},
{label: '30分钟', value: '30m'},
{label: '1小时', value: '1h'},
]);
});
}, []);
const {data: monitorStats = [], isLoading} = useQuery<MonitorStats[]>({
queryKey: ['monitorStats', id],
+47 -20
View File
@@ -32,6 +32,7 @@ import {
getAvailableNetworkInterfaces,
type GetAgentMetricsRequest,
} from '@/api/agent.ts';
import { getMetricsConfigPublic, type TimeRangeOption } from '@/api/property.ts';
import type {
Agent,
AggregatedCPUMetric,
@@ -87,20 +88,6 @@ const formatDateTime = (value: string | number | undefined | null): string => {
return date.toLocaleString('zh-CN');
};
const timeRangeOptions = [
{label: '15分钟', value: '15m'},
{label: '30分钟', value: '30m'},
{label: '1小时', value: '1h'},
{label: '3小时', value: '3h'},
{label: '6小时', value: '6h'},
{label: '12小时', value: '12h'},
{label: '1天', value: '1d'},
{label: '3天', value: '3d'},
{label: '7天', value: '7d'},
] as const;
type TimeRange = typeof timeRangeOptions[number]['value'];
// 网卡颜色配置(上行和下行使用不同的色调)
const INTERFACE_COLORS = [
{upload: '#6FD598', download: '#2C70F6'}, // 绿/蓝
@@ -221,12 +208,14 @@ const InfoGrid = ({items}: { items: Array<{ label: string; value: ReactNode }> }
const TimeRangeSelector = ({
value,
onChange,
options,
}: {
value: TimeRange;
onChange: (value: TimeRange) => void;
value: string;
onChange: (value: string) => void;
options: TimeRangeOption[];
}) => (
<div className="flex flex-wrap items-center gap-2">
{timeRangeOptions.map((option) => {
{options.map((option) => {
const isActive = option.value === value;
return (
<button
@@ -346,7 +335,7 @@ const useAgentOverview = (agentId?: string) => {
return {agent, latestMetrics, loading};
};
const useAggregatedMetrics = (agentId: string | undefined, range: TimeRange) => {
const useAggregatedMetrics = (agentId: string | undefined, range: string) => {
const [metrics, setMetrics] = useState<MetricsState>(() => createEmptyMetricsState());
useEffect(() => {
@@ -488,11 +477,49 @@ const ServerDetail = () => {
const {id} = useParams<{ id: string }>();
const navigate = useNavigate();
const [timeRange, setTimeRange] = useState<TimeRange>('15m');
const [timeRange, setTimeRange] = useState<string>('15m');
const [timeRangeOptions, setTimeRangeOptions] = useState<TimeRangeOption[]>([]);
const [selectedInterface, setSelectedInterface] = useState<string>('all');
const {agent, latestMetrics, loading} = useAgentOverview(id);
const metricsData = useAggregatedMetrics(id, timeRange);
// 从后端获取时间范围选项
useEffect(() => {
getMetricsConfigPublic()
.then(config => {
if (config.timeRangeOptions && config.timeRangeOptions.length > 0) {
setTimeRangeOptions(config.timeRangeOptions);
} else {
// 如果后端没有配置,使用默认值
setTimeRangeOptions([
{label: '15分钟', value: '15m'},
{label: '30分钟', value: '30m'},
{label: '1小时', value: '1h'},
{label: '3小时', value: '3h'},
{label: '6小时', value: '6h'},
{label: '12小时', value: '12h'},
{label: '1天', value: '1d'},
{label: '3天', value: '3d'},
{label: '7天', value: '7d'},
]);
}
})
.catch(() => {
// 如果请求失败,使用默认值
setTimeRangeOptions([
{label: '15分钟', value: '15m'},
{label: '30分钟', value: '30m'},
{label: '1小时', value: '1h'},
{label: '3小时', value: '3h'},
{label: '6小时', value: '6h'},
{label: '12小时', value: '12h'},
{label: '1天', value: '1d'},
{label: '3天', value: '3d'},
{label: '7天', value: '7d'},
]);
});
}, []);
const cpuChartData = useMemo(
() =>
metricsData.cpu.map((item) => ({
@@ -959,7 +986,7 @@ const ServerDetail = () => {
<Card
title="历史趋势"
description="针对选定时间范围展示 CPU、内存与网络的变化趋势"
action={<TimeRangeSelector value={timeRange} onChange={setTimeRange}/>}
action={<TimeRangeSelector value={timeRange} onChange={setTimeRange} options={timeRangeOptions}/>}
>
<div className="grid gap-6 md:grid-cols-2">
<section>