diff --git a/internal/app.go b/internal/app.go index fef174f..b0b16d0 100644 --- a/internal/app.go +++ b/internal/app.go @@ -294,21 +294,19 @@ func setupApi(app *orz.App, components *AppComponents) { func autoMigrate(database *gorm.DB) error { // 自动迁移数据库表 return database.AutoMigrate( - &models.Agent{}, - &models.ApiKey{}, - &models.HostMetric{}, // 保留主机静态信息表 - &models.AuditResult{}, - &models.Property{}, - &models.AlertRecord{}, - &models.AlertState{}, - &models.MonitorTask{}, - // MonitorStats 已废弃,统计数据从 VictoriaMetrics 查询 - &models.TamperProtectConfig{}, - &models.TamperEvent{}, - &models.TamperAlert{}, - &models.DDNSConfig{}, - &models.DDNSRecord{}, - // 指标数据已迁移到 VictoriaMetrics + &models.Agent{}, // 探针 + &models.ApiKey{}, // ApiKey + &models.HostMetric{}, // 保留主机静态信息表 + &models.AuditResult{}, // 审计历史 + &models.Property{}, // 系统属性 + &models.AlertRecord{}, // 告警记录 + &models.AlertState{}, // 告警状态 + &models.MonitorTask{}, // 服务监控 + &models.TamperProtectConfig{}, // 防篡改配置 + &models.TamperEvent{}, // 防篡改事件 + &models.TamperAlert{}, // 防篡改告警 + &models.DDNSConfig{}, // DDNS 配置 + &models.DDNSRecord{}, // DDNS 记录 ) } diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go index 1587976..d36eebb 100644 --- a/internal/handler/agent_handler.go +++ b/internal/handler/agent_handler.go @@ -80,7 +80,7 @@ func (h *AgentHandler) HandleWebSocket(c echo.Context) error { conn.SetReadDeadline(time.Time{}) // 解析注册消息 - var msg protocol.Message + var msg protocol.InputMessage if err := json.Unmarshal(message, &msg); err != nil { h.logger.Error("failed to parse register message", zap.Error(err)) conn.Close() @@ -154,11 +154,15 @@ func (h *AgentHandler) handleWebSocketMessage(ctx context.Context, agentID strin case protocol.MessageTypeMetrics: // 指标数据 - var metricsWrapper protocol.MetricsWrapper + var metricsWrapper protocol.MetricsPayload if err := json.Unmarshal(data, &metricsWrapper); err != nil { return err } - return h.metricService.HandleMetricData(ctx, agentID, string(metricsWrapper.Type), metricsWrapper.Data) + metricsData, err := json.Marshal(metricsWrapper.Data) + if err != nil { + return err + } + return h.metricService.HandleMetricData(ctx, agentID, string(metricsWrapper.Type), json.RawMessage(metricsData)) case protocol.MessageTypeCommandResp: // 指令响应 @@ -230,21 +234,10 @@ func (h *AgentHandler) sendRegisterSuccess(conn *websocket.Conn, agentID string) AgentID: agentID, Status: "success", } - respData, err := json.Marshal(resp) - if err != nil { - return err - } - - msg := protocol.Message{ + return conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeRegisterAck, - Data: respData, - } - msgData, err := json.Marshal(msg) - if err != nil { - return err - } - - return conn.WriteMessage(websocket.TextMessage, msgData) + Data: resp, + }) } // sendRegisterError 发送注册失败响应 @@ -253,15 +246,11 @@ func (h *AgentHandler) sendRegisterError(conn *websocket.Conn, errMsg string) er Status: "error", Message: errMsg, } - respData, _ := json.Marshal(resp) - msg := protocol.Message{ + return conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeRegisterErr, - Data: respData, - } - msgData, _ := json.Marshal(msg) - - return conn.WriteMessage(websocket.TextMessage, msgData) + Data: resp, + }) } // sendTamperConfig 发送防篡改配置(探针初始化时发送完整配置作为新增) @@ -286,17 +275,10 @@ func (h *AgentHandler) sendTamperConfig(conn *websocket.Conn, agentID string) er Removed: []string{}, // 初始化时没有需要移除的 } - data, err := json.Marshal(configData) - if err != nil { - return err - } - - msg := protocol.Message{ + msgData, err := json.Marshal(protocol.OutboundMessage{ Type: protocol.MessageTypeTamperProtect, - Data: data, - } - - msgData, err := json.Marshal(msg) + Data: configData, + }) if err != nil { return err } @@ -595,17 +577,10 @@ func (h *AgentHandler) SendCommand(c echo.Context) error { Type: cmdType, } - reqData, err := json.Marshal(cmdReq) - if err != nil { - return err - } - - msg := protocol.Message{ + msgData, err := json.Marshal(protocol.OutboundMessage{ Type: protocol.MessageTypeCommand, - Data: reqData, - } - - msgData, err := json.Marshal(msg) + Data: cmdReq, + }) if err != nil { return err } diff --git a/internal/protocol/message.go b/internal/protocol/message.go index 84eb530..0cd6e3d 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -2,12 +2,18 @@ package protocol import "encoding/json" -// Message WebSocket消息结构 -type Message struct { +// InputMessage WebSocket消息结构(主要用于接收) +type InputMessage struct { Type MessageType `json:"type"` Data json.RawMessage `json:"data"` } +// OutboundMessage WebSocket 出站消息结构 +type OutboundMessage struct { + Type MessageType `json:"type"` + Data interface{} `json:"data"` +} + // RegisterRequest 注册请求 type RegisterRequest struct { AgentInfo AgentInfo `json:"agentInfo"` @@ -31,12 +37,11 @@ type AgentInfo struct { Version string `json:"version"` // 版本号 } -// MetricsWrapper 指标数据包装 -type MetricsWrapper struct { - Type MetricType `json:"type"` - Data json.RawMessage `json:"data"` +// MetricsPayload 指标数据包装,发送端/接收端统一使用 +type MetricsPayload struct { + Type MetricType `json:"type"` + Data interface{} `json:"data"` } - type MessageType string // 控制消息 diff --git a/internal/service/ddns_service.go b/internal/service/ddns_service.go index e67c557..b813c70 100644 --- a/internal/service/ddns_service.go +++ b/internal/service/ddns_service.go @@ -402,17 +402,10 @@ func (s *DDNSService) sendDDNSConfigToAgent(config *models.DDNSConfig) error { return err } - data, err := json.Marshal(configData) - if err != nil { - return err - } - - msg := protocol.Message{ + msgData, err := json.Marshal(protocol.OutboundMessage{ Type: protocol.MessageTypeDDNSConfig, - Data: data, - } - - msgData, err := json.Marshal(msg) + Data: configData, + }) if err != nil { return err } diff --git a/internal/service/metric_service.go b/internal/service/metric_service.go index 864c0a8..8034839 100644 --- a/internal/service/metric_service.go +++ b/internal/service/metric_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "time" "github.com/dushixiang/pika/internal/models" @@ -666,9 +667,7 @@ func (s *MetricService) convertQueryResultToSeries(result *vmclient.QueryResult, continue } - var value float64 - fmt.Sscanf(valueStr, "%f", &value) - + value, _ := strconv.ParseFloat(valueStr, 64) dataPoints = append(dataPoints, MetricDataPoint{ Timestamp: int64(timestamp * 1000), // 转换为毫秒 Value: value, @@ -678,8 +677,8 @@ func (s *MetricService) convertQueryResultToSeries(result *vmclient.QueryResult, // 合并标签 labels := make(map[string]string) for k, v := range timeSeries.Metric { - // 排除内部标签 - if k != "__name__" && k != "agent_id" { + // 只排除 __name__ 内部标签,保留 agent_id(监控功能需要用它来区分探针) + if k != "__name__" { labels[k] = v } } @@ -734,20 +733,20 @@ type MonitorStatsResult struct { // AgentMonitorStat 单个探针的监控统计 type AgentMonitorStat struct { - AgentID string - CurrentResponse int64 - AvgResponse24h int64 - Uptime24h float64 - Uptime7d float64 - LastCheckStatus string - LastCheckError string - LastCheckTime int64 - CertExpiryDate int64 - CertExpiryDays int - TotalChecks24h int64 - SuccessChecks24h int64 - TotalChecks7d int64 - SuccessChecks7d int64 + AgentID string `json:"agentID,omitempty"` + CurrentResponse int64 `json:"currentResponse,omitempty"` + AvgResponse24h int64 `json:"avgResponse24H,omitempty"` + Uptime24h float64 `json:"uptime24H,omitempty"` + Uptime7d float64 `json:"uptime7D,omitempty"` + LastCheckStatus string `json:"lastCheckStatus,omitempty"` + LastCheckError string `json:"lastCheckError,omitempty"` + LastCheckTime int64 `json:"lastCheckTime,omitempty"` + CertExpiryDate int64 `json:"certExpiryDate,omitempty"` + CertExpiryDays int `json:"certExpiryDays,omitempty"` + TotalChecks24h int64 `json:"totalChecks24H,omitempty"` + SuccessChecks24h int64 `json:"successChecks24H,omitempty"` + TotalChecks7d int64 `json:"totalChecks7D,omitempty"` + SuccessChecks7d int64 `json:"successChecks7D,omitempty"` } // buildMonitorPromQLQueries 构建监控查询的 PromQL 语句 diff --git a/internal/service/monitor_service.go b/internal/service/monitor_service.go index e9564c8..02ebe3c 100644 --- a/internal/service/monitor_service.go +++ b/internal/service/monitor_service.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "sort" "strings" "time" @@ -420,17 +419,10 @@ func (s *MonitorService) resolveTargetAgents(monitor models.MonitorTask, availab // sendMonitorConfigToAgent 向指定探针发送监控配置(内部方法) func (s *MonitorService) sendMonitorConfigToAgent(agentID string, payload protocol.MonitorConfigPayload) error { - payloadData, err := json.Marshal(payload) - if err != nil { - return err - } - - msg := protocol.Message{ + msgData, err := json.Marshal(protocol.OutboundMessage{ Type: protocol.MessageTypeMonitorConfig, - Data: payloadData, - } - - msgData, err := json.Marshal(msg) + Data: payload, + }) if err != nil { return err } @@ -542,108 +534,17 @@ func (s *MonitorService) GetMonitorStatsByID(ctx context.Context, monitorID stri return &overview, nil } -type MonitorStats struct { - ID string `gorm:"primaryKey" json:"id"` // ID - AgentID string `json:"agentId"` // 探针ID - AgentName string `gorm:"-" json:"agentName,omitempty"` // 探针名称(不存储在数据库,仅用于 API 返回) - MonitorId string `json:"monitorId"` // 监控项ID - MonitorName string `gorm:"-" json:"name"` // 监控项名称(不存储在数据库,仅用于 API 返回) - MonitorType string `json:"type"` // 监控类型 - Target string `json:"target"` // 目标地址 - CurrentResponse int64 `json:"currentResponse"` // 当前响应时间(ms) - AvgResponse24h int64 `json:"avgResponse24h"` // 24小时平均响应时间(ms) - Uptime24h float64 `json:"uptime24h"` // 24小时在线率(百分比) - Uptime7d float64 `json:"uptime7d"` // 7天在线率(百分比) - CertExpiryDate int64 `json:"certExpiryDate"` // 证书过期时间(毫秒时间戳),0表示无证书 - CertExpiryDays int `json:"certExpiryDays"` // 证书剩余天数 - TotalChecks24h int64 `json:"totalChecks24h"` // 24小时总检测次数 - SuccessChecks24h int64 `json:"successChecks24h"` // 24小时成功次数 - TotalChecks7d int64 `json:"totalChecks7d"` // 7天总检测次数 - SuccessChecks7d int64 `json:"successChecks7d"` // 7天成功次数 - LastCheckTime int64 `json:"lastCheckTime"` // 最后检测时间 - LastCheckStatus string `json:"lastCheckStatus"` // 最后检测状态: up/down - LastCheckError string `json:"lastCheckError"` // 最后检测错误信息 - UpdatedAt int64 `gorm:"autoUpdateTime:milli" json:"updatedAt"` // 更新时间 -} - // GetMonitorAgentStats 获取监控任务各探针的统计数据(详细列表) -func (s *MonitorService) GetMonitorAgentStats(ctx context.Context, monitorID string) ([]MonitorStats, error) { - // 查询监控任务 - monitor, err := s.MonitorRepo.FindById(ctx, monitorID) - if err != nil { - return nil, err - } - - // 从 VictoriaMetrics 查询各探针统计 - agentStats, err := s.metricService.GetMonitorAgentStats(ctx, monitorID) - if err != nil { - s.logger.Error("查询 VictoriaMetrics 失败", zap.String("monitorID", monitorID), zap.Error(err)) - return nil, err - } - - // 填充探针名称、监控名称 - agentMap := make(map[string]*models.Agent) - if len(agentStats) > 0 { - agentIDs := make([]string, 0, len(agentStats)) - for _, stat := range agentStats { - agentIDs = append(agentIDs, stat.AgentID) - } - agents, _ := s.agentRepo.FindByIdIn(ctx, agentIDs) - for _, agent := range agents { - agentMap[agent.ID] = &agent - } - } - - // 确定目标 target 值(是否隐藏) - target := monitor.Target - if !monitor.ShowTargetPublic { - target = "******" - } - - // 转换为 MonitorStats 格式 - result := make([]MonitorStats, 0, len(agentStats)) - for _, stat := range agentStats { - ms := MonitorStats{ - AgentID: stat.AgentID, - AgentName: "", - MonitorId: monitorID, - MonitorName: monitor.Name, - MonitorType: monitor.Type, - Target: target, - CurrentResponse: stat.CurrentResponse, - AvgResponse24h: stat.AvgResponse24h, - Uptime24h: stat.Uptime24h, - Uptime7d: stat.Uptime7d, - CertExpiryDate: stat.CertExpiryDate, - CertExpiryDays: stat.CertExpiryDays, - TotalChecks24h: stat.TotalChecks24h, - SuccessChecks24h: stat.SuccessChecks24h, - TotalChecks7d: stat.TotalChecks7d, - SuccessChecks7d: stat.SuccessChecks7d, - LastCheckTime: stat.LastCheckTime, - LastCheckStatus: stat.LastCheckStatus, - LastCheckError: stat.LastCheckError, - } - - if agent, ok := agentMap[stat.AgentID]; ok { - ms.AgentName = agent.Name - } - - result = append(result, ms) - } - - return result, nil +// 直接返回 VictoriaMetrics 查询结果,无需额外转换 +func (s *MonitorService) GetMonitorAgentStats(ctx context.Context, monitorID string) ([]AgentMonitorStat, error) { + // 直接返回 VictoriaMetrics 查询结果 + return s.metricService.GetMonitorAgentStats(ctx, monitorID) } -// GetMonitorHistory 获取监控任务的历史响应时间数据 -// 根据前端时间范围选择,直接使用聚合表数据 +// GetMonitorHistory 获取监控任务的历史时序数据 +// 直接返回 VictoriaMetrics 的原始时序数据,包含所有探针的独立序列 // 支持时间范围:15m, 30m, 1h, 3h, 6h, 12h, 1d, 3d, 7d -func (s *MonitorService) GetMonitorHistory(ctx context.Context, monitorID, timeRange string) ([]repo.AggregatedMonitorMetric, error) { - // 验证监控任务存在 - if _, err := s.MonitorRepo.FindById(ctx, monitorID); err != nil { - return nil, err - } - +func (s *MonitorService) GetMonitorHistory(ctx context.Context, monitorID, timeRange string) (*GetMetricsResponse, error) { // 计算时间范围 var duration time.Duration switch timeRange { @@ -673,67 +574,8 @@ func (s *MonitorService) GetMonitorHistory(ctx context.Context, monitorID, timeR end := now.UnixMilli() start := now.Add(-duration).UnixMilli() - // 从 VictoriaMetrics 查询历史数据 - metricsResp, err := s.metricService.GetMonitorHistory(ctx, monitorID, start, end) - if err != nil { - s.logger.Error("查询 VictoriaMetrics 失败", zap.String("monitorID", monitorID), zap.Error(err)) - return []repo.AggregatedMonitorMetric{}, nil - } - - // 转换为 AggregatedMonitorMetric 格式 - result := make([]repo.AggregatedMonitorMetric, 0) - - // 按时间戳分组,聚合多个探针的数据 - timestampMap := make(map[int64]map[string]float64) // timestamp -> agentID -> avgResponse - - for _, series := range metricsResp.Series { - // 从 series.Labels 中提取 agent_id - agentID := "" - if series.Labels != nil { - agentID = series.Labels["agent_id"] - } - if agentID == "" && len(metricsResp.Series) > 0 { - // 如果没有 agent_id 标签,尝试从 series name 中提取 - // 这是 response_time 系列,每个探针一条时间序列 - // series.Name 可能包含探针信息 - agentID = series.Name - } - - for _, dataPoint := range series.Data { - if timestampMap[dataPoint.Timestamp] == nil { - timestampMap[dataPoint.Timestamp] = make(map[string]float64) - } - timestampMap[dataPoint.Timestamp][agentID] = dataPoint.Value - } - } - - // 将 map 转换为数组,并计算每个时间点的聚合数据 - for timestamp, agentData := range timestampMap { - var totalResponse float64 - var count int - for _, avgResp := range agentData { - totalResponse += avgResp - count++ - } - - var avgResponse float64 - if count > 0 { - avgResponse = totalResponse / float64(count) - } - - result = append(result, repo.AggregatedMonitorMetric{ - Timestamp: timestamp, - AgentID: "", // 聚合数据不指定单个 agentID - AvgResponse: avgResponse, - }) - } - - // 按时间戳排序 - sort.Slice(result, func(i, j int) bool { - return result[i].Timestamp < result[j].Timestamp - }) - - return result, nil + // 直接返回 VictoriaMetrics 查询结果,无需任何转换 + return s.metricService.GetMonitorHistory(ctx, monitorID, start, end) } // GetMonitorByAuth 根据认证状态获取监控任务(已登录返回全部,未登录返回公开可见) diff --git a/internal/service/tamper_service.go b/internal/service/tamper_service.go index 05a325e..134868b 100644 --- a/internal/service/tamper_service.go +++ b/internal/service/tamper_service.go @@ -142,17 +142,10 @@ func (s *TamperService) sendConfigToAgent(agentID string, added, removed []strin Removed: removed, } - data, err := json.Marshal(configData) - if err != nil { - return err - } - - msg := protocol.Message{ + msgBytes, err := json.Marshal(protocol.OutboundMessage{ Type: protocol.MessageTypeTamperProtect, - Data: data, - } - - msgBytes, err := json.Marshal(msg) + Data: configData, + }) if err != nil { return err } diff --git a/internal/websocket/manager.go b/internal/websocket/manager.go index 3c4afc2..246966a 100644 --- a/internal/websocket/manager.go +++ b/internal/websocket/manager.go @@ -221,7 +221,7 @@ func (c *Client) ReadPump(ctx context.Context) { c.LastActive = time.Now() // 解析消息 - var msg protocol.Message + var msg protocol.InputMessage if err := json.Unmarshal(message, &msg); err != nil { c.Manager.logger.Error("failed to parse message", zap.Error(err), zap.String("agentID", c.ID)) continue diff --git a/pkg/agent/collector/manager.go b/pkg/agent/collector/manager.go index 6022b97..cd04adf 100644 --- a/pkg/agent/collector/manager.go +++ b/pkg/agent/collector/manager.go @@ -1,8 +1,6 @@ package collector import ( - "encoding/json" - "github.com/dushixiang/pika/internal/protocol" "github.com/dushixiang/pika/pkg/agent/config" ) @@ -168,42 +166,21 @@ func (m *Manager) CollectAndSendDDNSIP(conn WebSocketWriter) error { return nil } - dataBytes, err := json.Marshal(ipData) - if err != nil { - return err - } - - msg := protocol.Message{ + return conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeDDNSIPReport, - Data: dataBytes, - } - - return conn.WriteJSON(msg) + Data: ipData, + }) } // sendMetrics 发送指标数据 func (m *Manager) sendMetrics(conn WebSocketWriter, metricType protocol.MetricType, data interface{}) error { - dataBytes, err := json.Marshal(data) - if err != nil { - return err - } - - metrics := protocol.MetricsWrapper{ - Type: metricType, - Data: json.RawMessage(dataBytes), - } - - metricsData, err := json.Marshal(metrics) - if err != nil { - return err - } - - msg := protocol.Message{ + return conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeMetrics, - Data: metricsData, - } - - return conn.WriteJSON(msg) + Data: protocol.MetricsPayload{ + Type: metricType, + Data: data, + }, + }) } // GetPublicIP 通过 API 获取公网 IP 地址 diff --git a/pkg/agent/service/agent.go b/pkg/agent/service/agent.go index 6f4c186..4cfa179 100644 --- a/pkg/agent/service/agent.go +++ b/pkg/agent/service/agent.go @@ -258,7 +258,7 @@ func (a *Agent) readLoop(conn *websocket.Conn, done chan struct{}) error { } // 解析消息 - var msg protocol.Message + var msg protocol.InputMessage if err := json.Unmarshal(message, &msg); err != nil { log.Printf("⚠️ 解析消息失败: %v", err) continue @@ -313,22 +313,15 @@ func (a *Agent) registerAgent(conn *safeConn) error { ApiKey: a.cfg.Server.APIKey, } - reqData, err := json.Marshal(registerReq) - if err != nil { - return fmt.Errorf("序列化注册请求失败: %w", err) - } - - msg := protocol.Message{ + if err := conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeRegister, - Data: reqData, - } - - if err := conn.WriteJSON(msg); err != nil { + Data: registerReq, + }); err != nil { return fmt.Errorf("发送注册消息失败: %w", err) } // 读取注册响应 - var response protocol.Message + var response protocol.InputMessage if err := conn.ReadJSON(&response); err != nil { return fmt.Errorf("读取注册响应失败: %w", err) } @@ -392,11 +385,10 @@ func (a *Agent) heartbeatLoop(ctx context.Context, conn *safeConn, done chan str for { select { case <-ticker.C: - msg := protocol.Message{ + if err := conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeHeartbeat, - Data: json.RawMessage(`{}`), - } - if err := conn.WriteJSON(msg); err != nil { + Data: struct{}{}, + }); err != nil { return fmt.Errorf("发送心跳失败: %w", err) } //log.Println("💓 心跳已发送") @@ -581,24 +573,10 @@ func (a *Agent) sendCommandResponse(conn *safeConn, cmdID, cmdType, status, errM Result: result, } - respData, err := json.Marshal(resp) - if err != nil { - log.Printf("⚠️ 序列化指令响应失败: %v", err) - return - } - - msg := protocol.Message{ + if err := conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeCommandResp, - Data: respData, - } - - msgData, err := json.Marshal(msg) - if err != nil { - log.Printf("⚠️ 序列化消息失败: %v", err) - return - } - - if err := conn.WriteMessage(websocket.TextMessage, msgData); err != nil { + Data: resp, + }); err != nil { log.Printf("⚠️ 发送指令响应失败: %v", err) } } @@ -670,18 +648,10 @@ func (a *Agent) sendTamperProtectResponse(success bool, message string, paths [] Error: errMsg, } - respData, err := json.Marshal(resp) - if err != nil { - log.Printf("⚠️ 序列化防篡改保护响应失败: %v", err) - return - } - - msg := protocol.Message{ + if err := conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeTamperProtect, - Data: respData, - } - - if err := conn.WriteJSON(msg); err != nil { + Data: resp, + }); err != nil { log.Printf("⚠️ 发送防篡改保护响应失败: %v", err) } } @@ -705,18 +675,10 @@ func (a *Agent) tamperEventLoop(ctx context.Context, conn *safeConn, done chan s Details: event.Details, } - data, err := json.Marshal(eventData) - if err != nil { - log.Printf("⚠️ 序列化防篡改事件失败: %v", err) - continue - } - - msg := protocol.Message{ + if err := conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeTamperEvent, - Data: data, - } - - if err := conn.WriteJSON(msg); err != nil { + Data: eventData, + }); err != nil { log.Printf("⚠️ 发送防篡改事件失败: %v", err) } else { log.Printf("📤 已上报防篡改事件: %s - %s", event.Path, event.Operation) @@ -744,18 +706,10 @@ func (a *Agent) tamperAlertLoop(ctx context.Context, conn *safeConn, done chan s Restored: alert.Restored, } - data, err := json.Marshal(alertData) - if err != nil { - log.Printf("⚠️ 序列化属性篡改告警失败: %v", err) - continue - } - - msg := protocol.Message{ + if err := conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeTamperAlert, - Data: data, - } - - if err := conn.WriteJSON(msg); err != nil { + Data: alertData, + }); err != nil { log.Printf("⚠️ 发送属性篡改告警失败: %v", err) } else { status := "未恢复" @@ -829,18 +783,10 @@ func (a *Agent) collectAndSendDDNSIP(conn *safeConn, manager *collector.Manager, return fmt.Errorf("未获取到任何 IP 地址") } - // 序列化并发送 - data, err := json.Marshal(ipReport) - if err != nil { - return fmt.Errorf("序列化 IP 报告失败: %w", err) - } - - msg := protocol.Message{ + if err := conn.WriteJSON(protocol.OutboundMessage{ Type: protocol.MessageTypeDDNSIPReport, - Data: data, - } - - if err := conn.WriteJSON(msg); err != nil { + Data: ipReport, + }); err != nil { return fmt.Errorf("发送 IP 报告失败: %w", err) } diff --git a/web/src/api/monitor.ts b/web/src/api/monitor.ts index f73700c..e4e6f34 100644 --- a/web/src/api/monitor.ts +++ b/web/src/api/monitor.ts @@ -39,26 +39,51 @@ export const getMonitorStatsById = (id: string) => { return get(`/monitors/${encodeURIComponent(id)}/stats`); }; -// 公开接口 - 获取指定监控各探针的统计数据(详细列表) +// 公开接口 - 获取指定监控各探针的统计数据(直接从 VictoriaMetrics 查询) export const getMonitorAgentStats = (id: string) => { - return get(`/monitors/${encodeURIComponent(id)}/agents`); + return get(`/monitors/${encodeURIComponent(id)}/agents`); }; -// 聚合的监控历史数据 -export interface AggregatedMonitorMetric { - timestamp: number; - agentId: string; - avgResponse: number; - maxResponse: number; - minResponse: number; - successCount: number; - totalCount: number; - successRate: number; - lastStatus: string; - lastErrorMsg: string; +// VictoriaMetrics 时序数据点 +export interface MetricDataPoint { + timestamp: number; // 毫秒时间戳 + value: number; } -// 公开接口 - 获取指定监控的历史数据 -export const getMonitorHistory = (id: string, range: string = '5m') => { - return get(`/monitors/${encodeURIComponent(id)}/history?range=${range}`); +// VictoriaMetrics 时序数据系列 +export interface MetricSeries { + name: string; // 系列名称(如 "response_time") + labels?: Record; // 标签(如 { agent_id: "xxx", monitor_id: "yyy" }) + data: MetricDataPoint[]; // 数据点数组 +} + +// VictoriaMetrics 查询响应(直接返回时序数据) +export interface GetMetricsResponse { + agentId: string; // 为空表示多探针 + type: string; // "monitor" + range: string; // 时间范围描述 + series: MetricSeries[]; // 时序数据系列(每个探针一个系列) +} + +// 探针监控统计(直接从 VictoriaMetrics 查询) +export interface AgentMonitorStat { + agentID: string; + currentResponse: number; + avgResponse24h: number; + uptime24h: number; + uptime7d: number; + lastCheckStatus: string; + lastCheckError: string; + lastCheckTime: number; + certExpiryDate: number; + certExpiryDays: number; + totalChecks24h: number; + successChecks24h: number; + totalChecks7d: number; + successChecks7d: number; +} + +// 公开接口 - 获取指定监控的历史数据(VictoriaMetrics 原始时序数据) +export const getMonitorHistory = (id: string, range: string = '15m') => { + return get(`/monitors/${encodeURIComponent(id)}/history?range=${range}`); }; diff --git a/web/src/pages/Public/MonitorDetail.tsx b/web/src/pages/Public/MonitorDetail.tsx index 221f242..75b9338 100644 --- a/web/src/pages/Public/MonitorDetail.tsx +++ b/web/src/pages/Public/MonitorDetail.tsx @@ -16,12 +16,13 @@ import { import type {TooltipProps} from 'recharts'; import {Area, AreaChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis,} from 'recharts'; import { - type AggregatedMonitorMetric, + type AgentMonitorStat, + type GetMetricsResponse, getMonitorAgentStats, getMonitorHistory, getMonitorStatsById } from '@/api/monitor.ts'; -import type {MonitorStats, PublicMonitor} from '@/types'; +import type {PublicMonitor} from '@/types'; import {cn} from '@/lib/utils'; const formatTime = (ms: number): string => { @@ -357,7 +358,7 @@ const MonitorDetail = () => { }); // 获取各探针的统计数据 - const {data: monitorStats = []} = useQuery({ + const {data: monitorStats = []} = useQuery({ queryKey: ['monitorAgentStats', id], queryFn: async () => { if (!id) return []; @@ -369,12 +370,12 @@ const MonitorDetail = () => { }); // 获取历史数据 - const {data: historyData = []} = useQuery({ + const {data: historyData} = useQuery({ queryKey: ['monitorHistory', id, timeRange], queryFn: async () => { - if (!id) return []; + if (!id) throw new Error('Monitor ID is required'); const response = await getMonitorHistory(id, timeRange); - return response.data || []; + return response.data; }, refetchInterval: 30000, enabled: !!id, @@ -384,8 +385,8 @@ const MonitorDetail = () => { const availableAgents = useMemo(() => { if (monitorStats.length === 0) return []; return monitorStats.map(stat => ({ - id: stat.agentId, - label: stat.agentName || stat.agentId.substring(0, 8), + id: stat.agentID, + label: stat.agentID.substring(0, 8), })); }, [monitorStats]); @@ -401,30 +402,42 @@ const MonitorDetail = () => { // 生成图表数据 const chartData = useMemo(() => { - if (historyData.length === 0) return []; + if (!historyData?.series) return []; + + // 过滤出响应时间指标的 series + const responseTimeSeries = historyData.series.filter(s => s.name === 'response_time'); + + // 根据选择的探针过滤 + const filteredSeries = selectedAgent === 'all' + ? responseTimeSeries + : responseTimeSeries.filter(s => s.labels?.agent_id === selectedAgent); + + if (filteredSeries.length === 0) return []; // 按时间戳分组数据 - const grouped = historyData.reduce((acc, item) => { - const time = new Date(item.timestamp).toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', + const grouped: Record = {}; + + filteredSeries.forEach(series => { + const agentId = series.labels?.agent_id || 'unknown'; + const agentKey = `agent_${agentId}`; + + series.data.forEach(point => { + if (!grouped[point.timestamp]) { + grouped[point.timestamp] = { + time: new Date(point.timestamp).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }), + timestamp: point.timestamp, + }; + } + grouped[point.timestamp][agentKey] = point.value; }); + }); - if (!acc[time]) { - acc[time] = {time, timestamp: item.timestamp}; - } - - // 根据选择的探针过滤 - if (selectedAgent === 'all' || item.agentId === selectedAgent) { - const agentKey = `agent_${item.agentId}`; - acc[time][agentKey] = item.avgResponse; - } - - return acc; - }, {} as Record); - - return Object.values(grouped); + // 按时间戳排序 + return Object.values(grouped).sort((a, b) => a.timestamp - b.timestamp); }, [historyData, selectedAgent]); if (isLoading) { @@ -655,7 +668,7 @@ const MonitorDetail = () => { {monitorStats.map((stat, index) => { - const agentKey = `agent_${stat.agentId}`; + const agentKey = `agent_${stat.agentID}`; const color = AGENT_COLORS[index % AGENT_COLORS.length]; return ( { className="hidden sm:block" /> {monitorStats - .filter(stat => selectedAgent === 'all' || stat.agentId === selectedAgent) + .filter(stat => selectedAgent === 'all' || stat.agentID === selectedAgent) .map((stat) => { // 使用原始索引保持颜色一致性 - const originalIndex = monitorStats.findIndex(s => s.agentId === stat.agentId); - const agentKey = `agent_${stat.agentId}`; + const originalIndex = monitorStats.findIndex(s => s.agentID === stat.agentID); + const agentKey = `agent_${stat.agentID}`; const color = AGENT_COLORS[originalIndex % AGENT_COLORS.length]; - const agentLabel = stat.agentName || stat.agentId.substring(0, 8); + const agentLabel = stat.agentID.substring(0, 8); return ( { {monitorStats.map((stats, index) => { const color = AGENT_COLORS[index % AGENT_COLORS.length]; return ( -
@@ -768,23 +781,10 @@ const MonitorDetail = () => { style={{backgroundColor: color}} />
- {stats.agentName ? ( - <> - - {stats.agentName} - - - {stats.agentId.substring(0, 8)}... - - - ) : ( - - {stats.agentId.substring(0, 8)}... + + {stats.agentID.substring(0, 8)}... - )}