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
+340
@@ -0,0 +1,340 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"ezops/router"
|
||||
"ezops/config"
|
||||
"ezops/seed"
|
||||
"ezops/pkg/database"
|
||||
"ezops/pkg/logger"
|
||||
ezredis "ezops/pkg/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 版本信息(通过编译时 ldflags 注入)// Logan.Li
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildTime = "unknown"
|
||||
GitCommit = "unknown"
|
||||
)
|
||||
|
||||
// CLI 参数定义 // Logan.Li
|
||||
var (
|
||||
flagPort = flag.String("port", "", "服务端口 (默认: 8080)")
|
||||
flagConfig = flag.String("config", "", "配置文件路径 (默认: config.yaml)")
|
||||
flagMode = flag.String("mode", "", "运行模式: development, production, release")
|
||||
flagLogLevel = flag.String("log-level", "", "日志级别: debug, info, warn, error")
|
||||
|
||||
// 数据库参数
|
||||
flagDBDriver = flag.String("db-driver", "", "数据库驱动: sqlite, mysql, postgres (默认: sqlite)")
|
||||
flagDBHost = flag.String("db-host", "", "数据库主机")
|
||||
flagDBPort = flag.Int("db-port", 0, "数据库端口")
|
||||
flagDBUser = flag.String("db-user", "", "数据库用户名")
|
||||
flagDBPassword = flag.String("db-password", "", "数据库密码")
|
||||
flagDBName = flag.String("db-name", "", "数据库名 / SQLite 文件路径")
|
||||
flagDBCharset = flag.String("db-charset", "", "数据库字符集")
|
||||
|
||||
// Redis 参数
|
||||
flagRedisHost = flag.String("redis-host", "", "Redis 主机")
|
||||
flagRedisPort = flag.Int("redis-port", 0, "Redis 端口")
|
||||
flagRedisPassword = flag.String("redis-password", "", "Redis 密码")
|
||||
flagRedisDB = flag.Int("redis-db", 0, "Redis 数据库编号")
|
||||
flagRedisDriver = flag.String("redis-driver", "", "Redis 驱动: redis (外部Redis), redka (内嵌,需CGO)")
|
||||
|
||||
// JWT 参数
|
||||
flagJWTSecret = flag.String("jwt-secret", "", "JWT 密钥")
|
||||
flagJWTExpire = flag.Int("jwt-expire", 0, "JWT 过期时间 (小时)")
|
||||
|
||||
// 显示版本
|
||||
flagVersion = flag.Bool("version", false, "显示版本信息")
|
||||
)
|
||||
|
||||
// @title AIOps运维平台 API
|
||||
// @version 1.0
|
||||
// @description AIOps智能运维平台后端API文档
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.swagger.io/support
|
||||
// @contact.email support@swagger.io
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /api/v1
|
||||
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
func main() {
|
||||
// 解析 CLI 参数 // Logan.Li
|
||||
flag.Parse()
|
||||
|
||||
// 显示版本信息 // Logan.Li
|
||||
if *flagVersion {
|
||||
fmt.Printf("AIOps 智能运维平台\n")
|
||||
fmt.Printf(" 版本: %s\n", Version)
|
||||
fmt.Printf(" 构建: %s\n", BuildTime)
|
||||
fmt.Printf(" 提交: %s\n", GitCommit)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 加载配置 // Logan.Li
|
||||
cfg := loadConfig()
|
||||
|
||||
// 初始化日志
|
||||
log := logger.New(cfg.LogLevel)
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("AIOps 启动中",
|
||||
zap.String("version", Version),
|
||||
zap.String("mode", cfg.Mode),
|
||||
zap.String("port", cfg.Port),
|
||||
)
|
||||
|
||||
// 设置Gin模式
|
||||
if cfg.Mode == "production" || cfg.Mode == "release" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
db, err := database.New(cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatal("数据库连接失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := database.AutoMigrate(db); err != nil {
|
||||
log.Fatal("数据库迁移失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 初始化种子数据
|
||||
seed.SeedData(db)
|
||||
|
||||
// 初始化缓存(Redka 内嵌 / Redis 外部)// Logan.Li
|
||||
cache, err := ezredis.New(ezredis.Config{
|
||||
Driver: cfg.Redis.Driver, // 从配置读取 driver
|
||||
Host: cfg.Redis.Host,
|
||||
Port: cfg.Redis.Port,
|
||||
Password: cfg.Redis.Password,
|
||||
DB: cfg.Redis.DB,
|
||||
DataDir: "data/redka",
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("缓存初始化失败,继续启动", zap.Error(err))
|
||||
cache = nil
|
||||
} else {
|
||||
log.Info("缓存已就绪", zap.String("driver", cfg.Redis.Driver))
|
||||
defer cache.Close()
|
||||
}
|
||||
|
||||
// 创建Gin实例
|
||||
r := gin.New()
|
||||
|
||||
// 中间件
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(logger.GinLogger(log))
|
||||
r.Use(CORS())
|
||||
|
||||
// 版本信息 API
|
||||
r.GET("/version", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": Version,
|
||||
"build_time": BuildTime,
|
||||
"git_commit": GitCommit,
|
||||
"mode": cfg.Mode,
|
||||
"db_driver": cfg.Database.Driver,
|
||||
})
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
router.Register(r, db, log, cfg, cache)
|
||||
|
||||
// 定期清理旧的 agent 上报数据(保留 7 天,防止表膨胀影响查询性能)
|
||||
go func() {
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
result := db.Exec("DELETE FROM monitor_agent_reports WHERE report_time < DATE_SUB(NOW(), INTERVAL 7 DAY)")
|
||||
if result.RowsAffected > 0 {
|
||||
log.Info("清理过期的 agent 上报数据", zap.Int64("rows", result.RowsAffected))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 前端静态文件服务(go:embed 模式,二进制部署用) // Logan.Li
|
||||
setupFrontend(r)
|
||||
|
||||
// 创建HTTP服务器
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// 优雅关闭
|
||||
go func() {
|
||||
log.Info("服务已启动", zap.String("addr", srv.Addr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal("服务启动失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info("正在关闭服务...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Fatal("强制关闭服务", zap.Error(err))
|
||||
}
|
||||
|
||||
log.Info("服务已退出")
|
||||
}
|
||||
|
||||
// loadConfig 加载配置(CLI flags 优先于 config.yaml)// Logan.Li
|
||||
func loadConfig() *config.Config {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./configs")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
// 如果指定了配置文件路径,使用它 // Logan.Li
|
||||
if *flagConfig != "" {
|
||||
viper.SetConfigFile(*flagConfig)
|
||||
}
|
||||
|
||||
// 默认值(独立二进制模式:SQLite + 无 Redis)// Logan.Li
|
||||
viper.SetDefault("mode", "release")
|
||||
viper.SetDefault("port", "8080")
|
||||
viper.SetDefault("log_level", "info")
|
||||
|
||||
viper.SetDefault("database.driver", "sqlite")
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", 3306)
|
||||
viper.SetDefault("database.user", "")
|
||||
viper.SetDefault("database.password", "")
|
||||
viper.SetDefault("database.dbname", "data/aiops.db")
|
||||
viper.SetDefault("database.charset", "utf8mb4")
|
||||
viper.SetDefault("database.max_conn", 100)
|
||||
viper.SetDefault("database.max_idle", 10)
|
||||
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", 6379)
|
||||
viper.SetDefault("redis.driver", "redka")
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.db", 0)
|
||||
|
||||
viper.SetDefault("jwt.secret", "") // 生产环境必须通过配置或 JWT_SECRET 环境变量设置
|
||||
viper.SetDefault("jwt.expire_hour", 24)
|
||||
|
||||
viper.SetDefault("websocket.read_buffer_size", 1024)
|
||||
viper.SetDefault("websocket.write_buffer_size", 1024)
|
||||
|
||||
// 读取配置文件(不存在也不报错)// Logan.Li
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
// 配置文件不存在是正常的(独立二进制模式)
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
fmt.Fprintf(os.Stderr, "警告: 读取配置文件失败: %v,使用默认配置\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 环境变量覆盖 // Logan.Li
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// CLI flags 覆盖(最高优先级)// Logan.Li
|
||||
applyFlags()
|
||||
|
||||
// 解析配置 // Logan.Li
|
||||
var cfg config.Config
|
||||
if err := viper.Unmarshal(&cfg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "配置解析失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
// applyFlags 将 CLI 参数映射到 viper(覆盖 config.yaml 和默认值)// Logan.Li
|
||||
func applyFlags() {
|
||||
// 检查是否有任何 flag 被显式设置 // Logan.Li
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "port":
|
||||
viper.Set("port", *flagPort)
|
||||
case "mode":
|
||||
viper.Set("mode", *flagMode)
|
||||
case "log-level":
|
||||
viper.Set("log_level", *flagLogLevel)
|
||||
|
||||
case "db-driver":
|
||||
viper.Set("database.driver", *flagDBDriver)
|
||||
case "db-host":
|
||||
viper.Set("database.host", *flagDBHost)
|
||||
case "db-port":
|
||||
viper.Set("database.port", *flagDBPort)
|
||||
case "db-user":
|
||||
viper.Set("database.user", *flagDBUser)
|
||||
case "db-password":
|
||||
viper.Set("database.password", *flagDBPassword)
|
||||
case "db-name":
|
||||
viper.Set("database.dbname", *flagDBName)
|
||||
case "db-charset":
|
||||
viper.Set("database.charset", *flagDBCharset)
|
||||
|
||||
case "redis-host":
|
||||
viper.Set("redis.host", *flagRedisHost)
|
||||
case "redis-port":
|
||||
viper.Set("redis.port", *flagRedisPort)
|
||||
case "redis-password":
|
||||
viper.Set("redis.password", *flagRedisPassword)
|
||||
case "redis-db":
|
||||
viper.Set("redis.db", *flagRedisDB)
|
||||
case "redis-driver":
|
||||
viper.Set("redis.driver", *flagRedisDriver)
|
||||
|
||||
case "jwt-secret":
|
||||
viper.Set("jwt.secret", *flagJWTSecret)
|
||||
case "jwt-expire":
|
||||
viper.Set("jwt.expire_hour", *flagJWTExpire)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CORS 跨域中间件
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user