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 @@
## 💎 项目介绍
-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)
-|  |  |
+|  |  |
|:------------------------------:|--------------------------------|
|  |  |
|  |  |
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)
+}