diff --git a/README.md b/README.md index 71f5bb8..7aacf63 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- WatchAlert 开源一站式监控告警管理系统 + WatchAlert 开源一站式多数据源监控告警引擎

@@ -18,7 +18,7 @@ License ## 💎 项目介绍 -WatchAlert 是一款为云原生环境量身打造的轻量级监控告警系统,专注于**可观测稳定性**主题,提供全面的监控与告警支持。 +WatchAlert 是一款为云原生环境量身打造的轻量级监控告警引擎,专注于**可观测稳定性**主题,提供全面的监控与告警支持。 **能力** - Metrics 监控 @@ -31,6 +31,8 @@ WatchAlert 是一款为云原生环境量身打造的轻量级监控告警系统 - 集成:Kubernetes - Network 监控 - 集成:HTTP、ICMP、TCP、SSL +- 告警通知 + - 飞书、钉钉、企业微信、邮件、自定义Hook **为什么选择 WatchAlert?** @@ -49,7 +51,7 @@ WatchAlert 是一款为云原生环境量身打造的轻量级监控告警系统 - 演示环境:http://8.147.234.89/login (admin/123) -| ![登陆页](assets/login.png) | ![首页](assets/home.png) | +| ![登陆页](assets/login.png) | ![首页](assets/home.png) | |:------------------------------:|--------------------------------| | ![img.png](assets/img.png) | ![img_1.png](assets/img_1.png) | | ![img_2.png](assets/img_2.png) | ![img_3.png](assets/img_3.png) | diff --git a/alert/consumer/consumer.go b/alert/consumer/consumer.go index 2d44732..641f93b 100644 --- a/alert/consumer/consumer.go +++ b/alert/consumer/consumer.go @@ -302,8 +302,13 @@ func (ec *Consume) handleAlert(rule models.AlertRule, alerts []models.AlertCurEv return } - n := templates.NewTemplate(ec.ctx, alert, noticeData) - err := sender.Sender(ec.ctx, sender.SendParmas{ + var content string + if noticeData.NoticeType == "CustomHook" { + content = tools.JsonMarshal(alert) + } else { + content = templates.NewTemplate(ec.ctx, alert, noticeData).CardContentMsg + } + err := sender.Sender(ec.ctx, sender.SendParams{ TenantId: alert.TenantId, RuleName: alert.RuleName, Severity: alert.Severity, @@ -313,7 +318,7 @@ func (ec *Consume) handleAlert(rule models.AlertRule, alerts []models.AlertCurEv IsRecovered: alert.IsRecovered, Hook: noticeData.Hook, Email: noticeData.Email, - Content: n.CardContentMsg, + Content: content, Event: nil, }) if err != nil { diff --git a/alert/consumer/subscribe.go b/alert/consumer/subscribe.go index 22e68d8..ab3d063 100644 --- a/alert/consumer/subscribe.go +++ b/alert/consumer/subscribe.go @@ -64,7 +64,15 @@ func processSubscribe(ctx *ctx.Context, alert models.AlertCurEvent, notice model notice.NoticeTmplId = u.NoticeTemplateId emailTemp := templates.NewTemplate(ctx, alert, notice) - err = sender.SendToEmail(alert.IsRecovered, u.NoticeSubject, []string{u.Email}, nil, emailTemp.CardContentMsg) + err = sender.NewEmailSender().Send(sender.SendParams{ + IsRecovered: alert.IsRecovered, + Email: models.Email{ + Subject: u.NoticeSubject, + To: []string{u.Email}, + CC: nil, + }, + Content: emailTemp.CardContentMsg, + }) if err != nil { return fmt.Errorf("邮件发送失败, err: %s", err.Error()) } diff --git a/alert/probing/consumer.go b/alert/probing/consumer.go index 1c46f93..d65b48e 100644 --- a/alert/probing/consumer.go +++ b/alert/probing/consumer.go @@ -9,6 +9,7 @@ import ( "watchAlert/pkg/ctx" "watchAlert/pkg/sender" "watchAlert/pkg/templates" + "watchAlert/pkg/tools" ) type ConsumeProbing struct { @@ -69,8 +70,13 @@ func (m *ConsumeProbing) handleAlert(alert models.ProbingEvent) { noticeData, _ := ctx.DB.Notice().Get(r) alert.DutyUser = process.GetDutyUser(m.ctx, noticeData) - n := templates.NewTemplate(m.ctx, buildEvent(alert), noticeData) - err := sender.Sender(m.ctx, sender.SendParmas{ + var content string + if noticeData.NoticeType == "CustomHook" { + content = tools.JsonMarshal(alert) + } else { + content = templates.NewTemplate(m.ctx, buildEvent(alert), noticeData).CardContentMsg + } + err := sender.Sender(m.ctx, sender.SendParams{ TenantId: alert.TenantId, Severity: alert.Severity, NoticeType: noticeData.NoticeType, @@ -79,7 +85,7 @@ func (m *ConsumeProbing) handleAlert(alert models.ProbingEvent) { IsRecovered: alert.IsRecovered, Hook: noticeData.Hook, Email: noticeData.Email, - Content: n.CardContentMsg, + Content: content, Event: nil, }) if err != nil { diff --git a/alert/process/process.go b/alert/process/process.go index f8f3e72..184b8fb 100644 --- a/alert/process/process.go +++ b/alert/process/process.go @@ -161,20 +161,19 @@ func GetNoticeGroupId(alert models.AlertCurEvent) string { } func GetDutyUser(ctx *ctx.Context, noticeData models.AlertNotice) string { - user := ctx.DB.DutyCalendar().GetDutyUserInfo(noticeData.DutyId, time.Now().Format("2006-1-2")) - switch noticeData.NoticeType { - case "FeiShu": - // 判断是否有安排值班人员 - if len(user.DutyUserId) > 1 { + user, ok := ctx.DB.DutyCalendar().GetDutyUserInfo(noticeData.DutyId, time.Now().Format("2006-1-2")) + if ok { + switch noticeData.NoticeType { + case "FeiShu": return fmt.Sprintf("", user.DutyUserId) - } - case "DingDing": - if len(user.DutyUserId) > 1 { + case "DingDing": return fmt.Sprintf("%s", user.DutyUserId) + case "Email", "WeChat", "CustomHook": + return fmt.Sprintf("@%s", user.UserName) } } - return "" + return "暂无" } // RecordAlertHisEvent 记录历史告警 diff --git a/assets/login.png b/assets/login.png index b6dcb45..9138907 100644 Binary files a/assets/login.png and b/assets/login.png differ diff --git a/internal/models/dingding.go b/internal/models/template_dingding.go similarity index 100% rename from internal/models/dingding.go rename to internal/models/template_dingding.go diff --git a/internal/models/feishu.go b/internal/models/template_feishu.go similarity index 100% rename from internal/models/feishu.go rename to internal/models/template_feishu.go diff --git a/internal/models/template_wechat.go b/internal/models/template_wechat.go new file mode 100644 index 0000000..2e90ee1 --- /dev/null +++ b/internal/models/template_wechat.go @@ -0,0 +1,10 @@ +package models + +type WeChatMsgTemplate struct { + MsgType string `json:"msgtype"` + MarkDown WeChatMarkDown `json:"markdown"` +} + +type WeChatMarkDown struct { + Content string `json:"content"` +} diff --git a/internal/repo/duty_schedule.go b/internal/repo/duty_schedule.go index 8d744cc..200758a 100644 --- a/internal/repo/duty_schedule.go +++ b/internal/repo/duty_schedule.go @@ -14,7 +14,7 @@ type ( InterDutyCalendar interface { GetCalendarInfo(dutyId, time string) models.DutySchedule - GetDutyUserInfo(dutyId, time string) models.Member + GetDutyUserInfo(dutyId, time string) (models.Member, bool) Create(r models.DutySchedule) error Update(r models.DutySchedule) error Search(r models.DutyScheduleQuery) ([]models.DutySchedule, error) @@ -43,16 +43,19 @@ func (dc DutyCalendarRepo) GetCalendarInfo(dutyId, time string) models.DutySched } // GetDutyUserInfo 获取值班用户信息 -func (dc DutyCalendarRepo) GetDutyUserInfo(dutyId, time string) models.Member { +func (dc DutyCalendarRepo) GetDutyUserInfo(dutyId, time string) (models.Member, bool) { var user models.Member - schedule := dc.GetCalendarInfo(dutyId, time) + db := dc.db.Model(models.Member{}). + Where("user_id = ?", schedule.UserId) + if err := db.First(&user).Error; err != nil { + return user, false + } + if user.JoinDuty == "true" { + return user, true + } - dc.db.Model(models.Member{}). - Where("user_id = ?", schedule.UserId). - First(&user) - - return user + return user, false } func (dc DutyCalendarRepo) Create(r models.DutySchedule) error { diff --git a/pkg/sender/dingding.go b/pkg/sender/dingding.go index 028aca6..e5717de 100644 --- a/pkg/sender/dingding.go +++ b/pkg/sender/dingding.go @@ -7,19 +7,28 @@ import ( "watchAlert/pkg/tools" ) -type DingResponseMsg struct { - Code int `json:"errcode"` - Msg string `json:"errmsg"` +type ( + // DingDingSender 钉钉发送策略 + DingDingSender struct{} + + DingResponse struct { + Code int `json:"errcode"` + Msg string `json:"errmsg"` + } +) + +func NewDingSender() SendInter { + return &DingDingSender{} } -func SendToDingDing(hook, msg string) error { - cardContentByte := bytes.NewReader([]byte(msg)) - res, err := tools.Post(nil, hook, cardContentByte, 10) +func (d *DingDingSender) Send(params SendParams) error { + cardContentByte := bytes.NewReader([]byte(params.Content)) + res, err := tools.Post(nil, params.Hook, cardContentByte, 10) if err != nil { return err } - var response DingResponseMsg + var response DingResponse if err := tools.ParseReaderBody(res.Body, &response); err != nil { return errors.New(fmt.Sprintf("Error unmarshalling Dingding response: %s", err.Error())) } diff --git a/pkg/sender/email.go b/pkg/sender/email.go index e28cb11..fd52de7 100644 --- a/pkg/sender/email.go +++ b/pkg/sender/email.go @@ -7,20 +7,27 @@ import ( "watchAlert/pkg/ctx" ) -func SendToEmail(IsRecovered bool, subject string, to, cc []string, msg string) error { +// EmailSender 邮件发送策略 +type EmailSender struct{} + +func NewEmailSender() SendInter { + return &EmailSender{} +} + +func (e *EmailSender) Send(params SendParams) error { setting, err := ctx.DB.Setting().Get() if err != nil { return errors.New("获取系统配置失败: " + err.Error()) } eCli := client.NewEmailClient(setting.EmailConfig.ServerAddress, setting.EmailConfig.Email, setting.EmailConfig.Token, setting.EmailConfig.Port) - if IsRecovered { - subject = subject + "「已恢复」" + if params.IsRecovered { + params.Email.Subject = params.Email.Subject + "「已恢复」" } else { - subject = subject + "「报警中」" + params.Email.Subject = params.Email.Subject + "「报警中」" } - err = eCli.Send(to, cc, subject, []byte(msg)) + err = eCli.Send(params.Email.To, params.Email.CC, params.Email.Subject, []byte(params.Content)) if err != nil { - return fmt.Errorf("%s, %s", err.Error(), "Content: "+msg) + return fmt.Errorf("%s, %s", err.Error(), "Content: "+params.Content) } return nil diff --git a/pkg/sender/entry.go b/pkg/sender/entry.go index d88c660..29d6e6c 100644 --- a/pkg/sender/entry.go +++ b/pkg/sender/entry.go @@ -8,66 +8,83 @@ import ( "watchAlert/pkg/ctx" ) -type SendParmas struct { - // 基础 - TenantId string - RuleName string - Severity string - // 通知 - NoticeType string - NoticeId string - NoticeName string - // 恢复通知 - IsRecovered bool - // hook 地址 - Hook string - // 邮件 - Email models.Email - // 消息 - Content string - // 事件 - Event interface{} +type ( + // SendParams 定义发送参数 + SendParams struct { + // 基础 + TenantId string + RuleName string + Severity string + // 通知 + NoticeType string + NoticeId string + NoticeName string + // 恢复通知 + IsRecovered bool + // hook 地址 + Hook string + // 邮件 + Email models.Email + // 消息 + Content string + // 事件 + Event interface{} + } + + // SendInter 发送通知的接口 + SendInter interface { + Send(params SendParams) error + } +) + +// Sender 发送通知的主函数 +func Sender(ctx *ctx.Context, sendParams SendParams) error { + // 根据通知类型获取对应的发送器 + sender, err := senderFactory(sendParams.NoticeType) + if err != nil { + return fmt.Errorf("Send alarm failed, %s", err.Error()) + } + + // 发送通知 + if err := sender.Send(sendParams); err != nil { + addRecord(ctx, sendParams, 1, sendParams.Content, err.Error()) + return fmt.Errorf("Send alarm failed to %s, err: %s", sendParams.NoticeType, err.Error()) + } + + // 记录成功发送的日志 + addRecord(ctx, sendParams, 0, sendParams.Content, "") + logc.Info(ctx.Ctx, fmt.Sprintf("Send alarm ok, msg: %s", sendParams.Content)) + return nil } -func Sender(ctx *ctx.Context, sendParmas SendParmas) error { - NoticeType := sendParmas.NoticeType - var sendFunc func() error - switch NoticeType { +// senderFactory 创建发送器的工厂函数 +func senderFactory(noticeType string) (SendInter, error) { + switch noticeType { case "Email": - sendFunc = func() error { - return SendToEmail(sendParmas.IsRecovered, sendParmas.Email.Subject, sendParmas.Email.To, sendParmas.Email.CC, sendParmas.Content) - } + return NewEmailSender(), nil case "FeiShu": - sendFunc = func() error { - return SendToFeiShu(sendParmas.Hook, sendParmas.Content) - } + return NewFeiShuSender(), nil case "DingDing": - sendFunc = func() error { - return SendToDingDing(sendParmas.Hook, sendParmas.Content) - } + return NewDingSender(), nil + case "WeChat": + return NewWeChatSender(), nil + case "CustomHook": + return NewWebHookSender(), nil default: - return fmt.Errorf("Send alarm failed, exist 无效的通知类型: %s", sendParmas.NoticeType) + return nil, fmt.Errorf("无效的通知类型: %s", noticeType) } - - if err := sendFunc(); err != nil { - addRecord(ctx, sendParmas, 1, sendParmas.Content, err.Error()) - return fmt.Errorf("Send alarm failed to %s, err: %s", sendParmas.NoticeType, err.Error()) - } - - addRecord(ctx, sendParmas, 0, sendParmas.Content, "") - logc.Info(ctx.Ctx, fmt.Sprintf("Send alarm ok, msg: %s", sendParmas.Content)) - return nil } -func addRecord(ctx *ctx.Context, sendParmas SendParmas, status int, msg, errMsg string) { +// addRecord 记录通知发送结果 +func addRecord(ctx *ctx.Context, sendParams SendParams, status int, msg, errMsg string) { err := ctx.DB.Notice().AddRecord(models.NoticeRecord{ Date: time.Now().Format("2006-01-02"), CreateAt: time.Now().Unix(), - TenantId: sendParmas.TenantId, - RuleName: sendParmas.RuleName, - NType: sendParmas.NoticeType, - NObj: sendParmas.NoticeName + " (" + sendParmas.NoticeId + ")", - Severity: sendParmas.Severity, + TenantId: sendParams.TenantId, + RuleName: sendParams.RuleName, + NType: sendParams.NoticeType, + NObj: sendParams.NoticeName + " (" + sendParams.NoticeId + ")", + Severity: sendParams.Severity, Status: status, AlarmMsg: msg, ErrMsg: errMsg, diff --git a/pkg/sender/feishu.go b/pkg/sender/feishu.go index 26ac9b6..dd92ca1 100644 --- a/pkg/sender/feishu.go +++ b/pkg/sender/feishu.go @@ -7,19 +7,28 @@ import ( "watchAlert/pkg/tools" ) -type FeiShuResponseMsg struct { - Code int `json:"code"` - Msg string `json:"msg"` +type ( + // FeiShuSender 飞书发送策略 + FeiShuSender struct{} + + FeiShuResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + } +) + +func NewFeiShuSender() SendInter { + return &FeiShuSender{} } -func SendToFeiShu(hook, msg string) error { - cardContentByte := bytes.NewReader([]byte(msg)) - res, err := tools.Post(nil, hook, cardContentByte, 10) +func (f *FeiShuSender) Send(params SendParams) error { + cardContentByte := bytes.NewReader([]byte(params.Content)) + res, err := tools.Post(nil, params.Hook, cardContentByte, 10) if err != nil { return err } - var response FeiShuResponseMsg + var response FeiShuResponse if err := tools.ParseReaderBody(res.Body, &response); err != nil { return errors.New(fmt.Sprintf("Error unmarshalling Feishu response: %s", err.Error())) } diff --git a/pkg/sender/webhook.go b/pkg/sender/webhook.go new file mode 100644 index 0000000..a703dbc --- /dev/null +++ b/pkg/sender/webhook.go @@ -0,0 +1,36 @@ +package sender + +import ( + "bytes" + "errors" + "fmt" + "io" + "watchAlert/pkg/tools" +) + +type ( + // WebHookSender 自定义Hook发送策略 + WebHookSender struct{} +) + +func NewWebHookSender() SendInter { + return &WebHookSender{} +} + +func (w *WebHookSender) Send(params SendParams) error { + cardContentByte := bytes.NewReader([]byte(params.Content)) + res, err := tools.Post(nil, params.Hook, cardContentByte, 10) + if err != nil { + return err + } + + if res.StatusCode != 200 { + bodyByte, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("读取 Body 失败, err: %s", err.Error()) + } + return errors.New(string(bodyByte)) + } + + return nil +} diff --git a/pkg/sender/wechat.go b/pkg/sender/wechat.go new file mode 100644 index 0000000..ce246c7 --- /dev/null +++ b/pkg/sender/wechat.go @@ -0,0 +1,40 @@ +package sender + +import ( + "bytes" + "errors" + "fmt" + "watchAlert/pkg/tools" +) + +type ( + // WeChatSender 企业微信发送策略 + WeChatSender struct{} + + WeChatResponse struct { + Code int `json:"errcode"` + Msg string `json:"errmsg"` + } +) + +func NewWeChatSender() SendInter { + return &WeChatSender{} +} + +func (w *WeChatSender) Send(params SendParams) error { + cardContentByte := bytes.NewReader([]byte(params.Content)) + res, err := tools.Post(nil, params.Hook, cardContentByte, 10) + if err != nil { + return err + } + + var response WeChatResponse + if err := tools.ParseReaderBody(res.Body, &response); err != nil { + return errors.New(fmt.Sprintf("Error unmarshalling Feishu response: %s", err.Error())) + } + if response.Code != 0 { + return errors.New(response.Msg) + } + + return nil +} diff --git a/pkg/templates/new.go b/pkg/templates/new.go index aa1570b..135503a 100644 --- a/pkg/templates/new.go +++ b/pkg/templates/new.go @@ -18,6 +18,8 @@ func NewTemplate(ctx *ctx.Context, alert models.AlertCurEvent, notice models.Ale return Template{CardContentMsg: dingdingTemplate(alert, noticeTmpl)} case "Email": return Template{CardContentMsg: emailTemplate(alert, noticeTmpl)} + case "WeChat": + return Template{CardContentMsg: wechatTemplate(alert, noticeTmpl)} } return Template{} diff --git a/pkg/templates/wechatCard.go b/pkg/templates/wechatCard.go new file mode 100644 index 0000000..877c564 --- /dev/null +++ b/pkg/templates/wechatCard.go @@ -0,0 +1,24 @@ +package templates + +import ( + models2 "watchAlert/internal/models" + "watchAlert/pkg/tools" +) + +func wechatTemplate(alert models2.AlertCurEvent, noticeTmpl models2.NoticeTemplateExample) string { + Title := ParserTemplate("Title", alert, noticeTmpl.Template) + Footer := ParserTemplate("Footer", alert, noticeTmpl.Template) + + t := models2.WeChatMsgTemplate{ + MsgType: "markdown", + MarkDown: models2.WeChatMarkDown{ + Content: "**" + Title + "**" + + "\n" + "\n" + + ParserTemplate("Event", alert, noticeTmpl.Template) + + "\n" + + Footer, + }, + } + + return tools.JsonMarshal(t) +}