Files
Meshray-Manager/internal/api/handler/dashboard.go
T
zkcoi e9ca2f7d70 fix: rename manager module to git.zkcoi.com/zkcoi/Meshray-Manager
- go.mod module path matches repo zkcoi/Meshray-Manager (case-distinct from zkcoi/Meshray/core)
- rewrite internal imports meshray/{internal,web,pkg} -> Meshray-Manager/... (core refs kept)
- sync README.md / install.sh repo URLs; add CHANGELOG entry
2026-07-15 16:25:46 +08:00

377 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
"git.zkcoi.com/zkcoi/Meshray-Manager/internal/model"
sqlite "git.zkcoi.com/zkcoi/Meshray-Manager/internal/store/sqlite"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/v4/cpu"
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/host"
"github.com/shirou/gopsutil/v4/mem"
gopsutilNet "github.com/shirou/gopsutil/v4/net"
"go.uber.org/zap"
)
// DashboardHandler Dashboard Handler
type DashboardHandler struct {
logger *zap.Logger
store *sqlite.Store
}
// NewDashboardHandler 创建 Dashboard Handler
func NewDashboardHandler(store *sqlite.Store, logger *zap.Logger) *DashboardHandler {
return &DashboardHandler{
logger: logger,
store: store,
}
}
// GetStats 获取统计数据
func (h *DashboardHandler) GetStats(c *gin.Context) {
var deviceCount, networkCount, onlineCount int64
// 统计设备数量
h.store.DB().Model(&model.Device{}).Count(&deviceCount)
// 统计网络数量
h.store.DB().Model(&model.Network{}).Count(&networkCount)
// 统计在线设备数量
h.store.DB().Model(&model.Device{}).Where("status = ?", "online").Count(&onlineCount)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"total_devices": deviceCount,
"total_networks": networkCount,
"online_devices": onlineCount,
},
})
}
// GetRecentLogs 获取最近日志
func (h *DashboardHandler) GetRecentLogs(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "10")
limit := 10
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
// 从数据库查询最近的 AuditLog
var logs []model.AuditLog
if err := h.store.DB().
Order("created_at DESC").
Limit(limit).
Find(&logs).Error; err != nil {
h.logger.Error("查询日志失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "查询日志失败",
})
return
}
// 转换为前端格式
type LogEntry struct {
ID uint64 `json:"id"`
Timestamp string `json:"timestamp"`
Message string `json:"message"`
Action string `json:"action"`
OperatorIP string `json:"operator_ip"`
Type string `json:"type"` // info, success, warning, error
}
logEntries := make([]LogEntry, 0, len(logs))
for _, log := range logs {
// 从 Detail 中提取信息构建 Message
message := log.Action
if log.Detail != "" {
message = log.Detail
}
logEntries = append(logEntries, LogEntry{
ID: uint64(log.ID),
Timestamp: log.CreatedAt.Format(time.RFC3339),
Message: message,
Action: log.Action,
OperatorIP: log.OperatorIP,
Type: "info", // AuditLog 默认类型
})
}
c.JSON(http.StatusOK, gin.H{
"data": logEntries,
})
}
// GetSystemInfo 获取系统信息
func (h *DashboardHandler) GetSystemInfo(c *gin.Context) {
// 获取主机信息
hostInfo, err := host.Info()
if err != nil {
h.logger.Warn("获取主机信息失败", zap.Error(err))
hostInfo = &host.InfoStat{} // 使用空对象避免 nil 指针
}
// 获取 CPU 信息
cpuPercent, _ := cpu.Percent(0, false)
cpuUsage := float64(0)
if len(cpuPercent) > 0 {
cpuUsage = cpuPercent[0]
}
// 获取内存信息
memInfo, _ := mem.VirtualMemory()
// 获取磁盘信息
diskInfo, _ := disk.Usage("/")
// Windows 下使用 C:\
if runtime.GOOS == "windows" {
diskInfo, _ = disk.Usage("C:/")
}
// 获取网络 IO
netIO, _ := gopsutilNet.IOCounters(false)
// 获取本机 IPIPv4 + IPv6
ipv4, ipv6 := getLocalIPs()
ip := ipv4 // 默认返回 IPv4
if ipv4 == "unknown" && ipv6 != "unknown" {
ip = ipv6 // 如果只有 IPv6,则返回 IPv6
}
// 计算运行时长(小时)
uptimeHours := float64(hostInfo.Uptime) / 3600.0
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
// 基础信息
"hostname": hostInfo.Hostname,
"os": hostInfo.OS,
"platform": hostInfo.Platform,
"os_version": hostInfo.PlatformVersion,
"kernel": hostInfo.KernelVersion,
"arch": runtime.GOARCH,
// CPU
"cpu_count": runtime.NumCPU(),
"cpu_usage": cpuUsage,
"cpu_model": getCPUModel(), // 从 cpu.Info() 获取
// 内存
"memory_total": int(memInfo.Total / 1024 / 1024), // MB
"memory_used": int(memInfo.Used / 1024 / 1024), // MB
"memory_percent": memInfo.UsedPercent,
"memory_alloc": int(runtime.MemStats{}.Alloc / 1024 / 1024), // Go 内存
// 磁盘
"disk_total": int(diskInfo.Total / 1024 / 1024 / 1024), // GB
"disk_used": int(diskInfo.Used / 1024 / 1024 / 1024), // GB
"disk_percent": diskInfo.UsedPercent,
// 网络
"net_sent": netIO[0].BytesSent,
"net_recv": netIO[0].BytesRecv,
// 运行时长
"boot_time": formatBootTime(hostInfo),
"uptime": uptimeHours,
// IP 地址(同时返回 IPv4 和 IPv6)
"ip": ip, // 保持向后兼容
"ipv4": ipv4, // IPv4 地址
"ipv6": ipv6, // IPv6 地址
},
})
}
// getLocalIPs 获取本机 IPv4 和 IPv6 地址
func getLocalIPs() (ipv4, ipv6 string) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "unknown", "unknown"
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
// 检查是否为公网地址(可选,目前保留私有地址)
if ipNet.IP.To4() != nil {
// 优先选择非链路本地地址
if ipv4 == "" || !ipNet.IP.IsLinkLocalUnicast() {
ipv4 = ipNet.IP.String()
}
} else if ipNet.IP.To16() != nil {
// IPv6
if ipv6 == "" || !ipNet.IP.IsLinkLocalUnicast() {
ipv6 = ipNet.IP.String()
}
}
}
}
if ipv4 == "" {
ipv4 = "unknown"
}
if ipv6 == "" {
ipv6 = "unknown"
}
return ipv4, ipv6
}
// formatBootTime 格式化启动时间
func formatBootTime(hostInfo *host.InfoStat) string {
if hostInfo == nil || hostInfo.BootTime == 0 {
return "unknown"
}
return time.Unix(int64(hostInfo.BootTime), 0).Format("2006-01-02 15:04:05")
}
// GetLinkDistribution 获取链路分布
func (h *DashboardHandler) GetLinkDistribution(c *gin.Context) {
// 查询所有网络
var networks []model.Network
if err := h.store.DB().Find(&networks).Error; err != nil {
h.logger.Error("查询网络失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "查询网络失败",
})
return
}
// 构建链路分布数据
distribution := make([]gin.H, 0, len(networks))
totalP2P := 0
totalRelay := 0
for _, network := range networks {
// 查询该网络下的所有设备
var devices []model.Device
if err := h.store.DB().Where("network_id = ?", network.ID).Find(&devices).Error; err != nil {
h.logger.Warn("查询设备失败", zap.Uint64("network_id", network.ID), zap.Error(err))
continue
}
// 统计 P2P 和 Relay 连接
p2pCount := 0
relayCount := 0
for _, device := range devices {
// 根据 Endpoint 判断是 P2P 还是 Relay
// 如果 Endpoint 为空或为内网地址,认为是 P2P
// 如果 Endpoint 包含中继端口(如 53493),认为是 Relay
if device.Endpoint == "" {
p2pCount++
} else if isRelayEndpoint(device.Endpoint) {
relayCount++
} else {
p2pCount++
}
}
totalP2P += p2pCount
totalRelay += relayCount
distribution = append(distribution, gin.H{
"network_id": network.ID,
"network_name": network.Name,
"p2p_count": p2pCount,
"relay_count": relayCount,
"total_peers": len(devices),
"mode": network.Mode, // native / userspace
})
}
// 返回总体统计和详细分布
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"summary": gin.H{
"total_p2p": totalP2P,
"total_relay": totalRelay,
"total": totalP2P + totalRelay,
"p2p_percent": calculatePercent(totalP2P, totalP2P+totalRelay),
},
"by_network": distribution,
},
})
}
// isRelayEndpoint 判断是否为中继端点
func isRelayEndpoint(endpoint string) bool {
// 简单的启发式判断:如果端口在常见 TURN 端口范围内
_, port, err := net.SplitHostPort(endpoint)
if err != nil {
return false
}
// 常见 TURN/Relay 端口
relayPorts := []string{"53493", "53494", "53495", "3478", "5349"}
for _, p := range relayPorts {
if port == p {
return true
}
}
return false
}
// calculatePercent 计算百分比
func calculatePercent(part, total int) float64 {
if total == 0 {
return 0
}
return float64(part) / float64(total) * 100
}
// ClearLogs 清理日志文件
func (h *DashboardHandler) ClearLogs(c *gin.Context) {
logPath := "./logs"
// 检查日志目录是否存在
if _, err := os.Stat(logPath); os.IsNotExist(err) {
c.JSON(http.StatusOK, gin.H{
"message": "日志目录不存在",
})
return
}
// 读取所有日志文件
files, err := filepath.Glob(filepath.Join(logPath, "*.log"))
if err != nil {
h.logger.Error("读取日志文件失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "读取日志文件失败",
})
return
}
// 删除所有日志文件
deletedCount := 0
for _, file := range files {
if err := os.Remove(file); err != nil {
h.logger.Warn("删除日志文件失败", zap.String("file", file), zap.Error(err))
continue
}
deletedCount++
}
h.logger.Info("清理日志完成", zap.Int("deleted", deletedCount))
c.JSON(http.StatusOK, gin.H{
"message": "日志清理完成",
"deleted": deletedCount,
})
}
// getCPUModel 获取 CPU 型号
func getCPUModel() string {
cpuInfos, err := cpu.Info()
if err != nil || len(cpuInfos) == 0 {
return "Unknown"
}
return cpuInfos[0].ModelName
}