优化数据指标的存储和查询
This commit is contained in:
1 parent
b137cc9864
commit
16dc0fab99
9 files changed
+171
-51
No files matched your search
@@ -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 路由(探针连接)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"` // 前端时间范围选项
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user