修复图表时间和查询区间不匹配问题

This commit is contained in:
Alex Yang
2025-12-02 23:20:10 +08:00
commit 4d66bdf633
49 changed files with 16275 additions and 0 deletions

View File

@@ -0,0 +1,670 @@
package handler
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"os"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/monitor/backend/internal/device"
"github.com/monitor/backend/internal/storage"
)
// Handler API处理器
type Handler struct {
storage *storage.Storage
}
// NewHandler 创建新的API处理器
func NewHandler(storage *storage.Storage) *Handler {
return &Handler{
storage: storage,
}
}
// RegisterRoutes 注册所有路由
func RegisterRoutes(r *gin.Engine) {
// API路由组
api := r.Group("/api")
{
// 监控数据路由
metrics := api.Group("/metrics")
{
metrics.GET("/cpu", GetCPUMetrics)
metrics.GET("/memory", GetMemoryMetrics)
metrics.GET("/disk", GetDiskMetrics)
metrics.GET("/network", GetNetworkMetrics)
// 添加POST端点接收Agent发送的指标数据
metrics.POST("/", HandleMetricsPost)
}
// 设备管理路由
devices := api.Group("/devices")
{
devices.GET("/", GetDevices) // 获取活跃设备列表
devices.GET("/all", GetAllDevices) // 获取所有设备
devices.GET("/:id", GetDevice) // 获取单个设备详情
devices.POST("/", AddDevice) // 添加设备
devices.PUT("/:id", UpdateDevice) // 更新设备信息
devices.DELETE("/:id", DeleteDevice) // 删除设备
// 添加获取设备状态的端点
devices.GET("/status", GetAllDeviceStatus) // 获取所有活跃设备状态
devices.GET("/:id/status", GetDeviceStatus) // 获取单个设备状态
}
// WebSocket端点
api.GET("/ws", WebSocketHandler)
// 添加设备状态概览端点,作为/api/devices/status的别名
api.GET("/status", GetAllDeviceStatus)
}
}
// MetricsRequest 指标请求结构
type MetricsRequest struct {
CPU float64 `json:"cpu"`
Memory float64 `json:"memory"`
Disk map[string]float64 `json:"disk"`
Network struct {
BytesSent uint64 `json:"bytes_sent"`
BytesReceived uint64 `json:"bytes_received"`
} `json:"network"`
}
// HandleMetricsPost 处理Agent发送的指标数据
func HandleMetricsPost(c *gin.Context) {
// 获取设备令牌
token := c.GetHeader("X-Device-Token")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Device token is required",
})
return
}
// 通过令牌获取设备
device, ok := deviceStorage.GetDeviceByToken(token)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid device token",
})
return
}
// 获取设备ID用于后续处理
deviceID := device.ID
// 获取Agent名称
agentName := c.GetHeader("X-Agent-Name")
if agentName == "" {
// 如果没有提供名称,使用设备名称
agentName = device.Name
}
// 解析请求体
var req MetricsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request body",
})
return
}
// 更新设备状态为active
if err := deviceStorage.UpdateDeviceStatus(deviceID, "active"); err != nil {
// 只记录警告,不影响指标处理
log.Printf("Warning: Failed to update device status: %v", err)
}
// 创建基础标签包含Agent名称
baseTags := map[string]string{
"agent_name": agentName,
}
// 写入CPU指标
if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "cpu", req.CPU, baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write CPU metrics: %v", err)
}
// 写入内存指标
if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "memory", req.Memory, baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write memory metrics: %v", err)
}
// 写入磁盘指标,支持多个挂载点
for mountpoint, usage := range req.Disk {
// 为每个挂载点创建标签,包含基础标签和挂载点
tags := make(map[string]string)
// 复制基础标签
for k, v := range baseTags {
tags[k] = v
}
// 添加挂载点标签
tags["mountpoint"] = mountpoint
// 写入磁盘指标
if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "disk", usage, tags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write disk metrics for mountpoint %s: %v", mountpoint, err)
}
}
// 写入网络发送指标
if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "network_sent", float64(req.Network.BytesSent), baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write network sent metrics: %v", err)
}
// 写入网络接收指标
if err := globalStorage.WriteMetric(c.Request.Context(), deviceID, "network_received", float64(req.Network.BytesReceived), baseTags); err != nil {
// 只记录警告,不影响后续指标处理
log.Printf("Warning: Failed to write network received metrics: %v", err)
}
// 广播指标更新消息
metrics := map[string]interface{}{
"cpu": req.CPU,
"memory": req.Memory,
"disk": req.Disk,
"network": map[string]uint64{
"bytes_sent": req.Network.BytesSent,
"bytes_received": req.Network.BytesReceived,
},
}
broadcastMetricsUpdate(deviceID, metrics)
// 返回成功响应
c.JSON(http.StatusOK, gin.H{
"message": "Metrics received successfully",
})
}
// 全局存储实例(简化处理,实际应该使用依赖注入)
var globalStorage *storage.Storage
var deviceStorage device.Storage
// SetStorage 设置全局存储实例
func SetStorage(s *storage.Storage) {
globalStorage = s
}
// SetDeviceStorage 设置设备存储实例
func SetDeviceStorage(s device.Storage) {
deviceStorage = s
}
// 生成安全的设备令牌
func generateDeviceToken() string {
// 生成32字节的随机数据
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
// 如果生成随机数失败使用时间戳和PID作为备选方案
return hex.EncodeToString([]byte(time.Now().String() + "-" + string(os.Getpid())))
}
// 转换为32位的十六进制字符串
return hex.EncodeToString(bytes)
}
// WebSocket相关配置
var (
// 升级器用于将HTTP连接升级为WebSocket连接
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 允许所有来源,生产环境应该限制
},
}
// 客户端连接管理
clients = make(map[*websocket.Conn]bool)
clientsLock sync.Mutex
// 消息广播通道
broadcast = make(chan map[string]interface{})
)
// 启动WebSocket消息处理
func init() {
go handleMessages()
}
// 处理WebSocket消息
func handleMessages() {
for {
// 从广播通道接收消息
message := <-broadcast
// 序列化消息
jsonMessage, err := json.Marshal(message)
if err != nil {
continue
}
// 广播消息给所有客户端
clientsLock.Lock()
for client := range clients {
if err := client.WriteMessage(websocket.TextMessage, jsonMessage); err != nil {
// 连接出错,关闭连接并从客户端列表中移除
client.Close()
delete(clients, client)
}
}
clientsLock.Unlock()
}
}
// WebSocket端点用于客户端连接
func WebSocketHandler(c *gin.Context) {
// 升级HTTP连接为WebSocket连接
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
// 将客户端添加到客户端列表
clientsLock.Lock()
clients[conn] = true
clientsLock.Unlock()
// 处理客户端消息(这里我们只发送消息,不处理接收的消息)
for {
// 读取消息(如果客户端发送消息,我们不处理,直接忽略)
_, _, err := conn.ReadMessage()
if err != nil {
// 连接出错,从客户端列表中移除
clientsLock.Lock()
delete(clients, conn)
clientsLock.Unlock()
break
}
}
}
// 广播指标更新消息
func broadcastMetricsUpdate(deviceID string, metrics map[string]interface{}) {
message := map[string]interface{}{
"type": "metrics_update",
"device_id": deviceID,
"metrics": metrics,
}
broadcast <- message
}
// GetCPUMetrics 获取CPU指标
func GetCPUMetrics(c *gin.Context) {
// 获取查询参数
deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
startTime := c.DefaultQuery("start_time", "-24h")
endTime := c.DefaultQuery("end_time", "now()")
aggregation := c.DefaultQuery("aggregation", "average")
interval := c.DefaultQuery("interval", "10s") // 添加interval参数默认10秒
// 查询数据
points, err := globalStorage.QueryMetrics(context.Background(), deviceID, "cpu", startTime, endTime)
if err != nil {
// 只记录警告,返回空数据
log.Printf("Warning: Failed to query CPU metrics: %v", err)
c.JSON(http.StatusOK, gin.H{
"data": []MetricData{},
})
return
}
// 处理数据传递interval、startTime和endTime参数
processedData := ProcessMetrics(points, aggregation, interval, startTime, endTime)
c.JSON(http.StatusOK, gin.H{
"data": processedData,
})
}
// GetMemoryMetrics 获取内存指标
func GetMemoryMetrics(c *gin.Context) {
// 获取查询参数
deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
startTime := c.DefaultQuery("start_time", "-24h")
endTime := c.DefaultQuery("end_time", "now()")
aggregation := c.DefaultQuery("aggregation", "average")
interval := c.DefaultQuery("interval", "10s") // 添加interval参数默认10秒
// 查询数据
points, err := globalStorage.QueryMetrics(context.Background(), deviceID, "memory", startTime, endTime)
if err != nil {
// 只记录警告,返回空数据
log.Printf("Warning: Failed to query memory metrics: %v", err)
c.JSON(http.StatusOK, gin.H{
"data": []MetricData{},
})
return
}
// 处理数据传递interval、startTime和endTime参数
processedData := ProcessMetrics(points, aggregation, interval, startTime, endTime)
c.JSON(http.StatusOK, gin.H{
"data": processedData,
})
}
// GetDiskMetrics 获取磁盘指标
func GetDiskMetrics(c *gin.Context) {
// 获取查询参数
deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
startTime := c.DefaultQuery("start_time", "-24h")
endTime := c.DefaultQuery("end_time", "now()")
aggregation := c.DefaultQuery("aggregation", "average")
interval := c.DefaultQuery("interval", "10s") // 添加interval参数默认10秒
// 查询数据
points, err := globalStorage.QueryMetrics(context.Background(), deviceID, "disk", startTime, endTime)
if err != nil {
// 只记录警告,返回空数据
log.Printf("Warning: Failed to query disk metrics: %v", err)
c.JSON(http.StatusOK, gin.H{
"data": map[string][]MetricData{},
})
return
}
// 按挂载点分组
mountpointData := make(map[string][]storage.MetricPoint)
for _, point := range points {
// 获取挂载点标签
mountpoint := "unknown"
if point.Tags != nil && point.Tags["mountpoint"] != "" {
mountpoint = point.Tags["mountpoint"]
}
mountpointData[mountpoint] = append(mountpointData[mountpoint], point)
}
// 处理数据,为每个挂载点创建独立的数据集
result := make(map[string][]MetricData)
for mountpoint, mountpointPoints := range mountpointData {
processedData := ProcessMetrics(mountpointPoints, aggregation, interval, startTime, endTime)
result[mountpoint] = processedData
}
c.JSON(http.StatusOK, gin.H{
"data": result,
})
}
// GetNetworkMetrics 获取网络指标
func GetNetworkMetrics(c *gin.Context) {
// 获取查询参数
deviceID := c.Query("device_id") // 不使用默认值,空值表示查询所有设备
startTime := c.DefaultQuery("start_time", "-24h")
endTime := c.DefaultQuery("end_time", "now()")
aggregation := c.DefaultQuery("aggregation", "average")
interval := c.DefaultQuery("interval", "10s") // 添加interval参数默认10秒
// 查询发送和接收的网络指标
sentPoints, err1 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_sent", startTime, endTime)
receivedPoints, err2 := globalStorage.QueryMetrics(context.Background(), deviceID, "network_received", startTime, endTime)
// 处理错误
if err1 != nil {
log.Printf("Warning: Failed to query network sent metrics: %v", err1)
sentPoints = []storage.MetricPoint{}
}
if err2 != nil {
log.Printf("Warning: Failed to query network received metrics: %v", err2)
receivedPoints = []storage.MetricPoint{}
}
// 处理数据传递interval、startTime和endTime参数
processedSentData := ProcessMetrics(sentPoints, aggregation, interval, startTime, endTime)
processedReceivedData := ProcessMetrics(receivedPoints, aggregation, interval, startTime, endTime)
c.JSON(http.StatusOK, gin.H{
"data": map[string][]MetricData{
"sent": processedSentData,
"received": processedReceivedData,
},
})
}
// GetDevices 获取设备列表
func GetDevices(c *gin.Context) {
// 从设备存储获取所有设备
allDevices := deviceStorage.GetDevices()
// 转换为前端需要的格式
deviceList := make([]map[string]string, 0, len(allDevices))
for _, device := range allDevices {
// 只返回活跃设备
if device.Status == "active" {
deviceList = append(deviceList, map[string]string{
"id": device.ID,
"name": device.Name,
})
}
}
c.JSON(http.StatusOK, gin.H{
"devices": deviceList,
})
}
// GetDeviceStatus 获取设备状态
func GetDeviceStatus(c *gin.Context) {
// 获取设备ID
deviceID := c.Param("id")
if deviceID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Device ID is required",
})
return
}
// 从设备存储获取设备信息
device, ok := deviceStorage.GetDevice(deviceID)
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": "Device not found",
})
return
}
// 查询设备监控数据
_, status, err := globalStorage.QueryDeviceStatus(context.Background(), deviceID)
if err != nil {
// 如果查询监控数据失败,返回设备基本信息和空状态
c.JSON(http.StatusOK, gin.H{
"device_id": deviceID,
"name": device.Name,
"status": make(map[string]float64),
})
return
}
c.JSON(http.StatusOK, gin.H{
"device_id": deviceID,
"name": device.Name,
"status": status,
})
}
// GetAllDeviceStatus 获取所有设备状态
func GetAllDeviceStatus(c *gin.Context) {
// 从设备存储获取所有设备
allDevices := deviceStorage.GetDevices()
// 查询每个设备的状态
result := make([]map[string]interface{}, 0, len(allDevices))
for _, device := range allDevices {
// 查询设备监控数据
_, status, _ := globalStorage.QueryDeviceStatus(context.Background(), device.ID)
// 总是返回设备信息,无论是否有监控数据
result = append(result, map[string]interface{}{
"device_id": device.ID,
"name": device.Name,
"status": status,
})
}
c.JSON(http.StatusOK, gin.H{
"devices": result,
})
}
// AddDevice 添加设备
func AddDevice(c *gin.Context) {
var req struct {
ID string `json:"id" binding:"required"`
Name string `json:"name" binding:"required"`
IP string `json:"ip"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request: " + err.Error(),
})
return
}
// 生成设备令牌
token := generateDeviceToken()
newDevice := device.Device{
ID: req.ID,
Name: req.Name,
IP: req.IP,
Token: token,
Status: "inactive",
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
if err := deviceStorage.AddDevice(newDevice); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to add device: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Device added successfully",
"device": newDevice,
})
}
// DeleteDevice 删除设备
func DeleteDevice(c *gin.Context) {
deviceID := c.Param("id")
if deviceID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Device ID is required",
})
return
}
if err := deviceStorage.DeleteDevice(deviceID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to delete device: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Device deleted successfully",
})
}
// UpdateDevice 更新设备
func UpdateDevice(c *gin.Context) {
deviceID := c.Param("id")
if deviceID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Device ID is required",
})
return
}
var req struct {
Name string `json:"name"`
IP string `json:"ip"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request: " + err.Error(),
})
return
}
// 获取现有设备
existingDevice, ok := deviceStorage.GetDevice(deviceID)
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": "Device not found",
})
return
}
// 更新设备信息保留原有token
if req.Name != "" {
existingDevice.Name = req.Name
}
if req.IP != "" {
existingDevice.IP = req.IP
}
if req.Status != "" {
existingDevice.Status = req.Status
}
existingDevice.UpdatedAt = time.Now().Unix()
if err := deviceStorage.UpdateDevice(existingDevice); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to update device: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Device updated successfully",
"device": existingDevice,
})
}
// GetDevice 获取单个设备
func GetDevice(c *gin.Context) {
deviceID := c.Param("id")
if deviceID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Device ID is required",
})
return
}
device, ok := deviceStorage.GetDevice(deviceID)
if !ok {
c.JSON(http.StatusNotFound, gin.H{
"error": "Device not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"device": device,
})
}
// GetAllDevices 获取所有设备
func GetAllDevices(c *gin.Context) {
devices := deviceStorage.GetDevices()
c.JSON(http.StatusOK, gin.H{
"devices": devices,
})
}