Initial commit
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.zkcoi.com/zkcoi/meshray/internal/model"
|
||||
sqlite "git.zkcoi.com/zkcoi/meshray/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)
|
||||
|
||||
// 获取本机 IP(IPv4 + 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
|
||||
}
|
||||
Reference in New Issue
Block a user