├── .gitignore ├── README.md └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | golang-zabbix-alter-to-dingding-32 2 | golang-zabbix-alter-to-dingding-64 3 | golang-zabbix-alter-to-dingding 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-zabbix-alter-to-dingding 2 | zabbix报警到钉钉 3 | 4 | 更多详情:http://www.qiansw.com/golang-zabbix-alter-to-dingding.html 5 | 6 | 7 | ## 变更记录 8 | 2016-08-01 21:22 增加了对xml源消息的支持,防止json格式的消息中有引号造成消息失败的问题。 9 | 2017-01-09 13:08 增加了对消息内 url 字段支持。 10 | 2017-09-19 13:55 完善了log,可以记录到文件;支持了 zabbix 3.4 中恢复消息状态 “RESOLVED”。 11 | 12 | ## 更新 13 | 14 | 现在可以使用 zabbix 手机客户端 zCate 来接收 zabbix 的告警消息了:https://www.qiansw.com/how-to-use-zcate-to-receive-zabbix-alarm-messages.html 15 | 16 | 下面是效果图: 17 | 18 | ![图片预览](https://cache.img.qiansw.com:1443/usr/uploads/2019/06/3437100109.jpg) 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //执行方式: 4 | //alert -to=接收人 -agent=应用id -color=消息头部颜色 -corpid=corpid -corpsecret=corpsecret 5 | //alert -to=@all -agent=29481187 -color=FFE61A1A -corpid=dingd123465865 -corpsecret=zC5Jbed9S 6 | //CorpID和CorpSecret可以在钉钉后台找到 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "encoding/xml" 11 | "flag" 12 | "fmt" 13 | "io/ioutil" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "runtime" 18 | "strings" 19 | "time" 20 | ) 21 | 22 | type MsgInfo struct { 23 | //消息属性和内容 24 | To, Agentid, Corpid, Corpsecret, Msg, Url, Style string 25 | } 26 | 27 | var msgInfo MsgInfo 28 | var logPath string 29 | 30 | type Alert struct { 31 | From string `json:"from" xml:"from"` 32 | Time string `json:"time" xml:"time"` 33 | Level string `json:"level" xml:"level"` 34 | Name string `json:"name" xml:"name"` 35 | Key string `json:"key" xml:"key"` 36 | Value string `json:"value" xml:"value"` 37 | Now string `json:"now" xml:"now"` 38 | ID string `json:"id" xml:"id"` 39 | IP string `json:"ip" xml:"ip"` 40 | Color string `json:"color" xml:"color"` 41 | Url string `json:"url" xml:"url"` 42 | Age string `json:"age" xml:"age"` 43 | Status string `json:"status" xml:"status"` 44 | RecoveryTime string `json:"recoveryTime" xml:"recoveryTime"` 45 | Acknowledgement string `json:"acknowledgement" xml:"acknowledgement"` 46 | Acknowledgementhistory string `json:"acknowledgementhistory" xml:"acknowledgementhistory"` 47 | } 48 | 49 | type DingMsg struct { 50 | Touser string `json:"touser"` 51 | Agentid string `json:"agentid"` 52 | Msgtype string `json:"msgtype"` 53 | Oa struct { 54 | MessageURL string `json:"message_url"` 55 | Head struct { 56 | Bgcolor string `json:"bgcolor"` 57 | } `json:"head"` 58 | Body struct { 59 | Title string `json:"title"` 60 | Form [5]struct { 61 | Key string `json:"key"` 62 | Value string `json:"value"` 63 | } `json:"form"` 64 | Rich struct { 65 | Num string `json:"num"` 66 | } `json:"rich"` 67 | Content string `json:"content"` 68 | Author string `json:"author"` 69 | } `json:"body"` 70 | } `json:"oa"` 71 | } 72 | 73 | func log(message interface{}) { 74 | pc, file, line, _ := runtime.Caller(1) 75 | 76 | f := runtime.FuncForPC(pc).Name() 77 | now := time.Now().Format("15:04:05.000") 78 | date := time.Now().Format("2006-01-02") 79 | 80 | str := fmt.Sprintf("%s %s:%d [%s]: %v", now, file, line, f, message) 81 | 82 | fmt.Println(str) 83 | if logPath != "" { 84 | fname := fmt.Sprintf("%s/zabbix_to_dingding_%s.log", logPath, date) 85 | logfile, err := os.OpenFile(fname, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 86 | defer logfile.Close() 87 | if err != nil { 88 | fmt.Printf("%s %s:%d [%s]: [日志创建错误] %v\r\n", now, file, line, f, err) 89 | } 90 | logfile.WriteString(str + "\r\n") 91 | } 92 | } 93 | 94 | func init() { 95 | 96 | flag.StringVar(&msgInfo.To, "to", "@all", "消息的接收人,可以在钉钉后台查看,可空。") 97 | flag.StringVar(&msgInfo.Agentid, "agentid", "", "AgentID,可以在钉钉后台查看,不可空。") 98 | flag.StringVar(&msgInfo.Corpid, "corpid", "", "CorpID,可以在钉钉后台查看,不可空。") 99 | flag.StringVar(&msgInfo.Corpsecret, "corpsecret", "", "CorpSecret,可以在钉钉后台查看,不可空。") 100 | flag.StringVar(&msgInfo.Msg, "msg", `{ "from": "千思网", "time": "2016.07.28 17:00:05", "level": "Warning", "name": "这是一个千思网(qiansw.com)提供的ZABBIX钉钉报警插件。", "key": "icmpping", "value": "30ms", "now": "56ms", "id": "1637", "ip": "8.8.8.8", "color":"FF4A934A", "age":"3m", "recoveryTime":"2016.07.28 17:03:05", "status":"OK" }`, "Json格式的文本消息内容,不可空。") 101 | flag.StringVar(&msgInfo.Url, "url", "http://www.qiansw.com/golang-zabbix-alter-to-dingding.html", "消息内容点击后跳转到的URL,可空。") 102 | flag.StringVar(&msgInfo.Style, "style", "json", "Msg的格式,可选json和xml,推荐使用xml(支持消息中含双引号),可空。") 103 | flag.StringVar(&logPath, "log", "", "指定存放 log 的目录,不指定则不记录 log。") 104 | flag.Parse() 105 | 106 | pc, file, line, _ := runtime.Caller(1) 107 | 108 | f := runtime.FuncForPC(pc).Name() 109 | now := time.Now().Format("15:04:05.000") 110 | date := time.Now().Format("2006-01-02") 111 | 112 | if logPath != "" { 113 | fname := fmt.Sprintf("%s/zabbix_to_dingding_%s.log", logPath, date) 114 | logfile, err := os.OpenFile(fname, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 115 | defer logfile.Close() 116 | if err != nil { 117 | fmt.Printf("%s %s:%d [%s]: [日志创建错误] %v\r\n", now, file, line, f, err) 118 | os.Exit(1) 119 | } 120 | logfile.WriteString("程序启动……" + "\r\n") 121 | } 122 | log("初始化完成。") 123 | } 124 | 125 | func makeMsg(msg string) string { 126 | // 根据json或xml文本创建消息体 127 | log("开始创建消息。") 128 | log(fmt.Sprintf(`来源消息: 129 | %v`, msg)) 130 | 131 | var alert Alert 132 | if msgInfo.Style == "xml" { 133 | log("来源消息格式为XML。") 134 | err := xml.Unmarshal([]byte(msg), &alert) 135 | if err != nil { 136 | log(fmt.Sprintf("XML 解析失败:%v", err)) 137 | os.Exit(1) 138 | } 139 | } else if msgInfo.Style == "json" { 140 | log("来源消息格式为Json。") 141 | err := json.Unmarshal([]byte(msg), &alert) 142 | if err != nil { 143 | log(fmt.Sprintf("Json 解析失败:%v", err)) 144 | os.Exit(1) 145 | } 146 | } else { 147 | log("未指定来源消息格式,默认使用Json解析。") 148 | err := json.Unmarshal([]byte(msg), &alert) 149 | if err != nil { 150 | log(fmt.Sprintf("Json 解析失败:%v", err)) 151 | os.Exit(1) 152 | } 153 | } 154 | log("来源消息解析成功。") 155 | var dingMsg DingMsg 156 | //给dingMsg各元素赋值 157 | dingMsg.Touser = msgInfo.To 158 | dingMsg.Agentid = msgInfo.Agentid 159 | dingMsg.Msgtype = "oa" 160 | dingMsg.Oa.MessageURL = msgInfo.Url 161 | if alert.Url != "" { 162 | dingMsg.Oa.MessageURL = alert.Url 163 | } 164 | dingMsg.Oa.Head.Bgcolor = alert.Color 165 | dingMsg.Oa.Body.Title = alert.Name 166 | dingMsg.Oa.Body.Form[0].Key = "告警级别:" 167 | dingMsg.Oa.Body.Form[1].Key = "故障时间:" 168 | dingMsg.Oa.Body.Form[2].Key = "故障时长:" 169 | dingMsg.Oa.Body.Form[3].Key = "IP地址:" 170 | dingMsg.Oa.Body.Form[4].Key = "检测项:" 171 | dingMsg.Oa.Body.Form[0].Value = alert.Level 172 | dingMsg.Oa.Body.Form[1].Value = alert.Time 173 | dingMsg.Oa.Body.Form[2].Value = alert.Age 174 | dingMsg.Oa.Body.Form[3].Value = alert.IP 175 | dingMsg.Oa.Body.Form[4].Value = alert.Key 176 | dingMsg.Oa.Body.Rich.Num = alert.Now 177 | if alert.Status == "PROBLEM" { 178 | // 故障处理 179 | dingMsg.Oa.Body.Author = fmt.Sprintf("[%s·%s(%s)]", alert.From, "故障", alert.ID) 180 | if strings.Replace(alert.Acknowledgement, " ", "", -1) == "Yes" { 181 | dingMsg.Oa.Body.Content = "故障已经被确认," + alert.Acknowledgementhistory 182 | } 183 | } else if alert.Status == "OK" { 184 | // 恢复处理 185 | dingMsg.Oa.Body.Form[0].Key = "故障时间:" 186 | dingMsg.Oa.Body.Form[1].Key = "恢复时间:" 187 | dingMsg.Oa.Body.Form[0].Value = alert.Time 188 | dingMsg.Oa.Body.Form[1].Value = alert.RecoveryTime 189 | dingMsg.Oa.Body.Author = fmt.Sprintf("[%s·%s(%s)]", alert.From, "恢复", alert.ID) 190 | 191 | } else if alert.Status == "RESOLVED" { 192 | // 恢复处理 193 | dingMsg.Oa.Body.Form[0].Key = "故障时间:" 194 | dingMsg.Oa.Body.Form[1].Key = "恢复时间:" 195 | dingMsg.Oa.Body.Form[0].Value = alert.Time 196 | dingMsg.Oa.Body.Form[1].Value = alert.RecoveryTime 197 | dingMsg.Oa.Body.Author = fmt.Sprintf("[%s·%s(%s)]", alert.From, "恢复", alert.ID) 198 | 199 | } else if alert.Status == "msg" { 200 | dingMsg.Oa.Body.Title = alert.Name 201 | dingMsg.Oa.Body.Form[0].Key = "" 202 | dingMsg.Oa.Body.Form[1].Key = "" 203 | dingMsg.Oa.Body.Form[2].Key = "" 204 | dingMsg.Oa.Body.Form[3].Key = "" 205 | dingMsg.Oa.Body.Form[4].Key = "" 206 | dingMsg.Oa.Body.Form[0].Value = "" 207 | dingMsg.Oa.Body.Form[1].Value = "" 208 | dingMsg.Oa.Body.Form[2].Value = "" 209 | dingMsg.Oa.Body.Form[3].Value = "" 210 | dingMsg.Oa.Body.Form[4].Value = "" 211 | dingMsg.Oa.Body.Content = alert.Acknowledgementhistory 212 | } else { 213 | // 其他status状况处理 214 | dingMsg.Oa.MessageURL = "https://www.qiansw.com/golang-zabbix-alter-to-dingding.html" 215 | dingMsg.Oa.Body.Content = "ZABBIX动作配置有误,请至千思网[qiansw.com]或直接[点击此消息]查看具体配置文档。" 216 | dingMsg.Oa.Body.Author = fmt.Sprintf("[%s·%s(%s)]", alert.From, alert.Status, alert.ID) 217 | if strings.Replace(alert.Acknowledgement, " ", "", -1) == "Yes" { 218 | dingMsg.Oa.Body.Content = "故障已经被确认," + alert.Acknowledgementhistory 219 | } 220 | } 221 | // 创建post给钉钉的Json文本 222 | JsonMsg, err := json.Marshal(dingMsg) 223 | if err != nil { 224 | log(err) 225 | os.Exit(1) 226 | } 227 | log(fmt.Sprintf("消息创建完成:%s\r\n", string(JsonMsg))) 228 | return string(JsonMsg) 229 | } 230 | 231 | func getToken(corpid, corpsecret string) (token string) { //根据id和secret获取AccessToken 232 | log("获取Token……") 233 | type ResToken struct { 234 | Access_token string 235 | Errcode int 236 | Errmsg string 237 | } 238 | 239 | urlstr := fmt.Sprintf("https://oapi.dingtalk.com/gettoken?corpid=%s&corpsecret=%s", corpid, corpsecret) 240 | u, _ := url.Parse(urlstr) 241 | q := u.Query() 242 | u.RawQuery = q.Encode() 243 | res, err := http.Get(u.String()) 244 | 245 | if err != nil { 246 | log(err) 247 | os.Exit(1) 248 | return 249 | } 250 | result, err := ioutil.ReadAll(res.Body) 251 | res.Body.Close() 252 | if err != nil { 253 | log(err) 254 | os.Exit(1) 255 | return 256 | } 257 | 258 | var m ResToken 259 | err1 := json.Unmarshal(result, &m) 260 | 261 | if err1 == nil { 262 | if m.Errcode == 0 { 263 | log("成功!") 264 | return m.Access_token 265 | } else { 266 | log(fmt.Sprintf("AccessToken获取失败,", m.Errmsg)) 267 | os.Exit(1) 268 | return 269 | } 270 | return 271 | } else { 272 | log("Token解析失败!") 273 | os.Exit(1) 274 | return 275 | } 276 | 277 | } 278 | 279 | func sendMsg(token, msg string) (status bool) { //发送OA消息,,返回成功或失败 280 | log(fmt.Sprintf("需要POST的内容:%v", msg)) 281 | body := bytes.NewBuffer([]byte(msg)) 282 | url := fmt.Sprintf("https://oapi.dingtalk.com/message/send?access_token=%s", token) 283 | // fmt.Println(url) 284 | res, err := http.Post(url, "application/json;charset=utf-8", body) 285 | if err != nil { 286 | log(err) 287 | os.Exit(1) 288 | return 289 | } 290 | result, err := ioutil.ReadAll(res.Body) 291 | res.Body.Close() 292 | if err != nil { 293 | log(err) 294 | os.Exit(1) 295 | return 296 | } 297 | log(fmt.Sprintf("钉钉接口返回消息:%s", result)) 298 | return 299 | } 300 | 301 | func main() { 302 | sendMsg(getToken(msgInfo.Corpid, msgInfo.Corpsecret), makeMsg(msgInfo.Msg)) 303 | } 304 | --------------------------------------------------------------------------------