diff --git a/agent/app/dto/alert.go b/agent/app/dto/alert.go index fcf2d202f0bd..eed2c897427b 100644 --- a/agent/app/dto/alert.go +++ b/agent/app/dto/alert.go @@ -36,40 +36,43 @@ type AlertSearch struct { } type AlertDTO struct { - ID uint `json:"id"` - Type string `json:"type"` - Cycle uint `json:"cycle"` - Count uint `json:"count"` - Method string `json:"method"` - Title string `json:"title"` - Project string `json:"project"` - Status string `json:"status"` - SendCount uint `json:"sendCount"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID uint `json:"id"` + Type string `json:"type"` + Cycle uint `json:"cycle"` + Count uint `json:"count"` + Method string `json:"method"` + Title string `json:"title"` + Project string `json:"project"` + Status string `json:"status"` + SendCount uint `json:"sendCount"` + AdvancedParams string `json:"advancedParams"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type AlertCreate struct { - Type string `json:"type" validate:"required"` - Cycle uint `json:"cycle"` - Count uint `json:"count"` - Method string `json:"method" validate:"required"` - Title string `json:"title"` - Project string `json:"project"` - Status string `json:"status"` - SendCount uint `json:"sendCount"` + Type string `json:"type" validate:"required"` + Cycle uint `json:"cycle"` + Count uint `json:"count"` + Method string `json:"method" validate:"required"` + Title string `json:"title"` + Project string `json:"project"` + Status string `json:"status"` + SendCount uint `json:"sendCount"` + AdvancedParams string `json:"advancedParams"` } type AlertUpdate struct { - ID uint `json:"id" validate:"required"` - Type string `json:"type"` - Cycle uint `json:"cycle"` - Count uint `json:"count"` - Method string `json:"method"` - Title string `json:"title"` - Project string `json:"project"` - Status string `json:"status"` - SendCount uint `json:"sendCount"` + ID uint `json:"id" validate:"required"` + Type string `json:"type"` + Cycle uint `json:"cycle"` + Count uint `json:"count"` + Method string `json:"method"` + Title string `json:"title"` + Project string `json:"project"` + Status string `json:"status"` + SendCount uint `json:"sendCount"` + AdvancedParams string `json:"advancedParams"` } type DeleteRequest struct { diff --git a/agent/app/model/alert.go b/agent/app/model/alert.go index ac364f51082a..1676b35eab3e 100644 --- a/agent/app/model/alert.go +++ b/agent/app/model/alert.go @@ -3,14 +3,15 @@ package model type Alert struct { BaseModel - Title string `gorm:"type:varchar(256);not null" json:"title"` - Type string `gorm:"type:varchar(64);not null" json:"type"` - Cycle uint `gorm:"type:integer;not null" json:"cycle"` - Count uint `gorm:"type:integer;not null" json:"count"` - Project string `gorm:"type:varchar(64)" json:"project"` - Status string `gorm:"type:varchar(64);not null" json:"status"` - Method string `gorm:"type:varchar(64);not null" json:"method"` - SendCount uint `gorm:"type:integer" json:"sendCount"` + Title string `gorm:"type:varchar(256);not null" json:"title"` + Type string `gorm:"type:varchar(64);not null" json:"type"` + Cycle uint `gorm:"type:integer;not null" json:"cycle"` + Count uint `gorm:"type:integer;not null" json:"count"` + Project string `gorm:"type:varchar(64)" json:"project"` + Status string `gorm:"type:varchar(64);not null" json:"status"` + Method string `gorm:"type:varchar(64);not null" json:"method"` + SendCount uint `gorm:"type:integer" json:"sendCount"` + AdvancedParams string `gorm:"type:longText" json:"advancedParam"` } type AlertTask struct { @@ -43,3 +44,12 @@ type AlertConfig struct { Status string `gorm:"type:varchar(64);not null" json:"status"` Config string `gorm:"type:varchar(256);not null" json:"config"` } + +type LoginLog struct { + BaseModel + IP string `json:"ip"` + Address string `json:"address"` + Agent string `json:"agent"` + Status string `json:"status"` + Message string `json:"message"` +} diff --git a/agent/app/service/alert.go b/agent/app/service/alert.go index e58dcd899688..9e9c3a1a1edd 100644 --- a/agent/app/service/alert.go +++ b/agent/app/service/alert.go @@ -70,17 +70,18 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO, 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, - CreatedAt: item.CreatedAt, - UpdatedAt: item.UpdatedAt, + 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, }) } @@ -100,17 +101,18 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) { 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, - CreatedAt: item.CreatedAt, - UpdatedAt: item.UpdatedAt, + 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, }) } @@ -165,6 +167,7 @@ func (a AlertService) UpdateAlert(req dto.AlertUpdate) error { 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 diff --git a/agent/app/service/alert_helper.go b/agent/app/service/alert_helper.go index 89d018b336ce..dcb5ce1d1a60 100644 --- a/agent/app/service/alert_helper.go +++ b/agent/app/service/alert_helper.go @@ -4,24 +4,29 @@ import ( "encoding/json" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" - versionUtil "github.com/1Panel-dev/1Panel/agent/utils/version" - "github.com/1Panel-dev/1Panel/agent/utils/xpack" - "math" - "sort" - "strconv" - "strings" - "time" - "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" "github.com/1Panel-dev/1Panel/agent/utils/common" + versionUtil "github.com/1Panel-dev/1Panel/agent/utils/version" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/load" "github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/net" + "math" + "sort" + "strconv" + "strings" + "time" +) + +const ( + ResourceAlertInterval = 30 + CheckIntervalSec = 3 + LoadCheckIntervalMin = 5 ) type AlertTaskHelper struct { @@ -39,16 +44,17 @@ type IAlertTaskHelper interface { var cpuLoad1, cpuLoad5, cpuLoad15 []float64 var memoryLoad1, memoryLoad5, memoryLoad15 []float64 -const ResourceAlertInterval = 30 - var baseTypes = map[string]bool{"ssl": true, "siteEndTime": true, "panelPwdEndTime": true, "panelUpdate": true} -var resourceTypes = map[string]bool{"cpu": true, "memory": true, "disk": true, "load": true} +var resourceTypes = map[string]bool{"cpu": true, "memory": true, "disk": true, "load": true, "panelLogin": true, "sshLogin": true, "nodeException": true, "licenseException": true} func NewIAlertTaskHelper() IAlertTaskHelper { - return &AlertTaskHelper{} + return &AlertTaskHelper{ + DiskIO: make(chan []disk.IOCountersStat, 1), + NetIO: make(chan []net.IOCountersStat, 1), + } } func (m *AlertTaskHelper) StartTask() { - baseAlert, resourceAlert := handleTask() + baseAlert, resourceAlert := m.getClassifiedAlerts() if len(baseAlert) == 0 && len(resourceAlert) == 0 { return } @@ -67,16 +73,7 @@ func (m *AlertTaskHelper) ResetTask() { } func (m *AlertTaskHelper) InitTask(alertType string) { - if alertType == "cpu" { - cpuLoad1 = []float64{} - cpuLoad5 = []float64{} - cpuLoad15 = []float64{} - } - if alertType == "memory" { - memoryLoad1 = []float64{} - memoryLoad5 = []float64{} - memoryLoad15 = []float64{} - } + resetAlertState(alertType) if baseTypes[alertType] { stopBaseJob() } else if resourceTypes[alertType] { @@ -85,99 +82,65 @@ func (m *AlertTaskHelper) InitTask(alertType string) { m.StartTask() } -func resourceTask(resourceAlert []dto.AlertDTO) { - for _, alert := range resourceAlert { - if !alertUtil.CheckSendTimeRange(alert.Type) { - continue - } - switch alert.Type { - case "cpu": - loadCPUUsage(alert) - case "memory": - loadMemUsage(alert) - case "load": - loadLoadInfo(alert) - case "disk": - loadDiskUsage(alert) - default: - } - } -} - -func baseTask(baseAlert []dto.AlertDTO) { - for _, alert := range baseAlert { - if !alertUtil.CheckSendTimeRange(alert.Type) { - continue - } - switch alert.Type { - case "ssl": - loadSSLInfo(alert) - case "siteEndTime": - loadWebsiteInfo(alert) - case "panelPwdEndTime": - if global.IsMaster { - loadPanelPwd(alert) - } - case "panelUpdate": - if global.IsMaster { - loadPanelUpdate(alert) - } - default: - } +func resetAlertState(alertType string) { + switch alertType { + case "cpu": + cpuLoad1 = []float64{} + cpuLoad5 = []float64{} + cpuLoad15 = []float64{} + case "memory": + memoryLoad1 = []float64{} + memoryLoad5 = []float64{} + memoryLoad15 = []float64{} } } -func handleTask() (baseAlert []dto.AlertDTO, resourceAlert []dto.AlertDTO) { +func (m *AlertTaskHelper) getClassifiedAlerts() (baseAlerts, resourceAlerts []dto.AlertDTO) { alertList, _ := NewIAlertService().GetAlerts() - baseAlert, resourceAlert = classifyAlerts(alertList) - return baseAlert, resourceAlert -} - -func classifyAlerts(alertList []dto.AlertDTO) (baseAlert, resourceAlert []dto.AlertDTO) { for _, alert := range alertList { if baseTypes[alert.Type] { - baseAlert = append(baseAlert, alert) + baseAlerts = append(baseAlerts, alert) } else if resourceTypes[alert.Type] { - resourceAlert = append(resourceAlert, alert) + resourceAlerts = append(resourceAlerts, alert) } } return } -func handleBaseAlerts(baseAlert []dto.AlertDTO) { - if len(baseAlert) > 0 { - if global.AlertBaseJobID == 0 { - baseTask(baseAlert) - jobID, err := global.Cron.AddFunc("*/30 * * * *", func() { - baseTask(baseAlert) - }) - if err != nil { - global.LOG.Errorf("alert base job start failed: %v", err) - return - } - global.AlertBaseJobID = jobID - global.LOG.Info("start alert base job") +func handleBaseAlerts(baseAlerts []dto.AlertDTO) { + if len(baseAlerts) == 0 { + stopResourceJob() + return + } + if global.AlertBaseJobID == 0 { + baseTask(baseAlerts) + jobID, err := global.Cron.AddFunc("*/30 * * * *", func() { + baseTask(baseAlerts) + }) + if err != nil { + global.LOG.Errorf("alert base job start failed: %v", err) + return } - } else { - stopBaseJob() + global.AlertBaseJobID = jobID + global.LOG.Info("start alert base job") } } -func handleResourceAlerts(resourceAlert []dto.AlertDTO) { - if len(resourceAlert) > 0 { - if global.AlertResourceJobID == 0 { - jobID, err := global.Cron.AddFunc("*/1 * * * *", func() { - resourceTask(resourceAlert) - }) - if err != nil { - global.LOG.Errorf("alert resource job start failed: %v", err) - return - } - global.AlertResourceJobID = jobID - global.LOG.Info("start alert resource job") - } - } else { +func handleResourceAlerts(resourceAlerts []dto.AlertDTO) { + if len(resourceAlerts) == 0 { stopResourceJob() + return + } + if global.AlertResourceJobID == 0 { + jobID, err := global.Cron.AddFunc("*/1 * * * *", func() { + resourceTask(resourceAlerts) + }) + if err != nil { + global.LOG.Errorf("alert resource job start failed: %v", err) + return + } + global.AlertResourceJobID = jobID + global.LOG.Info("start alert resource job") } } @@ -197,214 +160,117 @@ func stopResourceJob() { } } -func loadSSLInfo(alert dto.AlertDTO) { - var opts []repo.DBOption - if alert.Project != "all" { - itemID, _ := strconv.Atoi(alert.Project) - opts = append(opts, repo.WithByID(uint(itemID))) +func baseTask(baseAlert []dto.AlertDTO) { + for _, alert := range baseAlert { + if !alertUtil.CheckSendTimeRange(alert.Type) { + continue + } + switch alert.Type { + case "ssl": + loadSSLInfo(alert) + case "siteEndTime": + loadWebsiteInfo(alert) + case "panelPwdEndTime": + if global.IsMaster { + loadPanelPwd(alert) + } + case "panelUpdate": + if global.IsMaster { + loadPanelUpdate(alert) + } + } } +} - sslList, _ := repo.NewISSLRepo().List(opts...) - currentDate := time.Now() - daysDifferenceMap := make(map[int][]string) - projectMap := make(map[uint][]time.Time) - for _, ssl := range sslList { - daysDifference := int(ssl.ExpireDate.Sub(currentDate).Hours() / 24) - if daysDifference > 0 && int(alert.Cycle) >= daysDifference { - daysDifferenceMap[daysDifference] = append(daysDifferenceMap[daysDifference], ssl.PrimaryDomain) - projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate) +func resourceTask(resourceAlert []dto.AlertDTO) { + minute := time.Now().Minute() + for _, alert := range resourceAlert { + if !alertUtil.CheckSendTimeRange(alert.Type) { + continue + } + execute := minute%LoadCheckIntervalMin == 0 + switch alert.Type { + case "cpu": + loadCPUUsage(alert) + case "memory": + loadMemUsage(alert) + case "load": + loadLoadInfo(alert) + case "disk": + loadDiskUsage(alert) + case "panelLogin": + loadPanelLogin(alert) + case "sshLogin": + loadSSHLogin(alert) + case "nodeException": + if execute && global.IsMaster { + loadNodeException(alert) + } + case "licenseException": + if execute && global.IsMaster { + loadLicenseException(alert) + } } } +} + +func loadSSLInfo(alert dto.AlertDTO) { + opts := getRepoOptionsByProject(alert.Project) + sslList, _ := repo.NewISSLRepo().List(opts...) + if len(sslList) == 0 { + return + } + daysDiffMap, projectMap := calculateSSLExpiryDays(sslList, alert.Cycle) projectJSON := serializeAndSortProjects(projectMap) - if projectJSON == "" { + if projectJSON == "" || len(daysDiffMap) == 0 { return } - if len(daysDifferenceMap) > 0 { - for daysDifference, ssl := range daysDifferenceMap { - primaryDomain := strings.Join(ssl, ",") - var params []dto.Param - params = createAlertBaseParams(strconv.Itoa(len(ssl)), strconv.Itoa(daysDifference)) - methods := strings.Split(alert.Method, ",") - for _, m := range methods { - m = strings.TrimSpace(m) - switch m { - case constant.SMS: - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, projectJSON, constant.SMS) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: totalCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - if !alertUtil.CheckTaskFrequency(constant.SMS) { - continue - } - _ = xpack.CreateSMSAlertLog(alert, create, primaryDomain, params, constant.SMS) - alertUtil.CreateNewAlertTask(alert.Project, alert.Type, projectJSON, constant.SMS) - case constant.Email: - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, projectJSON, constant.Email) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: totalCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - alertDetail := alertUtil.ProcessAlertDetail(alert, primaryDomain, params, constant.Email) - alertRule := alertUtil.ProcessAlertRule(alert) - create.AlertRule = alertRule - create.AlertDetail = alertDetail - transport := xpack.LoadRequestTransport() - _ = alertUtil.CreateEmailAlertLog(create, alert, params, transport) - alertUtil.CreateNewAlertTask(alert.Project, alert.Type, projectJSON, constant.Email) - default: - } - } - } - global.LOG.Info("SSL alert push successful") + sender := NewAlertSender(alert, projectJSON) + for daysDiff, domains := range daysDiffMap { + domainStr := strings.Join(domains, ",") + params := createAlertBaseParams(strconv.Itoa(len(domains)), strconv.Itoa(daysDiff)) + sender.Send(domainStr, params) } } func loadWebsiteInfo(alert dto.AlertDTO) { - var opts []repo.DBOption - if alert.Project != "all" { - itemID, _ := strconv.Atoi(alert.Project) - opts = append(opts, repo.WithByID(uint(itemID))) - } - + opts := getRepoOptionsByProject(alert.Project) websiteList, _ := websiteRepo.List(opts...) - currentDate := time.Now() - daysDifferenceMap := make(map[int][]string) - projectMap := make(map[uint][]time.Time) - for _, website := range websiteList { - daysDifference := int(website.ExpireDate.Sub(currentDate).Hours() / 24) - if daysDifference > 0 && int(alert.Cycle) >= daysDifference { - daysDifferenceMap[daysDifference] = append(daysDifferenceMap[daysDifference], website.PrimaryDomain) - projectMap[website.ID] = append(projectMap[website.ID], website.ExpireDate) - } + if len(websiteList) == 0 { + return } + + daysDiffMap, projectMap := calculateWebsiteExpiryDays(websiteList, alert.Cycle) projectJSON := serializeAndSortProjects(projectMap) - if projectJSON == "" { + if projectJSON == "" || len(daysDiffMap) == 0 { return } - if len(daysDifferenceMap) > 0 { - methods := strings.Split(alert.Method, ",") - for daysDifference, websites := range daysDifferenceMap { - primaryDomain := strings.Join(websites, ",") - var params []dto.Param - params = createAlertBaseParams(strconv.Itoa(len(websites)), strconv.Itoa(daysDifference)) - for _, m := range methods { - m = strings.TrimSpace(m) - switch m { - case constant.SMS: - if !alertUtil.CheckTaskFrequency(constant.SMS) { - continue - } - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, projectJSON, constant.SMS) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: totalCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - _ = xpack.CreateSMSAlertLog(alert, create, primaryDomain, params, constant.SMS) - alertUtil.CreateNewAlertTask(alert.Project, alert.Type, projectJSON, constant.SMS) - case constant.Email: - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, projectJSON, constant.Email) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: totalCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - alertDetail := alertUtil.ProcessAlertDetail(alert, primaryDomain, params, constant.Email) - alertRule := alertUtil.ProcessAlertRule(alert) - create.AlertDetail = alertDetail - create.AlertRule = alertRule - transport := xpack.LoadRequestTransport() - _ = alertUtil.CreateEmailAlertLog(create, alert, params, transport) - alertUtil.CreateNewAlertTask(alert.Project, alert.Type, projectJSON, constant.Email) - default: - } - } - } - global.LOG.Info("website expiration alert push successful") + sender := NewAlertSender(alert, projectJSON) + for daysDiff, domains := range daysDiffMap { + domainStr := strings.Join(domains, ",") + params := createAlertBaseParams(strconv.Itoa(len(domains)), strconv.Itoa(daysDiff)) + sender.Send(domainStr, params) } } func loadPanelPwd(alert dto.AlertDTO) { // only master alert - var expirationDays model.Setting - if err := global.CoreDB.Model(&model.Setting{}).Where("key = ?", "ExpirationDays").First(&expirationDays).Error; err != nil { - global.LOG.Errorf("load %s from db setting failed, err: %v", "ExpirationDays", err) - return - } - if expirationDays.Value == "0" { + expDays, err := getSettingValue("ExpirationDays") + if err != nil || expDays == "0" { global.LOG.Info("panel password expiration setting not enabled, skip") return } - var expirationTime model.Setting - if err := global.CoreDB.Model(&model.Setting{}).Where("key = ?", "ExpirationTime").First(&expirationTime).Error; err != nil { - global.LOG.Errorf("load %s from db setting failed, err: %v", "ExpirationTime", err) + + expTimeStr, err := getSettingValue("ExpirationTime") + if err != nil { return } - - var params []dto.Param - defaultDate, _ := time.Parse(constant.DateTimeLayout, expirationTime.Value) - daysDifference := calculateDaysDifference(defaultDate) - if daysDifference >= 0 && int(alert.Cycle) >= daysDifference { - params = createAlertPwdParams(strconv.Itoa(daysDifference)) - methods := strings.Split(alert.Method, ",") - for _, m := range methods { - m = strings.TrimSpace(m) - switch m { - case constant.SMS: - if !alertUtil.CheckTaskFrequency(constant.SMS) { - continue - } - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, expirationTime.Value, constant.SMS) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - create := dto.AlertLogCreate{ - Count: totalCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - _ = xpack.CreateSMSAlertLog(alert, create, strconv.Itoa(daysDifference), params, constant.SMS) - alertUtil.CreateNewAlertTask(expirationTime.Value, alert.Type, expirationTime.Value, constant.SMS) - case constant.Email: - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, expirationTime.Value, constant.Email) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - create := dto.AlertLogCreate{ - Count: totalCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - alertDetail := alertUtil.ProcessAlertDetail(alert, strconv.Itoa(daysDifference), params, constant.Email) - alertRule := alertUtil.ProcessAlertRule(alert) - create.AlertRule = alertRule - create.AlertDetail = alertDetail - transport := xpack.LoadRequestTransport() - _ = alertUtil.CreateEmailAlertLog(create, alert, params, transport) - alertUtil.CreateNewAlertTask(expirationTime.Value, alert.Type, expirationTime.Value, constant.Email) - default: - } - } - global.LOG.Info("panel password expiration alert push successful") + expTime, _ := time.Parse(constant.DateTimeLayout, expTimeStr) + daysDiff := calculateDaysDifference(expTime) + if daysDiff >= 0 && int(alert.Cycle) >= daysDiff { + params := createAlertPwdParams(strconv.Itoa(daysDiff)) + sender := NewAlertSender(alert, expTimeStr) + sender.Send(strconv.Itoa(daysDiff), params) } } @@ -412,103 +278,48 @@ func loadPanelUpdate(alert dto.AlertDTO) { // only master alert info, err := versionUtil.GetUpgradeVersionInfo() if err != nil { - global.LOG.Errorf("error getting version, err: %s", err) + global.LOG.Errorf("error getting version info: %s", err) return } - // 获取版本信息 - var version string - // 检查哪个版本字段不为空,并赋值 - if info.NewVersion != "" { - version = info.NewVersion - } else if info.TestVersion != "" { - version = info.TestVersion - } else if info.LatestVersion != "" { - version = info.LatestVersion - } + version := getValidVersion(info) if version == "" { return } - var params []dto.Param - methods := strings.Split(alert.Method, ",") - for _, m := range methods { - m = strings.TrimSpace(m) - switch m { - case constant.SMS: - if !alertUtil.CheckTaskFrequency(constant.SMS) { - continue - } - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, version, constant.SMS) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - var create = dto.AlertLogCreate{ - Type: alert.Type, - AlertId: alert.ID, - Count: totalCount + 1, - } - _ = xpack.CreateSMSAlertLog(alert, create, version, params, constant.SMS) - alertUtil.CreateNewAlertTask(version, alert.Type, version, constant.SMS) - case constant.Email: - todayCount, totalCount, err := alertRepo.LoadTaskCount(alert.Type, version, constant.Email) - if err != nil || todayCount >= 1 || alert.SendCount <= totalCount { - continue - } - var create = dto.AlertLogCreate{ - Type: alert.Type, - AlertId: alert.ID, - Count: totalCount + 1, - } - alertDetail := alertUtil.ProcessAlertDetail(alert, version, params, constant.Email) - alertRule := alertUtil.ProcessAlertRule(alert) - create.AlertRule = alertRule - create.AlertDetail = alertDetail - transport := xpack.LoadRequestTransport() - _ = alertUtil.CreateEmailAlertLog(create, alert, params, transport) - alertUtil.CreateNewAlertTask(version, alert.Type, version, constant.Email) - default: - } - } - global.LOG.Info("panel update alert push successful") + sender := NewAlertSender(alert, version) + sender.Send(version, []dto.Param{}) } // 获取 CPU 使用率数据并发送到通道 func loadCPUUsage(alert dto.AlertDTO) { - percent, err := cpu.Percent(3*time.Second, false) + percent, err := cpu.Percent(time.Duration(CheckIntervalSec)*time.Second, false) if err != nil { global.LOG.Errorf("error getting cpu usage, err: %v", err) return } if len(percent) > 0 { - var cpuLoad *[]float64 + var usageLoad *[]float64 var threshold int switch alert.Cycle { case 1: - cpuLoad = &cpuLoad1 + usageLoad = &cpuLoad1 threshold = 1 case 5: - cpuLoad = &cpuLoad5 + usageLoad = &cpuLoad5 threshold = 5 case 15: - cpuLoad = &cpuLoad15 + usageLoad = &cpuLoad15 threshold = 15 - default: - return - } - - if checkAndSendAlert(alert, percent[0], cpuLoad, threshold) { - global.LOG.Info("cpu alert push successful") } + shouldSendResourceAlert(alert, percent[0], usageLoad, threshold) } - } // 获取内存使用情况数据并发送到通道 func loadMemUsage(alert dto.AlertDTO) { - memStat, err := mem.VirtualMemory() if err != nil { global.LOG.Errorf("error getting memory usage, err: %v", err) @@ -529,12 +340,8 @@ func loadMemUsage(alert dto.AlertDTO) { case 15: memoryLoad = &memoryLoad15 threshold = 15 - default: - return - } - if checkAndSendAlert(alert, percent, memoryLoad, threshold) { - global.LOG.Info("memory alert push successful") } + shouldSendResourceAlert(alert, percent, memoryLoad, threshold) } // 获取系统负载数据并发送到通道 @@ -556,258 +363,391 @@ func loadLoadInfo(alert dto.AlertDTO) { default: return } + if loadValue < float64(alert.Count) { + return + } newDate, err := alertRepo.GetTaskLog(alert.Type, alert.ID) if err != nil { global.LOG.Errorf("task log record not found, err: %v", err) } - if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval { - if loadValue >= float64(alert.Count) { - global.LOG.Infof("%d minute load: %f,detail: %v", alert.Cycle, loadValue, avgStat) - createAndLogAlert(alert, loadValue) - global.LOG.Info("load alert task push successful") - } + if isAlertDue(newDate) { + sendResourceAlert(alert, loadValue) } } -// 内存/cpu检查是否需要发送告警并处理相关逻辑 -func checkAndSendAlert(alert dto.AlertDTO, currentUsage float64, usageLoad *[]float64, threshold int) bool { +func loadDiskUsage(alert dto.AlertDTO) { newDate, err := alertRepo.GetTaskLog(alert.Type, alert.ID) - if err != nil { + if err != nil || !isAlertDue(newDate) { global.LOG.Errorf("record not found, err: %v", err) - return false + return + } + if strings.Contains(alert.Project, "all") { + err = processAllDisks(alert) + } else { + err = processSingleDisk(alert) } - *usageLoad = append(*usageLoad, currentUsage) +} - if len(*usageLoad) > threshold { - *usageLoad = (*usageLoad)[1:] +func loadPanelLogin(alert dto.AlertDTO) { + count, isAlert, err := alertUtil.CountRecentFailedLoginLogs(alert.Cycle, alert.Count) + alertType := alert.Type + quota := strconv.Itoa(count) + quotaType := strconv.Itoa(int(alert.Cycle)) + var params []dto.Param + if err != nil { + global.LOG.Errorf("Failed to count recent failed login logs: %v", err) + } + if isAlert { + alertType = "panelLogin" + quota = strconv.Itoa(count) + quotaType = "panelLogin" + params = append([]dto.Param{ + { + Index: "1", + Key: "cycle", + Value: "", + }, + { + Index: "2", + Key: "project", + Value: "", + }, + }) + sendAlerts(alert, alertType, quota, quotaType, params) + } + + whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n") + records, err := alertUtil.FindRecentSuccessLoginsNotInWhitelist(30, whitelist) + if err != nil { + global.LOG.Errorf("Failed to check recent failed ip login logs: %v", err) } - - if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval { - if len(*usageLoad) == threshold { - avgUsage := average(*usageLoad) - if avgUsage >= float64(alert.Count) { - global.LOG.Infof("%d minute %s: %f , usage: %v", threshold, alert.Type, avgUsage, usageLoad) - createAndLogAlert(alert, avgUsage) - return true + if len(records) > 0 { + quota = strings.Join(func() []string { + var ips []string + for _, r := range records { + ips = append(ips, r.IP) } + return ips + }(), "\n") + alertType = "panelIpLogin" + quotaType = "panelIpLogin" + params = append([]dto.Param{ + { + Index: "1", + Key: "cycle", + Value: "", + }, + { + Index: "2", + Key: "project", + Value: " IP ", + }, + }) + sendAlerts(alert, alertType, quota, quotaType, params) + } +} + +func loadSSHLogin(alert dto.AlertDTO) { + count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count) + alertType := alert.Type + quota := strconv.Itoa(count) + quotaType := strconv.Itoa(int(alert.Cycle)) + var params []dto.Param + if err != nil { + global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err) + } + if isAlert { + alertType = "sshLogin" + quota = strconv.Itoa(count) + quotaType = "sshLogin" + params = append([]dto.Param{ + { + Index: "1", + Key: "cycle", + Value: " SSH ", + }, + { + Index: "2", + Key: "project", + Value: "", + }, + }) + sendAlerts(alert, alertType, quota, quotaType, params) + } + whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n") + records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist) + if err != nil { + global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err) + } + if len(records) > 0 { + quota = strings.Join(func() []string { + var ips []string + for _, r := range records { + ips = append(ips, r) + } + return ips + }(), "\n") + alertType = "sshIpLogin" + quotaType = "sshIpLogin" + params = append([]dto.Param{ + { + Index: "1", + Key: "cycle", + Value: " SSH ", + }, + { + Index: "2", + Key: "project", + Value: " IP ", + }, + }) + sendAlerts(alert, alertType, quota, quotaType, params) + } +} + +func loadNodeException(alert dto.AlertDTO) { + // only master alert + failCount, err := xpack.GetNodeErrorAlert() + if err != nil { + global.LOG.Errorf("error getting node, err: %s", err) + return + } + if failCount > 0 { + quotaType := "node-error" + params := []dto.Param{ + { + Index: "1", + Key: "cycle", + Value: strconv.Itoa(int(failCount)), + }, + } + newDate, err := alertRepo.GetTaskLog(alert.Type, alert.ID) + if err != nil || !isAlertDue(newDate) { + global.LOG.Errorf("record not found, err: %v", err) + return } + sender := NewAlertSender(alert, quotaType) + sender.ResourceSend(strconv.Itoa(int(failCount)), params) } - return false + } -// 检查是否超过今日发送次数限制 -func checkTaskFrequency(alertType, quotaType string, sendCount uint, method string) (uint, bool) { - todayCount, _, err := alertRepo.LoadTaskCount(alertType, quotaType, method) +func loadLicenseException(alert dto.AlertDTO) { + // only master alert + failCount, err := xpack.GetLicenseErrorAlert() if err != nil { - global.LOG.Errorf("error getting task info, err: %v", err) - return todayCount, false + global.LOG.Errorf("error getting license, err: %s", err) + return } - if todayCount >= sendCount { - return todayCount, false + if failCount > 0 { + quotaType := "license-error" + params := []dto.Param{ + { + Index: "1", + Key: "cycle", + Value: strconv.Itoa(int(failCount)), + }, + } + newDate, err := alertRepo.GetTaskLog(alert.Type, alert.ID) + if err != nil || !isAlertDue(newDate) { + global.LOG.Errorf("record not found, err: %v", err) + return + } + sender := NewAlertSender(alert, quotaType) + sender.ResourceSend(strconv.Itoa(int(failCount)), params) } - - return todayCount, true } -// 创建告警日志和详情 -func createAndLogAlert(alert dto.AlertDTO, avgUsage float64) { - avgUsagePercent := common.FormatPercent(avgUsage) - params := createAlertAvgParams(strconv.Itoa(int(alert.Cycle)), getModule(alert.Type), avgUsagePercent) +func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param) { methods := strings.Split(alert.Method, ",") - for _, m := range methods { - m = strings.TrimSpace(m) - switch m { - case constant.SMS: - if !alertUtil.CheckTaskFrequency(constant.SMS) { - continue - } - todayCount, isValid := checkTaskFrequency(alert.Type, strconv.Itoa(int(alert.Cycle)), alert.SendCount, constant.SMS) - if !isValid { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: todayCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - _ = xpack.CreateSMSAlertLog(alert, create, avgUsagePercent, params, constant.SMS) - alertUtil.CreateNewAlertTask(avgUsagePercent, alert.Type, strconv.Itoa(int(alert.Cycle)), constant.SMS) - case constant.Email: - todayCount, isValid := checkTaskFrequency(alert.Type, strconv.Itoa(int(alert.Cycle)), alert.SendCount, constant.Email) - if !isValid { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: todayCount + 1, - AlertId: alert.ID, - Type: alert.Type, + newDate, err := alertRepo.GetTaskLog(alertType, alert.ID) + if err != nil { + global.LOG.Errorf("task log record not found, err: %v", err) + } + if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval { + for _, m := range methods { + m = strings.TrimSpace(m) + switch m { + case constant.SMS: + if !alertUtil.CheckSMSSendLimit(constant.SMS) { + continue + } + todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.SMS) + if !isValid { + continue + } + create := dto.AlertLogCreate{ + Type: alertType, + AlertId: alert.ID, + Count: todayCount + 1, + } + _ = xpack.CreateSMSAlertLog(alertType, alert, create, quotaType, params, constant.SMS) + alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.SMS) + global.LOG.Infof("%s alert sms push successful", alertType) + + case constant.Email: + todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.Email) + if !isValid { + continue + } + create := dto.AlertLogCreate{ + Type: alertType, + AlertId: alert.ID, + Count: todayCount + 1, + } + alertInfo := alert + alertInfo.Type = alertType + create.AlertRule = alertUtil.ProcessAlertRule(alert) + create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email) + transport := xpack.LoadRequestTransport() + _ = alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport) + alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.Email) + global.LOG.Infof("%s alert email push successful", alertType) } - alertDetail := alertUtil.ProcessAlertDetail(alert, avgUsagePercent, params, constant.Email) - alertRule := alertUtil.ProcessAlertRule(alert) - create.AlertRule = alertRule - create.AlertDetail = alertDetail - transport := xpack.LoadRequestTransport() - _ = alertUtil.CreateEmailAlertLog(create, alert, params, transport) - alertUtil.CreateNewAlertTask(avgUsagePercent, alert.Type, strconv.Itoa(int(alert.Cycle)), constant.Email) - default: } } } -func getModule(alertType string) string { - var module string - switch alertType { - case "cpu": - module = " CPU " - case "memory": - module = "内存" - case "load": - module = "负载" - default: +// ------------------------------ +func getRepoOptionsByProject(project string) []repo.DBOption { + var opts []repo.DBOption + if project != "all" { + itemID, _ := strconv.Atoi(project) + opts = append(opts, repo.WithByID(uint(itemID))) } - return module + return opts } -func loadDiskUsage(alert dto.AlertDTO) { - newDate, err := alertRepo.GetTaskLog(alert.Type, alert.ID) +func serializeAndSortProjects(projectMap map[uint][]time.Time) string { + if len(projectMap) == 0 { + return "" + } + keys := make([]int, 0, len(projectMap)) + for k := range projectMap { + keys = append(keys, int(k)) + } + sort.Ints(keys) + projectJSON, err := json.Marshal(projectMap) if err != nil { - global.LOG.Errorf("record not found, err: %v", err) + global.LOG.Errorf("Failed to serialize projectMap: %v", err) + return "" } - if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval { - if strings.Contains(alert.Project, "all") { - err = processAllDisks(alert) - } else { - err = processSingleDisk(alert) - } - if err != nil { - global.LOG.Errorf("error processing disk usage, err: %v", err) + return string(projectJSON) +} + +func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]string, map[uint][]time.Time) { + currentDate := time.Now() + daysDiffMap := make(map[int][]string) + projectMap := make(map[uint][]time.Time) + + for _, ssl := range sslList { + daysDiff := int(ssl.ExpireDate.Sub(currentDate).Hours() / 24) + if daysDiff > 0 && int(cycle) >= daysDiff { + daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain) + projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate) } } + return daysDiffMap, projectMap } -func processAllDisks(alert dto.AlertDTO) error { - diskList, err := NewIAlertService().GetDisks() - if err != nil { - global.LOG.Errorf("error getting disk list, err: %v", err) - return err - } +func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) { + currentDate := time.Now() + daysDiffMap := make(map[int][]string) + projectMap := make(map[uint][]time.Time) - var flag bool - for _, item := range diskList { - if success, err := checkAndCreateDiskAlert(alert, item.Path); err == nil && success { - flag = true + for _, website := range websites { + daysDiff := int(website.ExpireDate.Sub(currentDate).Hours() / 24) + if daysDiff > 0 && int(cycle) >= daysDiff { + daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], website.PrimaryDomain) + projectMap[website.ID] = append(projectMap[website.ID], website.ExpireDate) } } - if flag { - global.LOG.Info("all disk alert push successful") - } - return nil + return daysDiffMap, projectMap } -func processSingleDisk(alert dto.AlertDTO) error { - success, err := checkAndCreateDiskAlert(alert, alert.Project) - if err != nil { - return err +func getSettingValue(key string) (string, error) { + var setting model.Setting + if err := global.CoreDB.Model(&model.Setting{}).Where("key = ?", key).First(&setting).Error; err != nil { + global.LOG.Errorf("load %s from db setting failed: %v", key, err) + return "", err } - if success { - global.LOG.Info("disk alert push successful") - } - return nil + return setting.Value, nil } -func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) (bool, error) { - usageStat, err := disk.Usage(path) - if err != nil { - global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err) - return false, err +func getValidVersion(info *dto.UpgradeInfo) string { + if info.NewVersion != "" { + return info.NewVersion + } else if info.TestVersion != "" { + return info.TestVersion + } else if info.LatestVersion != "" { + return info.LatestVersion } + return "" +} - usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat) - commonTotal := float64(alert.Count) - if alert.Cycle == 1 { - commonTotal *= 1024 * 1024 * 1024 - } - if usedTotal < commonTotal { - return false, nil +func shouldSendResourceAlert(alert dto.AlertDTO, currentUsage float64, usageLoad *[]float64, threshold int) { + newDate, err := alertRepo.GetTaskLog(alert.Type, alert.ID) + if err != nil { + global.LOG.Errorf("record not found, err: %v", err) } - global.LOG.Infof("disk「 %s 」usage: %s", path, usedStr) - var params []dto.Param - params = createAlertDiskParams(path, usedStr) - methods := strings.Split(alert.Method, ",") - for _, m := range methods { - m = strings.TrimSpace(m) - switch m { - case constant.SMS: - if !alertUtil.CheckTaskFrequency(constant.SMS) { - continue - } - todayCount, isValid := checkTaskFrequency(alert.Type, alert.Project, alert.SendCount, constant.SMS) - if !isValid { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: todayCount + 1, - AlertId: alert.ID, - Type: alert.Type, - } - _ = xpack.CreateSMSAlertLog(alert, create, path, params, constant.SMS) - alertUtil.CreateNewAlertTask(strconv.Itoa(int(alert.Cycle)), alert.Type, alert.Project, constant.SMS) - case constant.Email: - todayCount, isValid := checkTaskFrequency(alert.Type, alert.Project, alert.SendCount, constant.Email) - if !isValid { - continue - } - create := dto.AlertLogCreate{ - Status: constant.AlertSuccess, - Count: todayCount + 1, - AlertId: alert.ID, - Type: alert.Type, + if isAlertDue(newDate) { + *usageLoad = append(*usageLoad, currentUsage) + if len(*usageLoad) > threshold { + *usageLoad = (*usageLoad)[1:] + } + if len(*usageLoad) == threshold { + avgUsage := average(*usageLoad) + if avgUsage >= float64(alert.Count) { + //global.LOG.Infof("%d minute %s: %f , usage: %v", threshold, alert.Type, avgUsage, usageLoad) + sendResourceAlert(alert, avgUsage) } - alertDetail := alertUtil.ProcessAlertDetail(alert, path, params, constant.Email) - alertRule := alertUtil.ProcessAlertRule(alert) - create.AlertRule = alertRule - create.AlertDetail = alertDetail - transport := xpack.LoadRequestTransport() - _ = alertUtil.CreateEmailAlertLog(create, alert, params, transport) - alertUtil.CreateNewAlertTask(strconv.Itoa(int(alert.Cycle)), alert.Type, alert.Project, constant.Email) - default: } } - - return true, nil } -func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) { - if cycle == 1 { - return float64(usageStat.Used), common.FormatBytes(usageStat.Used) +func isAlertDue(lastAlertTime time.Time) bool { + if lastAlertTime.IsZero() { + return true } - return usageStat.UsedPercent, common.FormatPercent(usageStat.UsedPercent) + return calculateMinutesDifference(lastAlertTime) > ResourceAlertInterval } -func calculateDaysDifference(expirationTime time.Time) int { - currentDate := time.Now() - formattedTime := currentDate.Format(constant.DateTimeLayout) - parsedTime, _ := time.Parse(constant.DateTimeLayout, formattedTime) - timeGap := expirationTime.Sub(parsedTime).Milliseconds() - if timeGap < 0 { - return -1 +func sendResourceAlert(alert dto.AlertDTO, value float64) { + valueStr := common.FormatPercent(value) + module := getModuleName(alert.Type) + params := createAlertAvgParams(strconv.Itoa(int(alert.Cycle)), module, valueStr) + sender := NewAlertSender(alert, strconv.Itoa(int(alert.Cycle))) + sender.ResourceSend(valueStr, params) +} + +func getModuleName(alertType string) string { + var module string + switch alertType { + case "cpu": + module = " CPU " + case "memory": + module = "内存" + case "load": + module = "负载" + default: } - daysDifference := int(math.Floor(float64(timeGap) / (3600 * 1000 * 24))) - return daysDifference + return module } -func calculateMinutesDifference(newDate time.Time) int { - now := time.Now() - if newDate.After(now) { - return -1 +// 检查是否超过今日发送次数限制 +func canSendAlertToday(alertType, quotaType string, sendCount uint, method string) (uint, bool) { + todayCount, _, err := alertRepo.LoadTaskCount(alertType, quotaType, method) + if err != nil { + global.LOG.Errorf("error getting task info, err: %v", err) + return todayCount, false } - minutesDifference := int(now.Sub(newDate).Minutes()) - return minutesDifference + if todayCount >= sendCount { + return todayCount, false + } + + return todayCount, true } func average(arr []float64) float64 { @@ -878,17 +818,82 @@ func createAlertDiskParams(project, count string) []dto.Param { } } -func serializeAndSortProjects(projectMap map[uint][]time.Time) string { - keys := make([]int, 0, len(projectMap)) - for k := range projectMap { - keys = append(keys, int(k)) +func processAllDisks(alert dto.AlertDTO) error { + diskList, err := NewIAlertService().GetDisks() + if err != nil { + global.LOG.Errorf("error getting disk list, err: %v", err) + return err } - sort.Ints(keys) - projectJSON, err := json.Marshal(projectMap) + + var flag bool + for _, item := range diskList { + if success, err := checkAndCreateDiskAlert(alert, item.Path); err == nil && success { + flag = true + } + } + if flag { + global.LOG.Info("all disk alert push successful") + } + return nil +} + +func processSingleDisk(alert dto.AlertDTO) error { + success, err := checkAndCreateDiskAlert(alert, alert.Project) if err != nil { - global.LOG.Errorf("Failed to serialize projectMap: %v", err) - return "" + return err + } + if success { + global.LOG.Info("disk alert push successful") } + return nil +} - return string(projectJSON) +func checkAndCreateDiskAlert(alert dto.AlertDTO, path string) (bool, error) { + usageStat, err := disk.Usage(path) + if err != nil { + global.LOG.Errorf("error getting disk usage for %s, err: %v", path, err) + return false, err + } + + usedTotal, usedStr := calculateUsedTotal(alert.Cycle, usageStat) + commonTotal := float64(alert.Count) + if alert.Cycle == 1 { + commonTotal *= 1024 * 1024 * 1024 + } + if usedTotal < commonTotal { + return false, nil + } + global.LOG.Infof("disk「 %s 」usage: %s", path, usedStr) + params := createAlertDiskParams(path, usedStr) + sender := NewAlertSender(alert, alert.Project) + sender.ResourceSend(path, params) + return true, nil +} + +func calculateUsedTotal(cycle uint, usageStat *disk.UsageStat) (float64, string) { + if cycle == 1 { + return float64(usageStat.Used), common.FormatBytes(usageStat.Used) + } + return usageStat.UsedPercent, common.FormatPercent(usageStat.UsedPercent) +} + +func calculateDaysDifference(expirationTime time.Time) int { + currentDate := time.Now() + formattedTime := currentDate.Format(constant.DateTimeLayout) + parsedTime, _ := time.Parse(constant.DateTimeLayout, formattedTime) + timeGap := expirationTime.Sub(parsedTime).Milliseconds() + if timeGap < 0 { + return -1 + } + daysDifference := int(math.Floor(float64(timeGap) / (3600 * 1000 * 24))) + return daysDifference +} + +func calculateMinutesDifference(newDate time.Time) int { + now := time.Now() + if newDate.After(now) { + return -1 + } + minutesDifference := int(now.Sub(newDate).Minutes()) + return minutesDifference } diff --git a/agent/app/service/alert_sender.go b/agent/app/service/alert_sender.go new file mode 100644 index 000000000000..4dd2cf0b31cf --- /dev/null +++ b/agent/app/service/alert_sender.go @@ -0,0 +1,165 @@ +package service + +import ( + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/constant" + "github.com/1Panel-dev/1Panel/agent/global" + alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" + "github.com/1Panel-dev/1Panel/agent/utils/xpack" + "strings" +) + +type AlertSender struct { + alert dto.AlertDTO + quotaType string +} + +func NewAlertSender(alert dto.AlertDTO, quotaType string) *AlertSender { + return &AlertSender{ + alert: alert, + quotaType: quotaType, + } +} + +func (s *AlertSender) Send(quota string, params []dto.Param) { + methods := strings.Split(s.alert.Method, ",") + for _, method := range methods { + method = strings.TrimSpace(method) + switch method { + case constant.SMS: + s.sendSMS(quota, params) + case constant.Email: + s.sendEmail(quota, params) + } + } +} + +func (s *AlertSender) ResourceSend(quota string, params []dto.Param) { + methods := strings.Split(s.alert.Method, ",") + for _, method := range methods { + method = strings.TrimSpace(method) + switch method { + case constant.SMS: + s.sendResourceSMS(quota, params) + case constant.Email: + s.sendResourceEmail(quota, params) + } + } +} + +func (s *AlertSender) sendSMS(quota string, params []dto.Param) { + if !alertUtil.CheckSMSSendLimit(constant.SMS) { + return + } + + totalCount, isValid := s.canSendAlert(constant.SMS) + if !isValid { + return + } + + create := dto.AlertLogCreate{ + Status: constant.AlertSuccess, + Count: totalCount + 1, + AlertId: s.alert.ID, + Type: s.alert.Type, + } + + _ = xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS) + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS) + global.LOG.Infof("%s alert sms push successful", s.alert.Type) +} + +func (s *AlertSender) sendEmail(quota string, params []dto.Param) { + totalCount, isValid := s.canSendAlert(constant.Email) + if !isValid { + return + } + + create := dto.AlertLogCreate{ + Status: constant.AlertSuccess, + Count: totalCount + 1, + AlertId: s.alert.ID, + Type: s.alert.Type, + AlertRule: alertUtil.ProcessAlertRule(s.alert), + AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), + } + + transport := xpack.LoadRequestTransport() + _ = alertUtil.CreateEmailAlertLog(create, s.alert, params, transport) + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email) + global.LOG.Infof("%s alert email push successful", s.alert.Type) +} + +func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) { + if !alertUtil.CheckSMSSendLimit(constant.SMS) { + return + } + + todayCount, isValid := s.canResourceSendAlert(constant.SMS) + if !isValid { + return + } + + create := dto.AlertLogCreate{ + Status: constant.AlertSuccess, + Count: todayCount + 1, + AlertId: s.alert.ID, + Type: s.alert.Type, + } + + if err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil { + global.LOG.Errorf("failed to send SMS alert: %v", err) + return + } + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS) + global.LOG.Infof("%s alert sms push successful", s.alert.Type) +} + +func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) { + todayCount, isValid := s.canResourceSendAlert(constant.Email) + if !isValid { + return + } + + create := dto.AlertLogCreate{ + Status: constant.AlertSuccess, + Count: todayCount + 1, + AlertId: s.alert.ID, + Type: s.alert.Type, + AlertRule: alertUtil.ProcessAlertRule(s.alert), + AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), + } + + transport := xpack.LoadRequestTransport() + if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport); err != nil { + global.LOG.Errorf("failed to send Email alert: %v", err) + return + } + alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email) + global.LOG.Infof("%s alert email push successful", s.alert.Type) +} + +func (s *AlertSender) canSendAlert(method string) (uint, bool) { + todayCount, totalCount, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method) + if err != nil { + global.LOG.Errorf("error getting task count: %v", err) + return totalCount, false + } + + if todayCount >= 1 || s.alert.SendCount <= totalCount { + return totalCount, false + } + return totalCount, true +} + +func (s *AlertSender) canResourceSendAlert(method string) (uint, bool) { + todayCount, _, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method) + if err != nil { + global.LOG.Errorf("error getting task count: %v", err) + return todayCount, false + } + if s.alert.SendCount <= todayCount { + return todayCount, false + } + return todayCount, true +} diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 0e8dfd13b661..82751ced8f0d 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -448,4 +448,8 @@ SSLAlert: "There are {{ .num }} SSL certificates on your 1Panel that will expire DiskUsedAlert: "Your 1Panel disk '{{ .name }}' has used {{ .used }}. Please log in to the panel for details." ResourceAlert: "The average {{ .name }} usage over {{ .time }} minutes on your 1Panel is {{ .used }}. Please log in to the panel for details." PanelVersionAlert: "A new version of 1Panel is available. Please log in to the panel to upgrade." -PanelPwdExpirationAlert: "Your 1Panel password will expire in {{ .day }} days. Please log in to the panel for details." \ No newline at end of file +PanelPwdExpirationAlert: "Your 1Panel password will expire in {{ .day }} days. Please log in to the panel for details." +CommonAlert: "Your 1Panel, {{ .msg }}, please log in to the panel for details." +NodeExceptionAlert: "Your 1Panel, {{ .num }} nodes are abnormal, please log in to the panel for details." +LicenseExceptionAlert: "Your 1Panel, {{ .num }} licenses are abnormal, please log in to the panel for details." +SSHAndPanelLoginAlert: "Your 1Panel, abnormal panel {{ .name }} login from {{ .ip }}, please log in to the panel for details." \ No newline at end of file diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 034c05cef670..a360665abe46 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -448,4 +448,8 @@ SSLAlert: "1Panel にある {{ .num }} 枚のSSL証明書が {{ .day }} 日後 DiskUsedAlert: "1Panel のディスク '{{ .name }}' は {{ .used }} 使用されています。詳細はパネルでご確認ください。" ResourceAlert: "1Panel の {{ .time }} 分間の平均 {{ .name }} 使用率は {{ .used }} です。詳細はパネルでご確認ください。" PanelVersionAlert: "1Panel に新しいバージョンが利用可能です。アップグレードはパネルから行ってください。" -PanelPwdExpirationAlert: "1Panel のパスワードは {{ .day }} 日後に期限切れとなります。詳細はパネルでご確認ください。" \ No newline at end of file +PanelPwdExpirationAlert: "1Panel のパスワードは {{ .day }} 日後に期限切れとなります。詳細はパネルでご確認ください。" +CommonAlert: "お使いの1Panel、{{ .msg }}、詳細はパネルにログインしてご確認ください。" +NodeExceptionAlert: "お使いの1Panel、{{ .num }}個のノードに異常があります。詳細はパネルにログインしてご確認ください。" +LicenseExceptionAlert: "お使いの1Panel、{{ .num }}個のライセンスに異常があります。詳細はパネルにログインしてご確認ください。" +SSHAndPanelLoginAlert: "お使いの1Panel、パネル{{ .name }}が{{ .ip }}から異常ログインしました。詳細はパネルにログインしてご確認ください。" \ No newline at end of file diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 23e9a5031275..f81d803cb47f 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -448,4 +448,8 @@ SSLAlert: "1Panel 에 있는 {{ .num }}개의 SSL 인증서가 {{ .day }}일 후 DiskUsedAlert: "1Panel 디스크 '{{ .name }}'의 사용량은 {{ .used }}입니다. 자세한 내용은 패널에서 확인하세요." ResourceAlert: "1Panel 의 평균 {{ .time }}분 동안 {{ .name }} 사용률은 {{ .used }}입니다. 자세한 내용은 패널에서 확인하세요." PanelVersionAlert: "1Panel 의 새로운 버전이 이용 가능합니다. 패널에 로그인하여 업그레이드하세요." -PanelPwdExpirationAlert: "1Panel 비밀번호가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에서 확인하세요." \ No newline at end of file +PanelPwdExpirationAlert: "1Panel 비밀번호가 {{ .day }}일 후에 만료됩니다. 자세한 내용은 패널에서 확인하세요." +CommonAlert: "귀하의 1Panel, {{ .msg }}. 자세한 내용은 패널에 로그인하여 확인하세요." +NodeExceptionAlert: "귀하의 1Panel, {{ .num }}개의 노드에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요." +LicenseExceptionAlert: "귀하의 1Panel, {{ .num }}개의 라이선스에 이상이 있습니다. 자세한 내용은 패널에 로그인하여 확인하세요." +SSHAndPanelLoginAlert: "귀하의 1Panel, 패널 {{ .name }}이(가) {{ .ip }}에서 비정상 로그인했습니다. 자세한 내용은 패널에 로그인하여 확인하세요." \ No newline at end of file diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 0f813a7c2962..178dc63674f5 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -447,4 +447,8 @@ SSLAlert: "Terdapat {{ .num }} sijil SSL dalam 1Panel anda yang akan tamat dalam DiskUsedAlert: "Cakera '{{ .name }}' dalam 1Panel anda telah menggunakan {{ .used }}. Sila log masuk ke panel untuk maklumat lanjut." ResourceAlert: "Penggunaan purata {{ .name }} selama {{ .time }} minit dalam 1Panel anda ialah {{ .used }}. Sila log masuk ke panel untuk maklumat lanjut." PanelVersionAlert: "Versi baru 1Panel tersedia. Sila log masuk ke panel untuk menaik taraf." -PanelPwdExpirationAlert: "Kata laluan 1Panel anda akan tamat dalam {{ .day }} hari. Sila log masuk ke panel untuk maklumat lanjut." \ No newline at end of file +PanelPwdExpirationAlert: "Kata laluan 1Panel anda akan tamat dalam {{ .day }} hari. Sila log masuk ke panel untuk maklumat lanjut." +CommonAlert: "1Panel anda, {{ .msg }}, sila log masuk ke panel untuk maklumat lanjut." +NodeExceptionAlert: "1Panel anda, {{ .num }} nod bermasalah, sila log masuk ke panel untuk maklumat lanjut." +LicenseExceptionAlert: "1Panel anda, {{ .num }} lesen bermasalah, sila log masuk ke panel untuk maklumat lanjut." +SSHAndPanelLoginAlert: "1Panel anda, log masuk panel {{ .name }} yang tidak normal dari {{ .ip }}, sila log masuk ke panel untuk maklumat lanjut." \ No newline at end of file diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index a3adcc469dee..94c5370c346d 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -448,4 +448,8 @@ SSLAlert: "{{ .num }} certificados SSL do seu 1Panel expirarão em {{ .day }} di DiskUsedAlert: "O disco '{{ .name }}' do seu 1Panel está com uso de {{ .used }}. Acesse o painel para mais detalhes." ResourceAlert: "O uso médio de {{ .name }} em {{ .time }} minutos no seu 1Panel é de {{ .used }}. Acesse o painel para mais detalhes." PanelVersionAlert: "Uma nova versão do 1Panel está disponível. Acesse o painel para atualizá-lo." -PanelPwdExpirationAlert: "A senha do 1Panel expirará em {{ .day }} dias. Acesse o painel para mais detalhes." \ No newline at end of file +PanelPwdExpirationAlert: "A senha do 1Panel expirará em {{ .day }} dias. Acesse o painel para mais detalhes." +CommonAlert: "Seu 1Panel, {{ .msg }}. Para mais detalhes, faça login no painel." +NodeExceptionAlert: "Seu 1Panel, {{ .num }} nós estão com problemas. Para mais detalhes, faça login no painel." +LicenseExceptionAlert: "Seu 1Panel, {{ .num }} licenças estão com problemas. Para mais detalhes, faça login no painel." +SSHAndPanelLoginAlert: "Seu 1Panel, login anormal no painel {{ .name }} a partir de {{ .ip }}. Para mais detalhes, faça login no painel." \ No newline at end of file diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 874511f3d80e..7d2173588eb3 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -448,4 +448,8 @@ SSLAlert: "На вашем 1Panel {{ .num }} SSL-сертификатов ист DiskUsedAlert: "Диск '{{ .name }}' на вашем 1Panel использует {{ .used }}. Подробности смотрите в панели." ResourceAlert: "Средняя загрузка {{ .name }} за {{ .time }} минут составляет {{ .used }}. Подробности смотрите в панели." PanelVersionAlert: "Доступна новая версия 1Panel. Обновитесь через панель." -PanelPwdExpirationAlert: "Пароль для 1Panel истекает через {{ .day }} дней. Подробности смотрите в панели." \ No newline at end of file +PanelPwdExpirationAlert: "Пароль для 1Panel истекает через {{ .day }} дней. Подробности смотрите в панели." +CommonAlert: "Ваш 1Panel, {{ .msg }}. Подробности смотрите, войдя в панель." +NodeExceptionAlert: "Ваш 1Panel, {{ .num }} узлов работают неправильно. Подробности смотрите, войдя в панель." +LicenseExceptionAlert: "Ваш 1Panel, {{ .num }} лицензий работают неправильно. Подробности смотрите, войдя в панель." +SSHAndPanelLoginAlert: "Ваш 1Panel, обнаружен аномальный вход в панель {{ .name }} с {{ .ip }}. Подробности смотрите, войдя в панель." \ No newline at end of file diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 0c0ab880b05a..eae85794e303 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -447,3 +447,7 @@ DiskUsedAlert: "1Panel diski '{{ .name }}' {{ .used }} kullanıldı. Detaylar i ResourceAlert: "1Panel üzerinde ortalama {{ .time }} dakikalık {{ .name }} kullanım oranı {{ .used }}. Detaylar için panele giriş yapın." PanelVersionAlert: "1Panel için yeni bir sürüm mevcut. Güncellemek için panele giriş yapın." PanelPwdExpirationAlert: "1Panel şifreniz {{ .day }} gün içinde sona erecek. Detaylar için panele giriş yapın." +CommonAlert: "1Panel'iniz, {{ .msg }}. Detaylar için panele giriş yapınız." +NodeExceptionAlert: "1Panel'iniz, {{ .num }} düğümde sorun var. Detaylar için panele giriş yapınız." +LicenseExceptionAlert: "1Panel'iniz, {{ .num }} lisansda sorun var. Detaylar için panele giriş yapınız." +SSHAndPanelLoginAlert: "1Panel'iniz, {{ .ip }} adresinden {{ .name }} paneline anormal giriş tespit edildi. Detaylar için panele giriş yapınız." diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index a894adbcb99b..2cf438adbc2e 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -448,3 +448,7 @@ DiskUsedAlert: "您的 1Panel 面板磁碟 '{{ .name }}' 已使用 {{ .used }} ResourceAlert: "您的 1Panel 面板於 {{ .time }} 分鐘內的平均 {{ .name }} 使用率為 {{ .used }},詳情請登入面板查看。" PanelVersionAlert: "您的 1Panel 面板有可升級的新版本,詳情請登入面板查看。" PanelPwdExpirationAlert: "您的 1Panel 面板密碼將於 {{ .day }} 天後到期,詳情請登入面板查看。" +CommonAlert: "您的 1Panel 面板,{{ .msg }},詳情請登入面板查看。" +NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 個節點存在異常,詳情請登入面板查看。" +LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 個許可證存在異常,詳情請登入面板查看。" +SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}從 {{ .ip }} 登錄異常,詳情請登入面板查看。" diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index c97ff1818daa..b707831229c4 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -448,4 +448,8 @@ SSLAlert: "您的 1Panel 面板,有 {{ .num }} 张SSL证书将在 {{ .day }} DiskUsedAlert: "您的 1Panel 面板,磁盘 {{ .name }} 已使用 {{ .used }},详情请登录面板查看。" ResourceAlert: "您的 1Panel 面板,平均 {{ .time }} 分钟内的 {{ .name }} 使用率为 {{ .used }},详情请登录面板查看。" PanelVersionAlert: "您的 1Panel 面板,有最新面板版本可供升级,详情请登录面板查看。" -PanelPwdExpirationAlert: "您的 1Panel 面板,面板密码将在 {{ .day }} 天后到期,详情请登录面板查看。" \ No newline at end of file +PanelPwdExpirationAlert: "您的 1Panel 面板,面板密码将在 {{ .day }} 天后到期,详情请登录面板查看。" +CommonAlert: "您的 1Panel 面板,{{ .msg }},详情请登录面板查看。" +NodeExceptionAlert: "您的 1Panel 面板,{{ .num }} 个节点存在异常,详情请登录面板查看。" +LicenseExceptionAlert: "您的 1Panel 面板,{{ .num }} 个许可证存在异常,详情请登录面板查看。" +SSHAndPanelLoginAlert: "您的 1Panel 面板,面板{{ .name }}登录{{ .ip }}异常,详情请登录面板查看。" \ No newline at end of file diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index 21e483d38c26..306bee2edd74 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -35,6 +35,7 @@ func InitAgentDB() { migrations.AddMethodToAlertTask, migrations.UpdateMcpServer, migrations.InitCronjobGroup, + migrations.AddColumnToAlert, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index fd9e1448ba78..f07adb0051c7 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -446,3 +446,18 @@ var InitCronjobGroup = &gormigrate.Migration{ return nil }, } + +var AddColumnToAlert = &gormigrate.Migration{ + ID: "20250729-add-column-to-alert", + Migrate: func(tx *gorm.DB) error { + if err := global.AlertDB.AutoMigrate(&model.Alert{}); err != nil { + return err + } + if err := global.AlertDB.Model(&model.Alert{}). + Where("advanced_params IS NULL"). + Update("advanced_params", "").Error; err != nil { + return err + } + return nil + }, +} diff --git a/agent/utils/alert/alert.go b/agent/utils/alert/alert.go index 82b68aa67e6e..5a19e019c1a9 100644 --- a/agent/utils/alert/alert.go +++ b/agent/utils/alert/alert.go @@ -2,21 +2,24 @@ package alert import ( "encoding/json" + "errors" "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/email" "github.com/jinzhu/copier" "net/http" + "os" + "os/exec" + "regexp" "strings" "sync" "time" - - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/global" ) var cronJobAlertTypes = []string{"shell", "app", "website", "database", "directory", "log", "snapshot", "curl", "cutWebsiteLog", "clean", "ntp"} @@ -66,7 +69,7 @@ func CreateEmailAlertLog(create dto.AlertLogCreate, alert dto.AlertDTO, params [ Encryption: emailInfo.Encryption, Recipient: emailInfo.Recipient, } - content := alert.Title + content := i18n.GetMsgWithMap("CommonAlert", map[string]interface{}{"msg": alert.Title}) if GetEmailContent(alert.Type, params) != "" { content = GetEmailContent(alert.Type, params) } @@ -191,7 +194,7 @@ func CreateAlertParams(param string) []dto.Param { var checkTaskMutex sync.Mutex -func CheckTaskFrequency(method string) bool { +func CheckSMSSendLimit(method string) bool { alertRepo := repo.NewIAlertRepo() config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig)) if err != nil { @@ -204,8 +207,8 @@ func CheckTaskFrequency(method string) bool { } limitCount := cfg.AlertDailyNum checkTaskMutex.Lock() - todayCount, err := alertRepo.GetLicensePushCount(method) defer checkTaskMutex.Unlock() + todayCount, err := alertRepo.GetLicensePushCount(method) if err != nil { global.LOG.Errorf("error getting license push count info, err: %v", err) return false @@ -312,6 +315,18 @@ func GetEmailContent(alertType string, params []dto.Param) string { return i18n.GetMsgWithMap("CronJobFailedAlert", map[string]interface{}{"name": getValueByIndex(params, "1")}) case "clams": return i18n.GetMsgWithMap("ClamAlert", map[string]interface{}{"num": getValueByIndex(params, "1")}) + case "panelLogin": + return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")}) + case "sshLogin": + return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")}) + case "panelIpLogin": + return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")}) + case "sshIpLogin": + return i18n.GetMsgWithMap("SSHAndPanelLoginAlert", map[string]interface{}{"name": getValueByIndex(params, "1"), "ip": getValueByIndex(params, "2")}) + case "nodeException": + return i18n.GetMsgWithMap("NodeExceptionAlert", map[string]interface{}{"num": getValueByIndex(params, "1")}) + case "licenseException": + return i18n.GetMsgWithMap("LicenseExceptionAlert", map[string]interface{}{"num": getValueByIndex(params, "1")}) default: return "" } @@ -325,3 +340,162 @@ func getValueByIndex(params []dto.Param, index string) string { } return "" } + +func CountRecentFailedLoginLogs(minutes uint, failCount uint) (int, bool, error) { + now := time.Now() + startTime := now.Add(-time.Duration(minutes) * time.Minute) + db := global.CoreDB.Model(&model.LoginLog{}) + var count int64 + err := db.Where("created_at >= ? AND status = ?", startTime, constant.StatusFailed). + Count(&count).Error + if err != nil { + return 0, false, err + } + return int(count), int(count) > int(failCount), nil +} + +func FindRecentSuccessLoginsNotInWhitelist(minutes int, whitelist []string) ([]model.LoginLog, error) { + now := time.Now() + startTime := now.Add(-time.Duration(minutes) * time.Minute) + + whitelistMap := make(map[string]struct{}) + for _, ip := range whitelist { + whitelistMap[ip] = struct{}{} + } + + var logs []model.LoginLog + err := global.CoreDB.Model(&model.LoginLog{}). + Where("created_at >= ? AND status = ?", startTime, constant.StatusSuccess). + Find(&logs).Error + if err != nil { + return nil, err + } + + var abnormalLogs []model.LoginLog + for _, log := range logs { + if _, ok := whitelistMap[log.IP]; !ok { + abnormalLogs = append(abnormalLogs, log) + } + } + return abnormalLogs, nil +} + +func CountRecentFailedSSHLog(minutes uint, maxAllowed uint) (int, bool, error) { + lines, err := grepSSHLog("Failed password") + if err != nil { + return 0, false, err + } + + thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute) + count := 0 + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + t, err := parseLogTime(line) + if err != nil { + continue + } + if t.After(thresholdTime) { + count++ + } + } + return count, count > int(maxAllowed), nil +} + +func FindRecentSuccessLoginNotInWhitelist(minutes int, whitelist []string) ([]string, error) { + lines, err := grepSSHLog("Accepted password") + if err != nil { + return nil, err + } + + thresholdTime := time.Now().Add(-time.Duration(minutes) * time.Minute) + var abnormalLogins []string + + whitelistMap := make(map[string]struct{}, len(whitelist)) + for _, ip := range whitelist { + whitelistMap[ip] = struct{}{} + } + + ipRegex := regexp.MustCompile(`from\s+([0-9.]+)\s+port\s+(\d+)`) + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + t, err := parseLogTime(line) + if err != nil || t.Before(thresholdTime) { + continue + } + + match := ipRegex.FindStringSubmatch(line) + if len(match) >= 2 { + ip := match[1] + if _, ok := whitelistMap[ip]; !ok { + abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", ip, t.Format("2006-01-02 15:04:05"))) + } + } + } + + return abnormalLogins, nil +} + +func findGrepPath() (string, error) { + path, err := exec.LookPath("grep") + if err != nil { + return "", fmt.Errorf("grep not found in PATH: %w", err) + } + return path, nil +} + +func grepSSHLog(keyword string) ([]string, error) { + logFiles := []string{"/var/log/secure", "/var/log/auth.log"} + var results []string + grepPath, err := findGrepPath() + if err != nil { + panic(err) + } + + for _, logFile := range logFiles { + if _, err := os.Stat(logFile); err != nil { + continue + } + cmd := exec.Command(grepPath, "-a", keyword, logFile) + output, err := cmd.Output() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ExitCode() == 1 { + continue + } + } + return nil, fmt.Errorf("read log file fail [%s]: %w", logFile, err) + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" { + results = append(results, line) + } + } + } + + return results, nil +} + +func parseLogTime(line string) (time.Time, error) { + if len(line) < 15 { + return time.Time{}, errors.New("log line time is incorrect") + } + timeStr := line[:15] + parsedTime, err := time.ParseInLocation("Jan 2 15:04:05", timeStr, time.Local) + if err != nil { + return time.Time{}, err + } + return parsedTime.AddDate(time.Now().Year(), 0, 0), nil +} diff --git a/agent/utils/alert_push/alert_push.go b/agent/utils/alert_push/alert_push.go index da7aeacecb0e..2f5a385390f9 100644 --- a/agent/utils/alert_push/alert_push.go +++ b/agent/utils/alert_push/alert_push.go @@ -31,7 +31,7 @@ func PushAlert(pushAlert dto.PushAlert) error { m = strings.TrimSpace(m) switch m { case constant.SMS: - if !alertUtil.CheckTaskFrequency(constant.SMS) { + if !alertUtil.CheckSMSSendLimit(constant.SMS) { continue } todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS) @@ -43,7 +43,7 @@ func PushAlert(pushAlert dto.PushAlert) error { AlertId: alert.ID, Count: todayCount + 1, } - _ = xpack.CreateTaskScanSMSAlertLog(alert, create, pushAlert, constant.SMS) + _ = xpack.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, constant.SMS) alertUtil.CreateNewAlertTask(strconv.Itoa(int(pushAlert.EntryID)), alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.SMS) case constant.Email: todayCount, _, err := alertRepo.LoadTaskCount(alertUtil.GetCronJobType(alert.Type), strconv.Itoa(int(pushAlert.EntryID)), constant.Email) diff --git a/agent/utils/xpack/xpack.go b/agent/utils/xpack/xpack.go index 798eb8bb7280..efee272eccc5 100644 --- a/agent/utils/xpack/xpack.go +++ b/agent/utils/xpack/xpack.go @@ -53,14 +53,22 @@ func IsUseCustomApp() bool { return false } -func CreateTaskScanSMSAlertLog(alert dto.AlertDTO, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error { +func CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error { return nil } -func CreateSMSAlertLog(info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error { +func CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error { return nil } +func GetLicenseErrorAlert() (uint, error) { + return 0, nil +} + +func GetNodeErrorAlert() (uint, error) { + return 0, nil +} + func LoadRequestTransport() *http.Transport { return &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, diff --git a/frontend/src/api/interface/alert.ts b/frontend/src/api/interface/alert.ts index 2fdb9d10e51c..7939930d8ec2 100644 --- a/frontend/src/api/interface/alert.ts +++ b/frontend/src/api/interface/alert.ts @@ -13,6 +13,7 @@ export namespace Alert { status: string; sendCount: number; sendMethod: string[]; + advancedParams: string; } export interface AlertDetail { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index db226c7b6b2b..717ad3d55af5 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -3583,7 +3583,7 @@ const message = { cpuUseExceedAvgHelper: 'The average cpu usage within the specified time exceeds the specified value', memoryUseExceedAvgHelper: 'The average memory usage within the specified time exceeds the specified value', loadUseExceedAvgHelper: 'The average load usage within the specified time exceeds the specified value', - resourceAlertRulesHelper: 'Note: Continuous alerts within 30 minutes will send only one SMS', + resourceAlertRulesHelper: 'Note: Continuous alerts within 30 minutes will send only one', specifiedTime: 'Specified Time', deleteTitle: 'Delete Alert', deleteMsg: 'Are you sure you want to delete the alert task?', @@ -3715,6 +3715,19 @@ const message = { portHelper: 'SSL usually uses 465, TLS usually uses 587', sslHelper: 'If the SMTP port is 465, SSL is usually required', tlsHelper: 'If the SMTP port is 587, TLS is usually required', + triggerCondition: 'Trigger Condition', + loginFail: ' login failures within', + nodeException: 'Node Exception Alert', + licenseException: 'License Exception Alert', + panelLogin: 'Panel Login Exception Alert', + sshLogin: 'SSH Login Exception Alert', + panelIpLogin: 'Panel Login IP Exception Alert', + sshIpLogin: 'SSH Login IP Exception Alert', + ipWhiteListHelper: 'IPs in the whitelist are not restricted by any rules', + nodeExceptionRule: 'Node exception alert, sent {0} times per day', + licenseExceptionRule: 'License exception alert, sent {0} times per day', + panelLoginRule: 'Panel login alert, sent {0} times per day', + sshLoginRule: 'SSH login alert, sent {0} times per day', }, theme: { lingXiaGold: 'Ling Xia Gold', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 0241dab088f6..f713cb1fd94a 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -3466,7 +3466,7 @@ const message = { cpuUseExceedAvgHelper: '指定時間内の平均CPU使用率が指定した値を超過', memoryUseExceedAvgHelper: '指定時間内の平均メモリ使用率が指定した値を超過', loadUseExceedAvgHelper: '指定時間内の平均負荷使用率が指定した値を超過', - resourceAlertRulesHelper: '注意:30分以内に連続してアラートが発生した場合、SMSは1回だけ送信されます', + resourceAlertRulesHelper: '注意:30分以内に連続してアラートが発生した場合、は1回だけ送信されます', specifiedTime: '指定時間', deleteTitle: 'アラートを削除', deleteMsg: 'アラートタスクを削除してもよろしいですか?', @@ -3594,6 +3594,19 @@ const message = { portHelper: 'SSLは通常465、TLSは通常587', sslHelper: 'SMTPポートが465の場合、通常はSSLが必要です', tlsHelper: 'SMTPポートが587の場合、通常はTLSが必要です', + triggerCondition: 'トリガー条件', + loginFail: '以内にログイン失敗', + nodeException: 'ノード異常アラート', + licenseException: 'ライセンス異常アラート', + panelLogin: 'パネルログイン異常アラート', + sshLogin: 'SSHログイン異常アラート', + panelIpLogin: 'パネルログインIP異常アラート', + sshIpLogin: 'SSHログインIP異常アラート', + ipWhiteListHelper: 'ホワイトリスト内のIPは、いかなるルールの制限も受けません', + nodeExceptionRule: 'ノード異常アラートは、1日あたり{0}回送信', + licenseExceptionRule: 'ライセンス異常アラートは、1日あたり{0}回送信', + panelLoginRule: 'パネルログインアラートは、1日あたり{0}回送信', + sshLoginRule: 'SSHログインアラートは、1日あたり{0}回送信', }, theme: { lingXiaGold: '凌霞金', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index e8dedb2c2481..1c46fd8d3533 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -3404,7 +3404,7 @@ const message = { cpuUseExceedAvgHelper: '지정된 시간 내의 평균 CPU 사용량이 지정된 값을 초과함', memoryUseExceedAvgHelper: '지정된 시간 내의 평균 메모리 사용량이 지정된 값을 초과함', loadUseExceedAvgHelper: '지정된 시간 내의 평균 부하 사용량이 지정된 값을 초과함', - resourceAlertRulesHelper: '참고: 30분 내에 연속적인 알림은 SMS 한 번만 발송됩니다', + resourceAlertRulesHelper: '참고: 30분 내에 연속적인 알림은 한 번만 발송됩니다', specifiedTime: '지정된 시간', deleteTitle: '알림 삭제', deleteMsg: '알림 작업을 삭제하시겠습니까?', @@ -3529,6 +3529,19 @@ const message = { portHelper: 'SSL 은 일반적으로 465, TLS 는 587', sslHelper: 'SMTP 포트가 465 이면 일반적으로 SSL 이 필요합니다', tlsHelper: 'SMTP 포트가 587 이면 일반적으로 TLS 가 필요합니다', + triggerCondition: '트리거 조건', + loginFail: ' 이내 로그인 실패', + nodeException: '노드 이상 알림', + licenseException: '라이선스 이상 알림', + panelLogin: '패널 로그인 이상 알림', + sshLogin: 'SSH 로그인 이상 알림', + panelIpLogin: '패널 로그인 IP 이상 알림', + sshIpLogin: 'SSH 로그인 IP 이상 알림', + ipWhiteListHelper: '화이트리스트에 있는 IP는 어떠한 규칙의 제한도 받지 않습니다', + nodeExceptionRule: '노드 이상 알림은 하루 {0}회 전송', + licenseExceptionRule: '라이선스 이상 알림은 하루 {0}회 전송', + panelLoginRule: '패널 로그인 알림은 하루 {0}회 전송', + sshLoginRule: 'SSH 로그인 알림은 하루 {0}회 전송', }, theme: { lingXiaGold: '링샤 골드', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 6d489bbcfc9c..ef446f853fd2 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -3545,7 +3545,7 @@ const message = { cpuUseExceedAvgHelper: 'Penggunaan CPU purata dalam masa tertentu melebihi nilai yang ditetapkan', memoryUseExceedAvgHelper: 'Penggunaan memori purata dalam masa tertentu melebihi nilai yang ditetapkan', loadUseExceedAvgHelper: 'Penggunaan beban purata dalam masa tertentu melebihi nilai yang ditetapkan', - resourceAlertRulesHelper: 'Nota: Amaran berterusan dalam masa 30 minit hanya akan menghantar satu SMS', + resourceAlertRulesHelper: 'Nota: Amaran berterusan dalam masa 30 minit hanya akan menghantar satu', specifiedTime: 'Masa Tertentu', deleteTitle: 'Padam Amaran', deleteMsg: 'Adakah anda pasti ingin memadam tugas amaran?', @@ -3679,6 +3679,19 @@ const message = { portHelper: 'SSL biasanya 465, TLS biasanya 587', sslHelper: 'Jika port SMTP ialah 465, SSL biasanya diperlukan', tlsHelper: 'Jika port SMTP ialah 587, TLS biasanya diperlukan', + triggerCondition: 'Syarat Pencetus', + loginFail: ' kegagalan log masuk dalam', + nodeException: 'Amaran Kerosakan Nod', + licenseException: 'Amaran Kerosakan Lesen', + panelLogin: 'Amaran Log Masuk Panel Tidak Normal', + sshLogin: 'Amaran Log Masuk SSH Tidak Normal', + panelIpLogin: 'Amaran IP Log Masuk Panel Tidak Normal', + sshIpLogin: 'Amaran IP Log Masuk SSH Tidak Normal', + ipWhiteListHelper: 'IP dalam senarai putih tidak tertakluk kepada sebarang peraturan', + nodeExceptionRule: 'Amaran kerosakan nod, dihantar {0} kali sehari', + licenseExceptionRule: 'Amaran kerosakan lesen, dihantar {0} kali sehari', + panelLoginRule: 'Amaran log masuk panel, dihantar {0} kali sehari', + sshLoginRule: 'Amaran log masuk SSH, dihantar {0} kali sehari', }, theme: { lingXiaGold: 'Ling Xia Emas', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 5b99c7d2072a..45951ed0f033 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -3554,7 +3554,7 @@ const message = { cpuUseExceedAvgHelper: 'O uso médio da CPU dentro do tempo especificado excede o valor especificado', memoryUseExceedAvgHelper: 'O uso médio da memória dentro do tempo especificado excede o valor especificado', loadUseExceedAvgHelper: 'O uso médio da carga dentro do tempo especificado excede o valor especificado', - resourceAlertRulesHelper: 'Nota: Alertas contínuos em 30 minutos enviarão apenas um SMS', + resourceAlertRulesHelper: 'Nota: Alertas contínuos em 30 minutos enviarão apenas um', specifiedTime: 'Hora Especificada', deleteTitle: 'Excluir Alerta', deleteMsg: 'Tem certeza de que deseja excluir a tarefa de alerta?', @@ -3687,6 +3687,19 @@ const message = { portHelper: 'SSL geralmente usa 465, TLS geralmente usa 587', sslHelper: 'Se a porta SMTP for 465, normalmente é necessário SSL', tlsHelper: 'Se a porta SMTP for 587, normalmente é necessário TLS', + triggerCondition: 'Condição de Disparo', + loginFail: ' falhas de login em', + nodeException: 'Alerta de Exceção de Nó', + licenseException: 'Alerta de Exceção de Licença', + panelLogin: 'Alerta de Exceção de Login no Painel', + sshLogin: 'Alerta de Exceção de Login SSH', + panelIpLogin: 'Alerta de Exceção de IP de Login no Painel', + sshIpLogin: 'Alerta de Exceção de IP de Login SSH', + ipWhiteListHelper: 'IPs na lista branca não estão sujeitos a nenhuma regra', + nodeExceptionRule: 'Alerta de exceção de nó, enviado {0} vezes por dia', + licenseExceptionRule: 'Alerta de exceção de licença, enviado {0} vezes por dia', + panelLoginRule: 'Alerta de login no painel, enviado {0} vezes por dia', + sshLoginRule: 'Alerta de login SSH, enviado {0} vezes por dia', }, theme: { lingXiaGold: 'Ling Xia Gold', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 3a13371535bb..dbee0e4f5e52 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -3540,7 +3540,7 @@ const message = { cpuUseExceedAvgHelper: 'Среднее использование процессора за указанное время превышает заданное значение', memoryUseExceedAvgHelper: 'Среднее использование памяти за указанное время превышает заданное значение', loadUseExceedAvgHelper: 'Средняя нагрузка за указанное время превышает заданное значение', - resourceAlertRulesHelper: 'Примечание: Непрерывные уведомления в течение 30 минут отправят только одно SMS', + resourceAlertRulesHelper: 'Примечание: Непрерывные уведомления в течение 30 минут отправят только одно', specifiedTime: 'Указанное Время', deleteTitle: 'Удалить Уведомление', deleteMsg: 'Вы уверены, что хотите удалить задачу уведомления?', @@ -3678,6 +3678,19 @@ const message = { portHelper: 'SSL обычно использует 465, TLS — 587', sslHelper: 'Если порт SMTP — 465, обычно требуется SSL', tlsHelper: 'Если порт SMTP — 587, обычно требуется TLS', + triggerCondition: 'Условие срабатывания', + loginFail: ' неудачных попыток входа в течение', + nodeException: 'Оповещение о сбое узла', + licenseException: 'Оповещение о сбое лицензии', + panelLogin: 'Оповещение о сбое входа в панель', + sshLogin: 'Оповещение о сбое входа по SSH', + panelIpLogin: 'Оповещение о сбое IP входа в панель', + sshIpLogin: 'Оповещение о сбое IP входа по SSH', + ipWhiteListHelper: 'IP-адреса в белом списке не подпадают под действие каких-либо правил', + nodeExceptionRule: 'Оповещение о сбое узла, отправляется {0} раз в день', + licenseExceptionRule: 'Оповещение о сбое лицензии, отправляется {0} раз в день', + panelLoginRule: 'Оповещение о входе в панель, отправляется {0} раз в день', + sshLoginRule: 'Оповещение о входе по SSH, отправляется {0} раз в день', }, theme: { lingXiaGold: 'Лин Ся Золотой', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 5a86e9a2e924..1f6f33f6aa02 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -3622,7 +3622,7 @@ const message = { cpuUseExceedAvgHelper: 'Belirtilen süre içinde ortalama CPU kullanımı belirtilen değeri aşar', memoryUseExceedAvgHelper: 'Belirtilen süre içinde ortalama bellek kullanımı belirtilen değeri aşar', loadUseExceedAvgHelper: 'Belirtilen süre içinde ortalama yük kullanımı belirtilen değeri aşar', - resourceAlertRulesHelper: 'Not: 30 dakika içinde sürekli uyarılar yalnızca bir SMS gönderir', + resourceAlertRulesHelper: 'Not: 30 dakika içinde sürekli uyarılar yalnızca bir gönderir', specifiedTime: 'Belirtilen Süre', deleteTitle: 'Uyarıyı Sil', deleteMsg: 'Uyarı görevini silmek istediğinizden emin misiniz?', @@ -3757,6 +3757,19 @@ const message = { portHelper: 'SSL genellikle 465, TLS genellikle 587', sslHelper: 'SMTP portu 465 ise genellikle SSL gerekir', tlsHelper: 'SMTP portu 587 ise genellikle TLS gerekir', + triggerCondition: 'Tetikleme Koşulu', + loginFail: ' içinde oturum açma başarısızlığı', + nodeException: 'Düğüm Hatası Uyarısı', + licenseException: 'Lisans Hatası Uyarısı', + panelLogin: 'Panel Girişi Hatası Uyarısı', + sshLogin: 'SSH Girişi Hatası Uyarısı', + panelIpLogin: 'Panel Girişi IP Hatası Uyarısı', + sshIpLogin: 'SSH Girişi IP Hatası Uyarısı', + ipWhiteListHelper: 'Beyaz listedeki IP’ler herhangi bir kuralla kısıtlanmaz', + nodeExceptionRule: 'Düğüm hatası uyarısı, günde {0} kez gönderilir', + licenseExceptionRule: 'Lisans hatası uyarısı, günde {0} kez gönderilir', + panelLoginRule: 'Panel girişi uyarısı, günde {0} kez gönderilir', + sshLoginRule: 'SSH girişi uyarısı, günde {0} kez gönderilir', }, theme: { lingXiaGold: 'Ling Xia Altın', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 3f71cbe9e540..96a79504f308 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -3337,7 +3337,7 @@ const message = { cpuUseExceedAvgHelper: '指定時間內 CPU 平均使用率超過指定值', memoryUseExceedAvgHelper: '指定時間內記憶體平均使用率超過指定值', loadUseExceedAvgHelper: '指定時間內負載平均使用率超過指定值', - resourceAlertRulesHelper: '注意:30分鐘內持續告警只發送一次簡訊', + resourceAlertRulesHelper: '注意:30分鐘內持續告警只發送一次', specifiedTime: '指定時間', deleteTitle: '删除告警', deleteMsg: '是否確認删除告警任務?', @@ -3460,6 +3460,19 @@ const message = { portHelper: 'SSL 通常為 465,TLS 通常為 587', sslHelper: '若 SMTP 連接埠為 465,通常需要啟用 SSL', tlsHelper: '若 SMTP 連接埠為 587,通常需要啟用 TLS', + triggerCondition: '觸發條件', + loginFail: '內,登入失敗', + nodeException: '節點異常告警', + licenseException: '許可證異常告警', + panelLogin: '面板登入異常告警', + sshLogin: 'SSH 登入異常告警', + panelIpLogin: '面板登入 IP 異常告警', + sshIpLogin: 'SSH 登入 IP 異常告警', + ipWhiteListHelper: '白名單中的 IP 不受任何規則限制', + nodeExceptionRule: '節點異常告警,每天發送 {0} 次', + licenseExceptionRule: '許可證異常告警,每天發送 {0} 次', + panelLoginRule: '面板登入告警,每天發送 {0} 次', + sshLoginRule: 'SSH 登入告警,每天發送 {0} 次', }, theme: { lingXiaGold: '凌霞金', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 4dd4199420dd..314111e5939b 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -3306,7 +3306,7 @@ const message = { cpuUseExceedAvgHelper: '指定时间内 CPU 平均使用率超过指定值', memoryUseExceedAvgHelper: '指定时间内内存平均使用率超过指定值', loadUseExceedAvgHelper: '指定时间内负载平均使用率超过指定值', - resourceAlertRulesHelper: '注意:30 分钟内持续告警只发送一次短信', + resourceAlertRulesHelper: '注意:30 分钟内持续告警只发送一次', specifiedTime: '指定时间', deleteTitle: '删除告警', deleteMsg: '是否确认删除告警任务?', @@ -3430,6 +3430,19 @@ const message = { portHelper: 'SSL 通常为465,TLS 通常为587', sslHelper: '如果 SMTP 端口是 465,通常需要启用 SSL', tlsHelper: '如果 SMTP 端口是 587,通常需要启用 TLS', + triggerCondition: '触发条件', + loginFail: '内,登录失败', + nodeException: '节点异常告警', + licenseException: '许可证异常告警', + panelLogin: '面板登录异常告警', + sshLogin: 'SSH 登录异常告警', + panelIpLogin: '面板登录 IP 异常告警', + sshIpLogin: 'SSH 登录 IP 异常告警', + ipWhiteListHelper: '白名单中的 IP 不受任何规则限制', + nodeExceptionRule: '节点异常告警,每天发送 {0} 次', + licenseExceptionRule: '许可证异常告警,每天发送 {0} 次', + panelLoginRule: '面板登录告警,每天发送 {0} 次', + sshLoginRule: 'SSH 登录告警告警,每天发送 {0} 次', }, theme: { lingXiaGold: '凌霞金', diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts index 97dddea2fd3c..53455a6527c8 100644 --- a/frontend/src/utils/util.ts +++ b/frontend/src/utils/util.ts @@ -675,14 +675,12 @@ let fileTypes = { text: ['.iso', '.tiff', '.exe', '.so', '.bz', '.dmg', '.apk', '.pptx', '.ppt', '.xlsb'], }; -export const getFileType = (extension: string) => { - let type = 'text'; - Object.entries(fileTypes).forEach(([key, extensions]) => { - if (extensions.includes(extension.toLowerCase())) { - type = key; - } - }); - return type; +export const getFileType = (extension?: string) => { + const ext = extension?.toLowerCase(); + if (!ext) return 'text'; + + const match = Object.entries(fileTypes).find((extensions) => extensions.includes(ext)); + return match ? match[0] : 'text'; }; export const newUUID = () => { diff --git a/frontend/src/views/setting/alert/dash/index.vue b/frontend/src/views/setting/alert/dash/index.vue index 8c9b36caac4d..48582e8782d6 100644 --- a/frontend/src/views/setting/alert/dash/index.vue +++ b/frontend/src/views/setting/alert/dash/index.vue @@ -20,7 +20,11 @@ + @@ -238,6 +242,10 @@ const formatRule = (row: Alert.AlertInfo) => { cutWebsiteLog: () => t('xpack.alert.cronJobCutWebsiteLogRule', [row.sendCount]), clean: () => t('xpack.alert.cronJobCleanRule', [row.sendCount]), ntp: () => t('xpack.alert.cronJobNtpRule', [row.sendCount]), + nodeException: () => t('xpack.alert.nodeExceptionRule', [row.sendCount]), + licenseException: () => t('xpack.alert.licenseExceptionRule', [row.sendCount]), + panelLogin: () => t('xpack.alert.panelLoginRule', [row.sendCount]), + sshLogin: () => t('xpack.alert.sshLoginRule', [row.sendCount]), }; return ruleTemplates[row.type] ? ruleTemplates[row.type]() : ''; diff --git a/frontend/src/views/setting/alert/dash/task/index.vue b/frontend/src/views/setting/alert/dash/task/index.vue index 369a8bb90a80..f833e9317291 100644 --- a/frontend/src/views/setting/alert/dash/task/index.vue +++ b/frontend/src/views/setting/alert/dash/task/index.vue @@ -16,18 +16,14 @@ v-model="dialogData.rowData!.type" :disabled="dialogData.title === 'edit'" > - - -