优化服务监控统计
This commit is contained in:
1 parent
8e1bc50ee6
commit
a1e5ab191f
8 files changed
+266
-92
No files matched your search
@@ -193,6 +193,7 @@ func setupApi(app *orz.App, components *AppComponents) {
|
||||
// 监控统计数据(公开访问,支持可选认证)- 用于公共展示页面
|
||||
publicApiWithOptionalAuth.GET("/monitors", components.MonitorHandler.GetMonitors)
|
||||
publicApiWithOptionalAuth.GET("/monitors/:id/stats", components.MonitorHandler.GetStatsByID)
|
||||
publicApiWithOptionalAuth.GET("/monitors/:id/agents", components.MonitorHandler.GetAgentStatsByID)
|
||||
publicApiWithOptionalAuth.GET("/monitors/:id/history", components.MonitorHandler.GetHistoryByID)
|
||||
|
||||
// Logo(公开访问)- 用于公共页面只获取 Logo
|
||||
@@ -309,6 +310,7 @@ func autoMigrate(database *gorm.DB) error {
|
||||
&models.AggregatedDiskIOMetricModel{},
|
||||
&models.AggregatedGPUMetricModel{},
|
||||
&models.AggregatedTemperatureMetricModel{},
|
||||
&models.AggregatedMonitorMetricModel{},
|
||||
&models.AggregationProgress{},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -158,6 +158,24 @@ func (h *MonitorHandler) GetStatsByID(c echo.Context) error {
|
||||
return orz.Ok(c, stats)
|
||||
}
|
||||
|
||||
// GetAgentStatsByID 获取指定监控任务各探针的统计数据(公开接口,已登录返回全部,未登录返回公开可见)
|
||||
func (h *MonitorHandler) GetAgentStatsByID(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// 验证监控任务访问权限
|
||||
if _, err := h.monitorService.GetMonitorByAuth(ctx, id, utils.IsAuthenticated(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stats, err := h.monitorService.GetMonitorAgentStats(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return orz.Ok(c, stats)
|
||||
}
|
||||
|
||||
// GetHistoryByID 获取指定监控任务的历史响应时间数据(公开接口,已登录返回全部,未登录返回公开可见)
|
||||
func (h *MonitorHandler) GetHistoryByID(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -331,3 +331,24 @@ type AggregationProgress struct {
|
||||
func (AggregationProgress) TableName() string {
|
||||
return "aggregation_progress"
|
||||
}
|
||||
|
||||
// AggregatedMonitorMetricModel 监控指标聚合表
|
||||
type AggregatedMonitorMetricModel struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
MonitorID string `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:1;uniqueIndex:ux_monitoragg_bucket,priority:1" json:"monitorId"`
|
||||
AgentID string `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:2;uniqueIndex:ux_monitoragg_bucket,priority:2" json:"agentId"`
|
||||
BucketSeconds int `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:3;uniqueIndex:ux_monitoragg_bucket,priority:3" json:"bucketSeconds"`
|
||||
BucketStart int64 `gorm:"index:idx_monitoragg_monitor_agent_bucket,priority:4;uniqueIndex:ux_monitoragg_bucket,priority:4" json:"bucketStart"` // 毫秒
|
||||
AvgResponse float64 `json:"avgResponse"` // 平均响应时间(ms)
|
||||
MaxResponse int64 `json:"maxResponse"` // 最大响应时间(ms)
|
||||
MinResponse int64 `json:"minResponse"` // 最小响应时间(ms)
|
||||
SuccessCount int64 `json:"successCount"` // 成功次数
|
||||
TotalCount int64 `json:"totalCount"` // 总次数
|
||||
SuccessRate float64 `json:"successRate"` // 成功率(百分比)
|
||||
LastStatus string `json:"lastStatus"` // 最后状态
|
||||
LastError string `json:"lastError"` // 最后错误信息
|
||||
}
|
||||
|
||||
func (AggregatedMonitorMetricModel) TableName() string {
|
||||
return "monitor_metrics_aggs"
|
||||
}
|
||||
@@ -923,3 +923,70 @@ func (r *MetricRepo) GetAggregationProgress(ctx context.Context, metricType stri
|
||||
}
|
||||
return &progress, nil
|
||||
}
|
||||
|
||||
// ----------- 监控指标聚合表操作 -----------
|
||||
|
||||
// AggregateMonitorMetricsToAgg 将原始监控数据聚合到聚合表
|
||||
func (r *MetricRepo) AggregateMonitorMetricsToAgg(ctx context.Context, bucketSeconds int, start, end int64) error {
|
||||
bucketMs := int64(bucketSeconds * 1000)
|
||||
return r.db.WithContext(ctx).Exec(`
|
||||
INSERT INTO monitor_metrics_aggs (
|
||||
monitor_id, agent_id, bucket_seconds, bucket_start,
|
||||
avg_response, max_response, min_response,
|
||||
success_count, total_count, success_rate,
|
||||
last_status, last_error
|
||||
)
|
||||
WITH ranked_metrics AS (
|
||||
SELECT
|
||||
monitor_id,
|
||||
agent_id,
|
||||
(timestamp / ?) * ? as bucket_start,
|
||||
response_time,
|
||||
status,
|
||||
error,
|
||||
timestamp,
|
||||
ROW_NUMBER() OVER (PARTITION BY monitor_id, agent_id, (timestamp / ?) * ? ORDER BY timestamp DESC) as rn
|
||||
FROM monitor_metrics
|
||||
WHERE timestamp >= ? AND timestamp < ?
|
||||
)
|
||||
SELECT
|
||||
monitor_id,
|
||||
agent_id,
|
||||
? as bucket_seconds,
|
||||
bucket_start,
|
||||
AVG(response_time) as avg_response,
|
||||
MAX(response_time) as max_response,
|
||||
MIN(response_time) as min_response,
|
||||
SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) as success_count,
|
||||
COUNT(*) as total_count,
|
||||
CAST(SUM(CASE WHEN status = 'up' THEN 1 ELSE 0 END) AS REAL) / CAST(COUNT(*) AS REAL) * 100 as success_rate,
|
||||
MAX(CASE WHEN rn = 1 THEN status END) as last_status,
|
||||
MAX(CASE WHEN rn = 1 THEN error END) as last_error
|
||||
FROM ranked_metrics
|
||||
GROUP BY monitor_id, agent_id, bucket_start
|
||||
ON CONFLICT (monitor_id, agent_id, bucket_seconds, bucket_start) DO UPDATE SET
|
||||
avg_response = EXCLUDED.avg_response,
|
||||
max_response = EXCLUDED.max_response,
|
||||
min_response = EXCLUDED.min_response,
|
||||
success_count = EXCLUDED.success_count,
|
||||
total_count = EXCLUDED.total_count,
|
||||
success_rate = EXCLUDED.success_rate,
|
||||
last_status = EXCLUDED.last_status,
|
||||
last_error = EXCLUDED.last_error
|
||||
`, bucketMs, bucketMs, bucketMs, bucketMs, start, end, bucketSeconds).Error
|
||||
}
|
||||
|
||||
// GetMonitorMetricsAgg 从聚合表获取监控指标(按探针和时间间隔聚合)
|
||||
func (r *MetricRepo) GetMonitorMetricsAgg(ctx context.Context, monitorID string, start, end int64, bucketSeconds int) ([]AggregatedMonitorMetric, error) {
|
||||
var metrics []AggregatedMonitorMetric
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("monitor_metrics_aggs").
|
||||
Select(`bucket_start as timestamp, agent_id,
|
||||
avg_response, max_response, min_response,
|
||||
success_count, total_count, success_rate,
|
||||
last_status, last_error as last_error_msg`).
|
||||
Where("monitor_id = ? AND bucket_seconds = ? AND bucket_start >= ? AND bucket_start <= ?", monitorID, bucketSeconds, start, end).
|
||||
Order("bucket_start, agent_id").
|
||||
Scan(&metrics).Error
|
||||
return metrics, err
|
||||
}
|
||||
@@ -676,6 +676,7 @@ func (s *MetricService) runAggregation(ctx context.Context) {
|
||||
s.aggregateMetric(ctx, "disk_io", bucket, retention, s.metricRepo.AggregateDiskIOToAgg)
|
||||
s.aggregateMetric(ctx, "gpu", bucket, retention, s.metricRepo.AggregateGPUToAgg)
|
||||
s.aggregateMetric(ctx, "temperature", bucket, retention, s.metricRepo.AggregateTemperatureToAgg)
|
||||
s.aggregateMetric(ctx, "monitor", bucket, retention, s.metricRepo.AggregateMonitorMetricsToAgg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/dushixiang/pika/internal/protocol"
|
||||
"github.com/dushixiang/pika/internal/repo"
|
||||
ws "github.com/dushixiang/pika/internal/websocket"
|
||||
"github.com/go-orz/cache"
|
||||
"github.com/go-orz/orz"
|
||||
"github.com/go-orz/toolkit"
|
||||
"github.com/google/uuid"
|
||||
@@ -27,6 +28,11 @@ type MonitorService struct {
|
||||
metricRepo *repo.MetricRepo
|
||||
monitorStatsRepo *repo.MonitorStatsRepo
|
||||
wsManager *ws.Manager
|
||||
|
||||
// 监控概览缓存:缓存监控任务列表(使用不同的 key 区分 public 和 private)
|
||||
overviewCache cache.Cache[string, []PublicMonitorOverview]
|
||||
// 监控统计缓存:缓存单个监控任务的统计数据
|
||||
statsCache cache.Cache[string, []models.MonitorStats]
|
||||
}
|
||||
|
||||
func NewMonitorService(logger *zap.Logger, db *gorm.DB, wsManager *ws.Manager) *MonitorService {
|
||||
@@ -37,8 +43,11 @@ func NewMonitorService(logger *zap.Logger, db *gorm.DB, wsManager *ws.Manager) *
|
||||
agentRepo: repo.NewAgentRepo(db),
|
||||
metricRepo: repo.NewMetricRepo(db),
|
||||
monitorStatsRepo: repo.NewMonitorStatsRepo(db),
|
||||
wsManager: wsManager,
|
||||
|
||||
wsManager: wsManager,
|
||||
// 缓存 5 分钟,避免频繁查询
|
||||
overviewCache: cache.New[string, []PublicMonitorOverview](5 * time.Minute),
|
||||
statsCache: cache.New[string, []models.MonitorStats](5 * time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +187,18 @@ func (s *MonitorService) DeleteMonitor(ctx context.Context, id string) error {
|
||||
|
||||
// ListByAuth 返回公开展示所需的监控配置和汇总统计
|
||||
func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) ([]PublicMonitorOverview, error) {
|
||||
// 构建缓存键:根据认证状态使用不同的 key
|
||||
cacheKey := "overview:public"
|
||||
if isAuthenticated {
|
||||
cacheKey = "overview:private"
|
||||
}
|
||||
|
||||
// 尝试从缓存获取
|
||||
if cachedResult, ok := s.overviewCache.Get(cacheKey); ok {
|
||||
return cachedResult, nil
|
||||
}
|
||||
|
||||
// 缓存未命中,查询数据库
|
||||
// 获取符合权限的监控任务列表
|
||||
monitors, err := s.FindByAuth(ctx, isAuthenticated)
|
||||
if err != nil {
|
||||
@@ -185,7 +206,10 @@ func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) (
|
||||
}
|
||||
|
||||
if len(monitors) == 0 {
|
||||
return []PublicMonitorOverview{}, nil
|
||||
emptyResult := []PublicMonitorOverview{}
|
||||
// 缓存空结果
|
||||
s.overviewCache.Set(cacheKey, emptyResult, 5*time.Minute)
|
||||
return emptyResult, nil
|
||||
}
|
||||
|
||||
// 提取监控任务ID列表
|
||||
@@ -194,7 +218,7 @@ func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) (
|
||||
monitorIds = append(monitorIds, monitor.ID)
|
||||
}
|
||||
|
||||
// 批量获取统计数据
|
||||
// 批量获取统计数据(已经由后台任务预计算好)
|
||||
statsList, err := s.monitorStatsRepo.FindByMonitorIdIn(ctx, monitorIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -206,55 +230,23 @@ func (s *MonitorService) ListByAuth(ctx context.Context, isAuthenticated bool) (
|
||||
statsMap[stats.MonitorId] = append(statsMap[stats.MonitorId], stats)
|
||||
}
|
||||
|
||||
// 获取所有探针列表,用于过滤有效的统计数据
|
||||
agents, err := s.agentRepo.FindAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建监控概览列表
|
||||
items := make([]PublicMonitorOverview, 0, len(monitors))
|
||||
for _, monitor := range monitors {
|
||||
// 计算当前监控任务关联的目标探针
|
||||
targetAgents := s.resolveTargetAgents(monitor, agents)
|
||||
|
||||
// 过滤出目标探针的统计数据
|
||||
filteredStats := s.filterStatsByAgents(statsMap[monitor.ID], targetAgents)
|
||||
|
||||
// 聚合统计数据
|
||||
summary := aggregateMonitorStats(filteredStats)
|
||||
// 直接聚合预计算的统计数据
|
||||
summary := aggregateMonitorStats(statsMap[monitor.ID])
|
||||
|
||||
// 构建监控概览对象
|
||||
item := s.buildMonitorOverview(monitor, summary)
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
s.overviewCache.Set(cacheKey, items, 5*time.Minute)
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// filterStatsByAgents 过滤出指定探针的统计数据
|
||||
func (s *MonitorService) filterStatsByAgents(stats []models.MonitorStats, targetAgents []models.Agent) []models.MonitorStats {
|
||||
if len(stats) == 0 || len(targetAgents) == 0 {
|
||||
return []models.MonitorStats{}
|
||||
}
|
||||
|
||||
// 构建目标探针ID映射
|
||||
targetAgentsMap := make(map[string]bool, len(targetAgents))
|
||||
for _, agent := range targetAgents {
|
||||
targetAgentsMap[agent.ID] = true
|
||||
}
|
||||
|
||||
// 过滤统计数据
|
||||
filteredStats := make([]models.MonitorStats, 0, len(stats))
|
||||
for _, stat := range stats {
|
||||
if targetAgentsMap[stat.AgentID] {
|
||||
filteredStats = append(filteredStats, stat)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredStats
|
||||
}
|
||||
|
||||
// buildMonitorOverview 构建监控概览对象
|
||||
func (s *MonitorService) buildMonitorOverview(monitor models.MonitorTask, summary monitorOverviewSummary) PublicMonitorOverview {
|
||||
// 根据 ShowTargetPublic 字段决定是否返回真实的 Target
|
||||
@@ -652,8 +644,40 @@ func (s *MonitorService) calculateStatsForAgentMonitor(ctx context.Context, agen
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetMonitorStatsByID 获取监控任务的统计数据(所有探针)
|
||||
func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID string) ([]models.MonitorStats, error) {
|
||||
// GetMonitorStatsByID 获取监控任务的统计数据(聚合后的单个监控详情)
|
||||
func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID string) (*PublicMonitorOverview, error) {
|
||||
// 查询监控任务
|
||||
monitor, err := s.MonitorRepo.FindById(ctx, monitorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
statsList, err := s.monitorStatsRepo.FindByMonitorId(ctx, monitor.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 聚合统计数据
|
||||
summary := aggregateMonitorStats(statsList)
|
||||
|
||||
// 构建监控概览对象
|
||||
overview := s.buildMonitorOverview(monitor, summary)
|
||||
|
||||
return &overview, nil
|
||||
}
|
||||
|
||||
// GetMonitorAgentStats 获取监控任务各探针的统计数据(详细列表)
|
||||
func (s *MonitorService) GetMonitorAgentStats(ctx context.Context, monitorID string) ([]models.MonitorStats, error) {
|
||||
// 构建缓存键
|
||||
cacheKey := fmt.Sprintf("agents:%s", monitorID)
|
||||
|
||||
// 尝试从缓存获取
|
||||
if cachedResult, ok := s.statsCache.Get(cacheKey); ok {
|
||||
return cachedResult, nil
|
||||
}
|
||||
|
||||
// 缓存未命中,查询数据库
|
||||
monitor, err := s.MonitorRepo.FindById(ctx, monitorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -695,43 +719,69 @@ func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID stri
|
||||
filteredStatsList = append(filteredStatsList, stats)
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
s.statsCache.Set(cacheKey, filteredStatsList, 5*time.Minute)
|
||||
|
||||
return filteredStatsList, nil
|
||||
}
|
||||
|
||||
// GetMonitorHistory 获取监控任务的历史响应时间数据
|
||||
// 根据前端时间范围选择,直接使用聚合表数据
|
||||
// 支持时间范围:15m, 30m, 1h, 3h, 6h, 12h, 1d, 3d, 7d
|
||||
func (s *MonitorService) GetMonitorHistory(ctx context.Context, monitorID, timeRange string) ([]repo.AggregatedMonitorMetric, error) {
|
||||
monitor, err := s.MonitorRepo.FindById(ctx, monitorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析时间范围
|
||||
// 根据前端时间范围,选择最优的聚合粒度
|
||||
var duration time.Duration
|
||||
var interval int // 聚合间隔(秒)
|
||||
var bucketSeconds int // 使用的聚合 bucket(秒)
|
||||
|
||||
switch timeRange {
|
||||
case "5m":
|
||||
duration = 5 * time.Minute
|
||||
interval = 15 // 15秒聚合一次
|
||||
case "15m":
|
||||
duration = 15 * time.Minute
|
||||
interval = 30 // 30秒聚合一次
|
||||
bucketSeconds = 60 // 1分钟聚合,约15个点
|
||||
case "30m":
|
||||
duration = 30 * time.Minute
|
||||
interval = 60 // 1分钟聚合一次
|
||||
bucketSeconds = 60 // 1分钟聚合,约30个点
|
||||
case "1h":
|
||||
duration = 1 * time.Hour
|
||||
interval = 120 // 2分钟聚合一次
|
||||
bucketSeconds = 300 // 5分钟聚合,约12个点
|
||||
case "3h":
|
||||
duration = 3 * time.Hour
|
||||
bucketSeconds = 300 // 5分钟聚合,约36个点
|
||||
case "6h":
|
||||
duration = 6 * time.Hour
|
||||
bucketSeconds = 900 // 15分钟聚合,约24个点
|
||||
case "12h":
|
||||
duration = 12 * time.Hour
|
||||
bucketSeconds = 900 // 15分钟聚合,约48个点
|
||||
case "1d", "24h":
|
||||
duration = 24 * time.Hour
|
||||
bucketSeconds = 1800 // 30分钟聚合,约48个点
|
||||
case "3d":
|
||||
duration = 3 * 24 * time.Hour
|
||||
bucketSeconds = 3600 // 1小时聚合,约72个点
|
||||
case "7d":
|
||||
duration = 7 * 24 * time.Hour
|
||||
bucketSeconds = 7200 // 2小时聚合,约84个点
|
||||
default:
|
||||
duration = 5 * time.Minute
|
||||
interval = 15
|
||||
// 默认 15 分钟
|
||||
duration = 15 * time.Minute
|
||||
bucketSeconds = 60
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
end := now.UnixMilli()
|
||||
start := now.Add(-duration).UnixMilli()
|
||||
|
||||
return s.metricRepo.GetAggregatedMonitorMetrics(ctx, monitor.ID, start, end, interval)
|
||||
// 对齐时间范围到 bucket 边界
|
||||
bucketMs := int64(bucketSeconds * 1000)
|
||||
start, end = alignTimeRangeToBucket(start, end, bucketMs)
|
||||
|
||||
// 直接使用聚合表查询
|
||||
return s.metricRepo.GetMonitorMetricsAgg(ctx, monitor.ID, start, end, bucketSeconds)
|
||||
}
|
||||
|
||||
// GetMonitorByAuth 根据认证状态获取监控任务(已登录返回全部,未登录返回公开可见)
|
||||
|
||||
@@ -34,9 +34,14 @@ export const getPublicMonitors = () => {
|
||||
return get<PublicMonitor[]>('/monitors');
|
||||
};
|
||||
|
||||
// 公开接口 - 获取指定监控的统计数据
|
||||
// 公开接口 - 获取指定监控的统计数据(聚合后的单个监控详情)
|
||||
export const getMonitorStatsById = (id: string) => {
|
||||
return get<MonitorStats[]>(`/monitors/${encodeURIComponent(id)}/stats`);
|
||||
return get<PublicMonitor>(`/monitors/${encodeURIComponent(id)}/stats`);
|
||||
};
|
||||
|
||||
// 公开接口 - 获取指定监控各探针的统计数据(详细列表)
|
||||
export const getMonitorAgentStats = (id: string) => {
|
||||
return get<MonitorStats[]>(`/monitors/${encodeURIComponent(id)}/agents`);
|
||||
};
|
||||
|
||||
// 聚合的监控历史数据
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
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 type {MonitorStats} from '@/types';
|
||||
import {type AggregatedMonitorMetric, getMonitorHistory, getMonitorStatsById, getMonitorAgentStats} from '@/api/monitor.ts';
|
||||
import type {MonitorStats, PublicMonitor} from '@/types';
|
||||
|
||||
const formatTime = (ms: number): string => {
|
||||
if (!ms || ms <= 0) return '0 ms';
|
||||
@@ -74,12 +74,12 @@ 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'},
|
||||
{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 ChartPlaceholder = ({
|
||||
@@ -318,11 +318,24 @@ const MonitorDetail = () => {
|
||||
const [selectedAgent, setSelectedAgent] = useState<string>('all');
|
||||
const [timeRange, setTimeRange] = useState<string>('15m');
|
||||
|
||||
const {data: monitorStats = [], isLoading} = useQuery<MonitorStats[]>({
|
||||
queryKey: ['monitorStats', id],
|
||||
// 获取监控详情(聚合数据)
|
||||
const {data: monitorDetail, isLoading} = useQuery<PublicMonitor>({
|
||||
queryKey: ['monitorDetail', id],
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error('Monitor ID is required');
|
||||
const response = await getMonitorStatsById(id);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
// 获取各探针的统计数据
|
||||
const {data: monitorStats = []} = useQuery<MonitorStats[]>({
|
||||
queryKey: ['monitorAgentStats', id],
|
||||
queryFn: async () => {
|
||||
if (!id) return [];
|
||||
const response = await getMonitorStatsById(id);
|
||||
const response = await getMonitorAgentStats(id);
|
||||
return response.data || [];
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
@@ -392,23 +405,20 @@ const MonitorDetail = () => {
|
||||
return <LoadingSpinner/>;
|
||||
}
|
||||
|
||||
if (monitorStats.length === 0) {
|
||||
if (!monitorDetail) {
|
||||
return <EmptyState/>;
|
||||
}
|
||||
|
||||
const firstStat = monitorStats[0];
|
||||
const monitorTitle = firstStat?.name ?? '监控详情';
|
||||
const avgUptime24h = monitorStats.reduce((sum, s) => sum + s.uptime24h, 0) / monitorStats.length;
|
||||
const avgUptime30d = monitorStats.reduce((sum, s) => sum + s.uptime30d, 0) / monitorStats.length;
|
||||
const hasCert = firstStat.certExpiryDate > 0;
|
||||
const certExpired = hasCert && firstStat.certExpiryDays < 0;
|
||||
const certExpiringSoon = hasCert && firstStat.certExpiryDays >= 0 && firstStat.certExpiryDays < 30;
|
||||
const monitorTitle = monitorDetail.name ?? '监控详情';
|
||||
const hasCert = monitorDetail.certExpiryDate > 0;
|
||||
const certExpired = hasCert && monitorDetail.certExpiryDays < 0;
|
||||
const certExpiringSoon = hasCert && monitorDetail.certExpiryDays >= 0 && monitorDetail.certExpiryDays < 30;
|
||||
|
||||
const heroStats = [
|
||||
{label: '监控类型', value: firstStat.type.toUpperCase()},
|
||||
{label: '探针数量', value: `${monitorStats.length} 个`},
|
||||
{label: '24h在线率', value: `${formatPercentValue(avgUptime24h)}%`},
|
||||
{label: '30d在线率', value: `${formatPercentValue(avgUptime30d)}%`},
|
||||
{label: '监控类型', value: monitorDetail.type.toUpperCase()},
|
||||
{label: '探针数量', value: `${monitorDetail.agentCount} 个`},
|
||||
{label: '24h在线率', value: `${formatPercentValue(monitorDetail.uptime24h)}%`},
|
||||
{label: '30d在线率', value: `${formatPercentValue(monitorDetail.uptime30d)}%`},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -433,7 +443,7 @@ const MonitorDetail = () => {
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/10 text-white">
|
||||
{firstStat.type === 'tcp' ? (
|
||||
{monitorDetail.type === 'tcp' ? (
|
||||
<ServerIcon className="h-7 w-7"/>
|
||||
) : (
|
||||
<Globe className="h-7 w-7"/>
|
||||
@@ -444,7 +454,7 @@ const MonitorDetail = () => {
|
||||
<h1 className="text-3xl font-semibold">{monitorTitle}</h1>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-white/80">
|
||||
{firstStat.target}
|
||||
{monitorDetail.target}
|
||||
</p>
|
||||
<p className="text-xs text-white/60">公共视图 · 实时监控概览</p>
|
||||
</div>
|
||||
@@ -464,11 +474,11 @@ const MonitorDetail = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-white/70">
|
||||
<span>监控 ID:{firstStat.id || id}</span>
|
||||
<span>监控 ID:{monitorDetail.id || id}</span>
|
||||
<span className="hidden h-1 w-1 rounded-full bg-white/30 sm:inline-block"/>
|
||||
<span>探针数量:{monitorStats.length} 个</span>
|
||||
<span>探针数量:{monitorDetail.agentCount} 个</span>
|
||||
<span className="hidden h-1 w-1 rounded-full bg-white/30 sm:inline-block"/>
|
||||
<span>目标:{firstStat.target}</span>
|
||||
<span>目标:{monitorDetail.target}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -480,26 +490,26 @@ const MonitorDetail = () => {
|
||||
<StatCard
|
||||
icon={<Clock className="h-6 w-6"/>}
|
||||
label="当前响应"
|
||||
value={formatTime(firstStat.currentResponse)}
|
||||
value={formatTime(monitorDetail.currentResponse)}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Clock className="h-6 w-6"/>}
|
||||
label="24h 平均响应"
|
||||
value={formatTime(firstStat.avgResponse24h)}
|
||||
value={formatTime(monitorDetail.avgResponse24h)}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard
|
||||
icon={<CheckCircle2 className="h-6 w-6"/>}
|
||||
label="24h 在线率"
|
||||
value={`${formatPercentValue(avgUptime24h)}%`}
|
||||
color={avgUptime24h >= 99 ? 'emerald' : avgUptime24h >= 95 ? 'amber' : 'rose'}
|
||||
value={`${formatPercentValue(monitorDetail.uptime24h)}%`}
|
||||
color={monitorDetail.uptime24h >= 99 ? 'emerald' : monitorDetail.uptime24h >= 95 ? 'amber' : 'rose'}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<CheckCircle2 className="h-6 w-6"/>}
|
||||
label="30d 在线率"
|
||||
value={`${formatPercentValue(avgUptime30d)}%`}
|
||||
color={avgUptime30d >= 99 ? 'emerald' : avgUptime30d >= 95 ? 'amber' : 'rose'}
|
||||
value={`${formatPercentValue(monitorDetail.uptime30d)}%`}
|
||||
color={monitorDetail.uptime30d >= 99 ? 'emerald' : monitorDetail.uptime30d >= 95 ? 'amber' : 'rose'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -537,12 +547,12 @@ const MonitorDetail = () => {
|
||||
? 'text-amber-700 dark:text-amber-200'
|
||||
: 'text-slate-600 dark:text-slate-400'
|
||||
}`}>
|
||||
证书到期时间: {formatDate(firstStat.certExpiryDate)}
|
||||
证书到期时间: {formatDate(monitorDetail.certExpiryDate)}
|
||||
{certExpired ? (
|
||||
<span
|
||||
className="ml-1">(已过期 {Math.abs(firstStat.certExpiryDays)} 天)</span>
|
||||
className="ml-1">(已过期 {Math.abs(monitorDetail.certExpiryDays)} 天)</span>
|
||||
) : (
|
||||
<span className="ml-1">(剩余 {firstStat.certExpiryDays} 天)</span>
|
||||
<span className="ml-1">(剩余 {monitorDetail.certExpiryDays} 天)</span>
|
||||
)}
|
||||
</p>
|
||||
{certExpired && (
|
||||
|
||||
Reference in new issue
Block a user