feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现
F1 告警静默规则 [done]: - 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式) - 告警引擎 isSilenced 集成 (alert_engine.go) - 前端 SilenceRuleTab + SilenceDialog - 路由 /alerts/silences 已注册 F2 审计日志导出 [done]: - 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM) - 支持 module/action/username/startTime/endTime 筛选 - 导出限制 10000 条防溢出,自动记录审计日志 - JSON 格式向后兼容 (format=json) - 前端 exportLogs 传递查询参数 同时包含累积的前期工作: - 目录重构 internal/ -> controller/model/router (v0.55-v0.57) - 后端分层架构 dto/repository/service/utils - 审计模块全功能 (终端审计/操作审计/登录日志) - asciicast v2 录制格式 - SFTP 文件浏览器 - StatStrip 紧凑统计条 - 在线会话自动刷新 - LiveMonitor WebSocket 诊断修复 验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
1 parent
e57718998a
commit
77c234b060
314 files changed
+10050
-4065
No files matched your search
@@ -0,0 +1,284 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ezops/model"
|
||||
"ezops/pkg/zabbix"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ZabbixProxyHandler Zabbix 代理处理器 — 将 Zabbix API 数据转为前端面板可用格式
|
||||
type ZabbixProxyHandler struct{}
|
||||
|
||||
func NewZabbixProxyHandler() *ZabbixProxyHandler {
|
||||
return &ZabbixProxyHandler{}
|
||||
}
|
||||
|
||||
// ZabbixHostSummary Zabbix 主机汇总(前端面板数据)
|
||||
type ZabbixHostSummary struct {
|
||||
HostID string `json:"hostid"`
|
||||
Hostname string `json:"hostname"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
Available string `json:"available"` // 0=未知 1=可用 2=不可用
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
DiskUsage float64 `json:"disk_usage"`
|
||||
Load5 float64 `json:"load5"`
|
||||
NetInBPS float64 `json:"net_in_bps"`
|
||||
NetOutBPS float64 `json:"net_out_bps"`
|
||||
ProblemCount int `json:"problem_count"`
|
||||
Uptime int64 `json:"uptime_days"` // 天
|
||||
CPUCount int `json:"cpu_count"`
|
||||
TotalMemory float64 `json:"total_memory_gb"`
|
||||
}
|
||||
|
||||
// ZabbixConnectReq 连接 Zabbix 请求
|
||||
type ZabbixConnectReq struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// ZabbixConnect 连接 Zabbix 并返回主机摘要
|
||||
// POST /api/v1/zabbix/connect
|
||||
func (h *ZabbixProxyHandler) Connect(c *gin.Context) {
|
||||
var req ZabbixConnectReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
client := zabbix.NewClient(req.URL, req.Username, req.Password)
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Auth(ctx); err != nil {
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "Zabbix 认证失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 Zabbix 版本
|
||||
version, _ := client.GetVersion(ctx)
|
||||
|
||||
// 获取主机列表(含接口和组信息)
|
||||
hosts, err := client.GetHosts(ctx, &zabbix.GetHostsParams{
|
||||
Status: 0, // 只获取被监控的主机
|
||||
SelectInterfaces: []string{"interfaceid", "hostid", "main", "type", "useip", "ip", "dns", "port"},
|
||||
SelectGroups: []string{"groupid", "name"},
|
||||
Output: []string{"hostid", "host", "name", "status", "available", "description"},
|
||||
})
|
||||
if err != nil {
|
||||
client.Logout(ctx)
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "获取 Zabbix 主机失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 为每台主机查询监控项
|
||||
var summaries []ZabbixHostSummary
|
||||
itemKeys := []string{
|
||||
"system.cpu.util[,idle]", // CPU 空闲率
|
||||
"system.cpu.util", // CPU 使用率(备用)
|
||||
"vm.memory.size[total]", // 总内存 bytes
|
||||
"vm.memory.size[available]", // 可用内存 bytes
|
||||
"vfs.fs.size[/,total]", // 总磁盘 bytes
|
||||
"vfs.fs.size[/,used]", // 已用磁盘 bytes
|
||||
"system.cpu.load[all,avg5]", // 5分钟负载
|
||||
"net.if.in[ens18]", // 网络流入
|
||||
"net.if.out[ens18]", // 网络流出
|
||||
"system.cpu.num", // CPU 数量
|
||||
"system.uptime", // 运行时间
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
summary := ZabbixHostSummary{
|
||||
HostID: host.HostID,
|
||||
Hostname: host.Host,
|
||||
Name: host.Name,
|
||||
Available: host.Available,
|
||||
}
|
||||
|
||||
// 获取 IP
|
||||
for _, iface := range host.Interfaces {
|
||||
if iface.Main == "1" && summary.IP == "" {
|
||||
if iface.UseIP == "1" && iface.IP != "" {
|
||||
summary.IP = iface.IP
|
||||
} else if iface.DNS != "" {
|
||||
summary.IP = iface.DNS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取监控项
|
||||
items, err := client.GetItems(ctx, &zabbix.GetItemsParams{
|
||||
HostIDs: []string{host.HostID},
|
||||
Filter: map[string]interface{}{"key_": itemKeys},
|
||||
Output: []string{"itemid", "hostid", "name", "key_", "lastvalue", "units"},
|
||||
SortField: []string{"name"},
|
||||
})
|
||||
if err != nil {
|
||||
summaries = append(summaries, summary)
|
||||
continue
|
||||
}
|
||||
|
||||
var totalMemBytes, availMemBytes, totalDiskBytes, usedDiskBytes float64
|
||||
for _, item := range items {
|
||||
val, _ := strconv.ParseFloat(item.LastValue, 64)
|
||||
switch item.Key {
|
||||
case "system.cpu.util[,idle]":
|
||||
summary.CPUUsage = math.Round((100-val)*100) / 100
|
||||
case "system.cpu.util":
|
||||
if summary.CPUUsage == 0 {
|
||||
summary.CPUUsage = math.Round(val*100) / 100
|
||||
}
|
||||
case "vm.memory.size[total]":
|
||||
totalMemBytes = val
|
||||
case "vm.memory.size[available]":
|
||||
availMemBytes = val
|
||||
case "vfs.fs.size[/,total]":
|
||||
totalDiskBytes = val
|
||||
case "vfs.fs.size[/,used]":
|
||||
usedDiskBytes = val
|
||||
case "system.cpu.load[all,avg5]":
|
||||
summary.Load5 = math.Round(val*100) / 100
|
||||
case "net.if.in[ens18]":
|
||||
summary.NetInBPS = math.Round(val*100) / 100
|
||||
case "net.if.out[ens18]":
|
||||
summary.NetOutBPS = math.Round(val*100) / 100
|
||||
case "system.cpu.num":
|
||||
summary.CPUCount = int(val)
|
||||
case "system.uptime":
|
||||
summary.Uptime = int64(val) / 86400 // seconds → days
|
||||
}
|
||||
}
|
||||
|
||||
// 计算内存使用率
|
||||
if totalMemBytes > 0 {
|
||||
summary.TotalMemory = math.Round(totalMemBytes/1024/1024/1024*100) / 100 // GB
|
||||
if availMemBytes > 0 {
|
||||
summary.MemoryUsage = math.Round((totalMemBytes-availMemBytes)/totalMemBytes*10000) / 100
|
||||
}
|
||||
}
|
||||
// 计算磁盘使用率
|
||||
if totalDiskBytes > 0 && usedDiskBytes > 0 {
|
||||
summary.DiskUsage = math.Round(usedDiskBytes/totalDiskBytes*10000) / 100
|
||||
}
|
||||
|
||||
// 获取问题触发器
|
||||
triggers, _ := client.GetTriggersByHost(ctx, host.HostID, true)
|
||||
summary.ProblemCount = len(triggers)
|
||||
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
|
||||
client.Logout(ctx)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
"version": version,
|
||||
"hosts": summaries,
|
||||
"total": len(summaries),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ZabbixHostDetail Zabbix 单主机详情
|
||||
// GET /api/v1/zabbix/host/:hostid
|
||||
func (h *ZabbixProxyHandler) GetHostDetail(c *gin.Context) {
|
||||
hostID := c.Param("hostid")
|
||||
if hostID == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "缺少主机ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// 从 query 参数获取连接信息
|
||||
url := c.Query("url")
|
||||
username := c.Query("username")
|
||||
password := c.Query("password")
|
||||
if url == "" || username == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "缺少 Zabbix 连接信息"})
|
||||
return
|
||||
}
|
||||
|
||||
client := zabbix.NewClient(url, username, password)
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Auth(ctx); err != nil {
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "Zabbix 认证失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
defer client.Logout(ctx)
|
||||
|
||||
// 获取主机详情
|
||||
host, err := client.GetHostByID(ctx, hostID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "获取主机失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有监控项
|
||||
items, err := client.GetItemsByHost(ctx, hostID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, model.Response{Code: 502, Message: "获取监控项失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取问题触发器
|
||||
triggers, _ := client.GetTriggersByHost(ctx, hostID, true)
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: gin.H{
|
||||
"host": host,
|
||||
"items": items,
|
||||
"triggers": triggers,
|
||||
"problems": len(triggers),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// formatBps 格式化带宽
|
||||
func formatBps(bps float64) string {
|
||||
if bps == 0 {
|
||||
return "—"
|
||||
}
|
||||
units := []string{"B/s", "KB/s", "MB/s", "GB/s", "TB/s"}
|
||||
unitIdx := 0
|
||||
val := bps
|
||||
for val >= 1024 && unitIdx < len(units)-1 {
|
||||
val /= 1024
|
||||
unitIdx++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %s", val, units[unitIdx])
|
||||
}
|
||||
|
||||
// parseZabbixItemSummary 从 items 中提取汇总信息的辅助函数(供内部使用)
|
||||
func parseZabbixItemSummary(items []zabbix.Item) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for _, item := range items {
|
||||
key := item.Key
|
||||
// 标准化 key 名称
|
||||
if strings.Contains(item.Name, "CPU utilization") || key == "system.cpu.util" {
|
||||
result["cpu"] = item.LastValue
|
||||
}
|
||||
if strings.Contains(item.Name, "Memory utilization") || key == "vm.memory.size[pavailable]" {
|
||||
result["memory"] = item.LastValue
|
||||
}
|
||||
if strings.Contains(item.Name, "Space utilization") || key == "/Space utilization/" {
|
||||
result["disk"] = item.LastValue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user