package service import ( "encoding/json" "fmt" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/email" "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/shirou/gopsutil/v4/disk" "sort" "strings" "sync" "time" ) type AlertService struct{} type IAlertService interface { PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error) GetAlerts() ([]dto.AlertDTO, error) CreateAlert(create dto.AlertCreate) error UpdateAlert(req dto.AlertUpdate) error DeleteAlert(id uint) error GetAlert(id uint) (dto.AlertDTO, error) UpdateStatus(id uint, status string) error ExternalUpdateAlert(req dto.AlertCreate) error GetDisks() ([]dto.DiskDTO, error) PageAlertLogs(req dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) CleanAlertLogs() error GetClams() ([]dto.ClamDTO, error) GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error) GetAlertConfig() ([]model.AlertConfig, error) UpdateAlertConfig(req dto.AlertConfigUpdate) error DeleteAlertConfig(id uint) error TestAlertConfig(req dto.AlertConfigTest) (bool, error) } func NewIAlertService() IAlertService { return &AlertService{} } func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO, error) { var ( opts []repo.DBOption result []dto.AlertDTO ) if search.Status != "" { opts = append(opts, repo.WithByStatus(search.Status)) } if search.Type != "" { opts = append(opts, alertRepo.WithByType(search.Type)) } opts = append(opts, repo.WithOrderBy("created_at desc")) total, alerts, err := alertRepo.Page(search.Page, search.PageSize, opts...) if err != nil { return 0, nil, err } for _, item := range alerts { result = append(result, dto.AlertDTO{ ID: item.ID, Type: item.Type, Cycle: item.Cycle, Count: item.Count, Method: item.Method, Title: item.Title, Project: item.Project, Status: item.Status, SendCount: item.SendCount, AdvancedParams: item.AdvancedParams, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }) } return total, result, err } func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) { var ( opts []repo.DBOption result []dto.AlertDTO ) opts = append(opts, repo.WithByStatus(constant.AlertEnable)) alerts, err := alertRepo.List(opts...) if err != nil { return nil, err } for _, item := range alerts { result = append(result, dto.AlertDTO{ ID: item.ID, Type: item.Type, Cycle: item.Cycle, Count: item.Count, Method: item.Method, Title: item.Title, Project: item.Project, Status: item.Status, SendCount: item.SendCount, AdvancedParams: item.AdvancedParams, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }) } return result, err } func (a AlertService) CreateAlert(create dto.AlertCreate) error { var alertID uint var alertInfo model.Alert if create.Project != "" { alertInfo, _ := alertRepo.Get(alertRepo.WithByType(create.Type), alertRepo.WithByProject(create.Project)) alertID = alertInfo.ID } else { alertInfo, _ := alertRepo.Get(alertRepo.WithByType(create.Type)) alertID = alertInfo.ID } if alertID != 0 { var upAlert dto.AlertUpdate if err := copier.Copy(&upAlert, &create); err != nil { return buserr.WithErr("ErrStructTransform", err) } upAlert.ID = alertID err := a.UpdateAlert(upAlert) if err != nil { return err } } else { alertInfo.Status = constant.AlertEnable if err := copier.Copy(&alertInfo, &create); err != nil { return buserr.WithErr("ErrStructTransform", err) } if err := alertRepo.Create(&alertInfo); err != nil { return err } NewIAlertTaskHelper().InitTask(alertInfo.Type) } return nil } func (a AlertService) UpdateAlert(req dto.AlertUpdate) error { upMap := make(map[string]interface{}) upMap["id"] = req.ID upMap["type"] = req.Type upMap["cycle"] = req.Cycle upMap["count"] = req.Count upMap["method"] = req.Method upMap["title"] = req.Title upMap["project"] = req.Project upMap["status"] = req.Status upMap["send_count"] = req.SendCount upMap["advanced_params"] = req.AdvancedParams if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil { return err } NewIAlertTaskHelper().InitTask(req.Type) return nil } func (a AlertService) DeleteAlert(id uint) error { return alertRepo.Delete(repo.WithByID(id)) } func (a AlertService) GetAlert(id uint) (dto.AlertDTO, error) { var res dto.AlertDTO alertInfo, err := alertRepo.Get(repo.WithByID(id)) if err != nil { return res, err } _ = copier.Copy(&res, &alertInfo) return res, nil } func (a AlertService) UpdateStatus(id uint, status string) error { alertInfo, _ := alertRepo.Get(repo.WithByID(id)) if alertInfo.ID == 0 { return buserr.New("ErrRecordNotFound") } err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID)) if err != nil { return err } alerts, err := a.GetAlerts() if err != nil { return err } if len(alerts) > 0 { NewIAlertTaskHelper().InitTask(alertInfo.Type) } else { NewIAlertTaskHelper().StopTask() } return nil } func (a AlertService) GetDisks() ([]dto.DiskDTO, error) { var disks []dto.DiskDTO excludes := map[string]struct{}{ "/mnt/cdrom": {}, "/boot": {}, "/boot/efi": {}, "/dev": {}, "/dev/shm": {}, "/run/lock": {}, "/run": {}, "/run/shm": {}, "/run/user": {}, } stdout, err := executeDiskCommand() if err != nil { return disks, nil } lines := strings.Split(stdout, "\n") var mounts []dto.AlertDiskInfo for _, line := range lines { fields := strings.Fields(line) if len(fields) < 7 { continue } mountPoint := strings.Join(fields[6:], " ") if shouldExclude(fields, mountPoint, excludes) { continue } mounts = append(mounts, dto.AlertDiskInfo{Type: fields[1], Device: fields[0], Mount: mountPoint}) } var ( wg sync.WaitGroup mu sync.Mutex ) wg.Add(len(mounts)) for i := 0; i < len(mounts); i++ { go func(timeoutCh <-chan time.Time, mount dto.AlertDiskInfo) { defer wg.Done() var itemData dto.DiskDTO itemData.Path = mount.Mount itemData.Type = mount.Type itemData.Device = mount.Device select { case <-timeoutCh: mu.Lock() disks = append(disks, itemData) mu.Unlock() global.LOG.Errorf("load disk info from %s failed, err: timeout", mount.Mount) default: state, err := disk.Usage(mount.Mount) if err != nil { mu.Lock() disks = append(disks, itemData) mu.Unlock() global.LOG.Errorf("load disk info from %s failed, err: %v", mount.Mount, err) return } itemData.Total = state.Total itemData.Free = state.Free itemData.Used = state.Used itemData.UsedPercent = state.UsedPercent itemData.InodesTotal = state.InodesTotal itemData.InodesUsed = state.InodesUsed itemData.InodesFree = state.InodesFree itemData.InodesUsedPercent = state.InodesUsedPercent mu.Lock() disks = append(disks, itemData) mu.Unlock() } }(time.After(5*time.Second), mounts[i]) } wg.Wait() sort.Slice(disks, func(i, j int) bool { return disks[i].Path < disks[j].Path }) return disks, nil } func executeDiskCommand() (string, error) { cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second)) stdout, err := cmdMgr.RunWithStdoutBashC("df -hT -P | grep '/' | grep -v tmpfs | grep -v 'snap/core' | grep -v udev") if err != nil { cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second)) stdout, err = cmdMgr2.RunWithStdoutBashC("df -lhT -P | grep '/' | grep -v tmpfs | grep -v 'snap/core' | grep -v udev") } return stdout, err } func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool { if strings.HasPrefix(mountPoint, "/snap") || len(strings.Split(mountPoint, "/")) > 10 { return true } if strings.TrimSpace(fields[1]) == "tmpfs" { return true } if strings.Contains(fields[2], "K") { return true } if strings.Contains(mountPoint, "docker") { return true } _, excluded := excludes[mountPoint] return excluded } func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) { var ( opts []repo.DBOption result []dto.AlertLogDTO ) if search.Status != "" { opts = append(opts, repo.WithByStatus(search.Status)) } if search.Count != 0 { opts = append(opts, alertRepo.WithByCount(search.Count)) } opts = append(opts, repo.WithOrderBy("created_at desc")) total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...) if err != nil { return 0, nil, err } for _, item := range alerts { alertLogDTO, err := a.parseAlertLog(item) if err != nil { return 0, nil, err } result = append(result, alertLogDTO) } return total, result, err } func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error) { var alertDetail dto.AlertDetail var alertRule dto.AlertRule if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil { return dto.AlertLogDTO{}, err } if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil { return dto.AlertLogDTO{}, err } return dto.AlertLogDTO{ ID: item.ID, Count: item.Count, Type: item.Type, Status: item.Status, Method: item.Method, Message: item.Message, AlertId: item.AlertId, AlertDetail: alertDetail, AlertRule: alertRule, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, }, nil } func unmarshalAlertInfo(data string, v interface{}) error { if err := json.Unmarshal([]byte(data), v); err != nil { return fmt.Errorf("unmarshal alert info vars failed, err: %v", err) } return nil } func (a AlertService) CleanAlertLogs() error { return alertRepo.CleanAlertLogs() } func (a AlertService) GetClams() ([]dto.ClamDTO, error) { var clams []dto.ClamDTO clamList, err := clamRepo.List() for _, clam := range clamList { var clamDTO dto.ClamDTO clamDTO.ID = clam.ID clamDTO.Name = clam.Name clamDTO.Path = clam.Path clamDTO.Status = clam.Status clamDTO.UpdatedAt = clam.UpdatedAt clamDTO.CreatedAt = clam.CreatedAt clams = append(clams, clamDTO) } return clams, err } func (a AlertService) GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error) { var cronJobs []dto.CronJobDTO var ( opts []repo.DBOption ) if req.Status != "" { opts = append(opts, repo.WithByStatus(req.Status)) } if req.Type != "" { opts = append(opts, repo.WithByType(req.Type)) } cronjobList, err := cronjobRepo.List(opts...) for _, cronJob := range cronjobList { var cronJobDTO dto.CronJobDTO cronJobDTO.ID = cronJob.ID cronJobDTO.Name = cronJob.Name cronJobDTO.Status = cronJob.Status cronJobDTO.Type = cronJob.Type cronJobDTO.UpdatedAt = cronJob.UpdatedAt cronJobDTO.CreatedAt = cronJob.CreatedAt cronJobs = append(cronJobs, cronJobDTO) } return cronJobs, err } func (a AlertService) GetAlertConfig() ([]model.AlertConfig, error) { var ( opts []repo.DBOption configs []model.AlertConfig ) opts = append(opts, repo.WithByStatus(constant.AlertEnable)) configs, err := alertRepo.AlertConfigList(opts...) return configs, err } func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error { if req.ID != 0 { upMap := make(map[string]interface{}) upMap["id"] = req.ID upMap["type"] = req.Type upMap["title"] = req.Title upMap["status"] = req.Status upMap["config"] = req.Config if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil { return err } } else { var alertConfig model.AlertConfig if err := copier.Copy(&alertConfig, &req); err != nil { return buserr.WithErr("ErrStructTransform", err) } if err := alertRepo.CreateAlertConfig(&alertConfig); err != nil { return err } } return nil } func (a AlertService) DeleteAlertConfig(id uint) error { return alertRepo.DeleteAlertConfig(repo.WithByID(id)) } func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) { cfg := email.SMTPConfig{ Host: req.Host, Port: req.Port, Username: req.Sender, Password: req.Password, From: fmt.Sprintf("%s <%s>", req.DisplayName, req.Sender), Encryption: req.Encryption, Recipient: req.Recipient, } msg := email.EmailMessage{ Subject: i18n.GetMsgByKey("TestAlertTitle"), Body: i18n.GetMsgByKey("TestAlert"), IsHTML: false, } transport := xpack.LoadRequestTransport() if err := email.SendMail(cfg, msg, transport); err != nil { return false, err } return true, nil } func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error { upMap := make(map[string]interface{}) var newStatus string if updateAlert.SendCount == 0 { newStatus = constant.AlertDisable } else { newStatus = constant.AlertEnable upMap["send_count"] = updateAlert.SendCount if updateAlert.Method != "" { upMap["method"] = updateAlert.Method } } upMap["status"] = newStatus alertInfo, _ := alertRepo.Get( alertRepo.WithByType(updateAlert.Type), alertRepo.WithByProject(updateAlert.Project), ) if alertInfo.ID > 0 { shouldUpdate := false if alertInfo.Status != newStatus { shouldUpdate = true } if val, ok := upMap["send_count"]; ok && val != alertInfo.SendCount { shouldUpdate = true } if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method { shouldUpdate = true } if shouldUpdate { if err := alertRepo.Update( upMap, alertRepo.WithByProject(updateAlert.Project), alertRepo.WithByType(updateAlert.Type), ); err != nil { return err } } } else { if updateAlert.Method != "" && updateAlert.Title != "" { updateAlert.Status = newStatus if err := a.CreateAlert(updateAlert); err != nil { return err } } } return nil }