diff --git a/cfg.example.json b/cfg.example.json index 4a8b679..b656c31 100644 --- a/cfg.example.json +++ b/cfg.example.json @@ -1,13 +1,15 @@ { - "addr": "localhost:23329", - "dingTalk": { - "enable": true - }, - "wexin": { - "enable": false, - "corpID": "", - "agentId": "", - "secret": "", - "encodingAESKey": "" - } + "addr": "localhost:23329", + "dingTalk": { + "enable": true, + "templateFile": "message-template.md", + "messageType": "markdown" + }, + "wexin": { + "enable": false, + "corpID": "", + "agentId": "", + "secret": "", + "encodingAESKey": "" + } } \ No newline at end of file diff --git a/config/config.go b/config/config.go index efa7601..31c221a 100644 --- a/config/config.go +++ b/config/config.go @@ -1,46 +1,58 @@ package config import ( - "encoding/json" - "io/ioutil" - "log" + "encoding/json" + "io/ioutil" + "log" ) // Config 配置 type Config struct { - Addr string `json:"addr"` - DingTalk DingTalk `json:"dingTalk"` + Addr string `json:"addr"` + DingTalk DingTalk `json:"dingTalk"` - Weixin Weixin `json:"weixin"` + Weixin Weixin `json:"weixin"` } // Weixin 微信配置 type Weixin struct { - Enable bool - CorpID string `json:"corpID"` - AgentID string `json:"agentId"` - Secret string `json:"secret"` - EncodingAESKey string `json:"encodingAESKey"` + Enable bool + CorpID string `json:"corpID"` + AgentID string `json:"agentId"` + Secret string `json:"secret"` + EncodingAESKey string `json:"encodingAESKey"` +} + +type AlarmMessage struct { + Level string // 告警等级 P1 + Type string // 类型 PROBLEM,OK + Endpoint string // 主机host或者ip + Desc string // 告警描述 + Condition string // 告警条件 + Count string // 当前告警次数 + Time string // 告警时间 } // DingTalk 钉钉配置 type DingTalk struct { - Enable bool `json:"enable"` - // Level 等级, 只发送level 及其以下的消息 + Enable bool `json:"enable"` + // Level 等级, 只发送level 及其以下 的消息 - Level uint `json:"level"` + Level uint `json:"level"` + TemplateFile string + MessageType string // markdown ,text } // Read 读取配置 func Read() Config { - bytes, err := ioutil.ReadFile("cfg.json") - if err != nil { - log.Fatalln("need file: cfg.json") - } - var cfg Config - if err = json.Unmarshal(bytes, &cfg); err != nil { - log.Fatalln("config file error", err.Error()) - } - - return cfg + bytes, err := ioutil.ReadFile("cfg.json") + if err != nil { + log.Fatalln("need file: cfg.json") + } + var cfg Config + if err = json.Unmarshal(bytes, &cfg); err != nil { + log.Fatalln("config file error", err.Error()) + } + + return cfg } diff --git a/main.go b/main.go index 5e50e73..4c73e51 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,20 @@ package main import ( - "errors" - "log" - "net/http" - "strings" - "time" - - "github.com/labstack/echo" - "github.com/labstack/echo/middleware" - "github.com/sdvdxl/falcon-message/config" - "github.com/sdvdxl/falcon-message/sender" - "github.com/sdvdxl/falcon-message/util" - "github.com/tylerb/graceful" + "bytes" + "errors" + "html/template" + "log" + "net/http" + "strings" + "time" + + "github.com/labstack/echo" + "github.com/labstack/echo/middleware" + "github.com/sdvdxl/falcon-message/config" + "github.com/sdvdxl/falcon-message/sender" + "github.com/sdvdxl/falcon-message/util" + "github.com/tylerb/graceful" ) // OK @@ -27,87 +29,106 @@ import ( // http://127.0.0.1:8081/portal/template/view/37 const ( - // IMDingPrefix 钉钉 前缀 - IMDingPrefix = "[ding]:" - // IMWexinPrefix 微信前缀 - // IMWexinPrefix = "[wexin]:" + // IMDingPrefix 钉钉 前缀 + IMDingPrefix = "[ding]:" + // IMWexinPrefix 微信前缀 + // IMWexinPrefix = "[wexin]:" ) var ( - cfg config.Config - ding *sender.DingTalk - wx *sender.Weixin + cfg config.Config + ding *sender.DingTalk + wx *sender.Weixin ) func main() { - cfg = config.Read() - - if cfg.DingTalk.Enable { - ding = sender.NewDingTalk() - } - - if cfg.Weixin.Enable { - wx = sender.NewWeixin(cfg.Weixin.CorpID, cfg.Weixin.Secret) - go wx.GetAccessToken() - } - - engine := echo.New() - engine.Server.Addr = cfg.Addr - server := &graceful.Server{Timeout: time.Second * 10, Server: engine.Server, Logger: graceful.DefaultLogger()} - engine.Use(middleware.Recover()) - // engine.Use(middleware.Logger()) - api := engine.Group("/api/v1") - api.GET("/wechat/auth", wxAuth) - api.POST("/message", func(c echo.Context) error { - log.Println("message comming") - tos := c.FormValue("tos") - content := c.FormValue("content") - log.Println("tos:", tos, " content:", content) - if content == "" { - return echo.NewHTTPError(http.StatusBadRequest, "content is requied") - } - - content = util.HandleContent(content) - if strings.HasPrefix(tos, IMDingPrefix) { //是钉钉 - tokens := tos[len(IMDingPrefix):] - - if cfg.DingTalk.Enable { - for _, v := range strings.Split(tokens, ";") { - go ding.Send(v, content) - } - } - } else { //微信 - if cfg.Weixin.Enable { - if err := wx.Send(tos, content); err != nil { - return echo.NewHTTPError(500, err.Error()) - } - } - } - - return nil - }) - - log.Println("listening on ", cfg.Addr) - if err := server.ListenAndServe(); err != nil { - log.Fatal(err) - } + cfg = config.Read() + + if cfg.DingTalk.Enable { + ding = sender.NewDingTalk() + } + + if cfg.Weixin.Enable { + wx = sender.NewWeixin(cfg.Weixin.CorpID, cfg.Weixin.Secret) + go wx.GetAccessToken() + } + + engine := echo.New() + engine.Server.Addr = cfg.Addr + server := &graceful.Server{Timeout: time.Second * 10, Server: engine.Server, Logger: graceful.DefaultLogger()} + engine.Use(middleware.Recover()) + // engine.Use(middleware.Logger()) + api := engine.Group("/api/v1") + api.GET("/wechat/auth", wxAuth) + api.POST("/message", func(c echo.Context) error { + log.Println("message comming") + tos := c.FormValue("tos") + content := c.FormValue("content") + log.Println("tos:", tos, " content:", content) + if content == "" { + return echo.NewHTTPError(http.StatusBadRequest, "content is requied") + } + + msg, err := util.HandleContent(content) + if err != nil { + return err + } + + t, err := template.New("alarm").ParseFiles(cfg.DingTalk.TemplateFile) + if err != nil { + return err + } + + var buffer bytes.Buffer + if err := t.Execute(&buffer, msg); err != nil { + return err + } + content = buffer.String() + + if strings.HasPrefix(tos, IMDingPrefix) { //是钉钉 + tokens := tos[len(IMDingPrefix):] + + if cfg.DingTalk.Enable { + for _, v := range strings.Split(tokens, ";") { + go func() { + if err := ding.Send(v, content, cfg.DingTalk.MessageType); err != nil { + log.Println("ERR:", err) + } + }() + } + } + } else { //微信 + if cfg.Weixin.Enable { + if err := wx.Send(tos, content); err != nil { + return echo.NewHTTPError(500, err.Error()) + } + } + } + + return nil + }) + + log.Println("listening on ", cfg.Addr) + if err := server.ListenAndServe(); err != nil { + log.Fatal(err) + } } // WxAuth 开启回调模式验证 func wxAuth(context echo.Context) error { - if cfg.Weixin.Enable { - echostr := context.FormValue("echostr") - if echostr == "" { - return errors.New("无法获取请求参数, echostr 为空") - } - var buf []byte - var err error - if buf, err = wx.Auth(echostr); err != nil { - return err - } - - return context.JSONBlob(200, buf) - } - - return context.String(200, "微信没有启用") + if cfg.Weixin.Enable { + echostr := context.FormValue("echostr") + if echostr == "" { + return errors.New("无法获取请求参数, echostr 为空") + } + var buf []byte + var err error + if buf, err = wx.Auth(echostr); err != nil { + return err + } + + return context.JSONBlob(200, buf) + } + + return context.String(200, "微信没有启用") } diff --git a/message-template.md b/message-template.md new file mode 100644 index 0000000..42aa03a --- /dev/null +++ b/message-template.md @@ -0,0 +1,8 @@ +## 告警 + +- 告警等级: {{.Level}} +- 告警类型: {{.Type}} +- 告警指标: {{.Condition}} +- 告警主机: {{.Host}} +- 告警时间: {{.Time}} +- 告警说明: {{.Desc}},已持续{{.Count}}分钟 diff --git a/sender/dingtalk.go b/sender/dingtalk.go index 9935261..5775ed2 100644 --- a/sender/dingtalk.go +++ b/sender/dingtalk.go @@ -1,34 +1,38 @@ package sender import ( - "errors" - "log" - "net/http" - - "github.com/labstack/echo" - "github.com/sdvdxl/dinghook" + "errors" + "github.com/labstack/echo" + "github.com/sdvdxl/dinghook" + "log" + "net/http" ) type DingTalk struct { } -func (d *DingTalk) Send(token string, content string) error { - if token == "" { - return errors.New("need dingding token") - } +func (d *DingTalk) Send(token string, content, msgType string) error { + if token == "" { + return errors.New("need dingding token") + } - // 发送钉钉 - ding := dinghook.NewDing(token) - result := ding.SendMessage(dinghook.Message{Content: content}) - log.Println(result) - if !result.Success { - log.Println("token:", token) - return echo.NewHTTPError(http.StatusBadRequest, result.ErrMsg) - } + // 发送钉钉 + ding := dinghook.NewDing(token) + var result dinghook.Result + if msgType == dinghook.MsgTypeMarkdown { + result = ding.SendMarkdown(dinghook.Markdown{Title: "告警", Content: content}) + } else { + result = ding.SendMessage(dinghook.Message{Content: content}) + } + log.Println(result) + if !result.Success { + log.Println("token:", token, " send result:", result) + return echo.NewHTTPError(http.StatusBadRequest, result.ErrMsg) + } - return nil + return nil } func NewDingTalk() *DingTalk { - return &DingTalk{} + return &DingTalk{} } diff --git a/util/util.go b/util/util.go index 2de72dc..ad74b38 100644 --- a/util/util.go +++ b/util/util.go @@ -1,44 +1,58 @@ package util import ( - "bytes" - "encoding/json" - "log" - "strconv" - "strings" + "bytes" + "encoding/json" + "errors" + "github.com/sdvdxl/falcon-message/config" + "log" + "strconv" + "strings" ) // EncodeJSON json序列化(禁止 html 符号转义) func EncodeJSON(v interface{}) ([]byte, error) { - var buf bytes.Buffer - encoder := json.NewEncoder(&buf) - encoder.SetEscapeHTML(false) - if err := encoder.Encode(v); err != nil { - return nil, err - } - return buf.Bytes(), nil + var buf bytes.Buffer + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(v); err != nil { + return nil, err + } + return buf.Bytes(), nil } //StringToInt string 类型转 int func StringToInt(s string) int { - n, err := strconv.Atoi(s) - if err != nil { - log.Printf("agent 类型转换失败, 请检查配置文件中 agentid 配置是否为纯数字(%v)", err) - return 0 - } - return n + n, err := strconv.Atoi(s) + if err != nil { + log.Printf("agent 类型转换失败, 请检查配置文件中 agentid 配置是否为纯数字(%v)", err) + return 0 + } + return n } -// HandleContent [P2][PROBLEM][10-13-33-153][][测试 all(#1) net.port.listen port=2 0==0][O3 2017-06-06 16:46:00] -func HandleContent(content string) string { - content = strings.Replace(content, "][", "\n", -1) - if content[0] == '[' { - content = content[1:] - } +// HandleContent [P2][PROBLEM][10-13-33-153][][测试 all(#1) net.port.listen port=2][O3 2017-06-06 16:46:00] +func HandleContent(content string) (*config.AlarmMessage, error) { + args := strings.Split(content, "][") + if len(args) < 6 { + return nil, errors.New("告警消息格式不匹配,可能是版本不一致导致") + } - if content[len(content)-1] == ']' { - content = content[:len(content)-1] - } + args[0] = string([]rune(args[0])[1:]) + arg := args[5] + args[5] = string([]rune(arg)[:len(arg)-1]) - return content + // 描述和条件 + argStr := args[4] + subArgs := strings.Split(argStr, " ") + condition := strings.Join(subArgs[len(subArgs)-3:], " ") + desc := strings.Join(subArgs[:len(subArgs)-3], " ") + + // 次数和时间 + argStr = args[5] + subArgs = strings.Split(argStr, " ") + count := subArgs[0][1:] + time := strings.Join(subArgs[1:], " ") + return &config.AlarmMessage{Level: args[0], Type: args[1], Endpoint: args[2], + Desc: desc, Condition: condition, Count: count, Time: time}, nil } diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 0000000..adc65ce --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,17 @@ +package util + +import ( + "encoding/json" + "fmt" + "testing" +) + +func TestHandleContent(t *testing.T) { + c, err := HandleContent(`[P2][PROBLEM][10-13-33-153][][测试 all(#1) net.port.listen port=2][O3 2017-06-06 16:46:00]`) + if err != nil { + t.Error(err) + } + + j, _ := json.Marshal(c) + fmt.Println(string(j)) +}