├── README.md ├── alertmanager-webhook.yaml ├── conf └── config.go ├── example ├── alertmanager ├── default.tmpl ├── dingding.tmpl ├── feishu.tmpl ├── image │ ├── dingding.jpg │ ├── feishu.jpg │ └── qywechat.jpg ├── qywechat.tmpl └── systemd │ └── alertmanager-webhook.service ├── go.mod ├── go.sum ├── main.go ├── model ├── dingding.go ├── feishu.go ├── message.go ├── prometheus.go └── qywechat.go ├── sender ├── dingdingnotify.go ├── feishunotify.go └── qywechatnotify.go ├── template └── alert.tmpl └── transformer └── transformer.go /README.md: -------------------------------------------------------------------------------- 1 | # alertmanager-webhook 2 | 3 | ## 项目说明 4 | ``` 5 | 一个用于alertmanager转发告警信息到指定平台的webhook,适用于使用alertmanager作为告警工具的运维或者开发人员,也适用于自行开发告警平台的开发人员,告警信息转换成alertmanager的json告警格式即可。 6 | ``` 7 | 8 | ## 功能特性 9 | ``` 10 | 1. 支持自定义告警模板 11 | 2. 支持对接企业微信机器人 12 | 3. 支持对接飞书机器人 13 | 4. 支持对接钉钉机器人 14 | ``` 15 | 16 | 17 | ## 使用用法 18 | **项目依赖redis,需要先启动redis** 19 | 20 | **1. 二进制启动** 21 | 22 | 下载二进制文件,修改 alertmanager-webhook.yaml 23 | ```bash 24 | ./alertmanager-webhook -c alertmanager-webhook.yaml 25 | ``` 26 | **2. docker启动** 27 | ```bash 28 | docker run -d --name redis -v /root/redis.conf:/etc/redis/redis.conf -p 0.0.0.0:6379:6379 redis:5.0.0 redis-server /etc/redis/redis.conf 29 | docker run -d --name alertmanager-webhook -p 0.0.0.0:9095:9095 -v /root/alertmanager-webhook.yaml:/etc/alertmanager-webhook/alertmanager-webhook.yaml rainbowhhy/alertmanager-webhook:v1.0 30 | ``` 31 | **3. 模板修改** 32 | ```text 33 | 如果要修改告警模板,则需要编辑template/alert.tmpl,可以参照example中的模板来修改,docker启动则使用-v挂载,比如-v /root/dingding.tmpl:/data/alertmanager-webhook/template/alert.tmpl 34 | ``` 35 | 36 | ## 告警配置 37 | 38 | alertmanager中配置webhook 39 | 40 | ```yaml 41 | # alertmanager.yml 42 | ...... 43 | 44 | receivers: 45 | name: webhook 46 | webhook_configs: 47 | # 配置企业微信接口 48 | - url: 'http://127.0.0.1:9095/qywechat' 49 | # 配置飞书接口 50 | - url: 'http://127.0.0.1:9095/feishu' 51 | # 配置钉钉接口 52 | - url: 'http://127.0.0.1:9095/dingding' 53 | 54 | ...... 55 | ``` 56 | 57 | ## 使用示例 58 | **使用curl模仿alertmanager往webhook发送消息** 59 | 60 | ```bash 61 | curl -X POST -H "Content-Type: application/json" -d ' 62 | {"alerts": [ 63 | { 64 | "status": "firing|resolved", 65 | "labels": { 66 | "alertname": "机器宕机监测", 67 | "instance": "10.180.48.2", 68 | "job": "node_exporter", 69 | "serverity": "warning" 70 | }, 71 | "annotations": { 72 | "description": "机器:10.180.48.2 所属 job:node_exporter 宕机超过1分钟,请检查!", 73 | "summary": "机器发生宕机" 74 | }, 75 | "startsAt": "2024-01-10T11:59:09.775Z", 76 | "endsAt": "2024-01-10T13:00:00Z", 77 | "fingerprint": "02f13394997e5211" 78 | } 79 | ] 80 | }' 127.0.0.1:9095/ 81 | 82 | # 注意firing|resolved选择一个,触发告警选择firing,告警恢复选择resolved; 从中选择一个,比如企业微信就是:127.0.0.1:9095/qywechat 83 | ``` 84 | 85 | ## 实现效果 86 | **企业微信告警效果** 87 | 88 | ![img](./example/image/qywechat.jpg) 89 | 90 | **飞书告警效果** 91 | 92 | ![img](./example/image/feishu.jpg) 93 | 94 | **钉钉告警效果** 95 | 96 | ![img](./example/image/dingding.jpg) 97 | -------------------------------------------------------------------------------- /alertmanager-webhook.yaml: -------------------------------------------------------------------------------- 1 | # 企业微信机器人key 2 | # 使用企业微信时必须配置,不使用则留空 3 | qywechatKey: 4 | 5 | # 飞书机器人key 6 | # 使用飞书时必须配置,不使用则留空 7 | feishuKey: 8 | 9 | # 钉钉机器人key 10 | # 使用钉钉时必须配置,不使用则留 11 | dingdingKey: 12 | 13 | # Redis配置 14 | redisServer: 127.0.0.1 # 必须配置 15 | redisPort: # 可选项,为空默认为6379 16 | redisPassword: wecloudsre@2023 # redis未设置密码则留空,如果设置了密码登陆则必须配置 17 | 18 | # 日志配置 19 | logFileDir: # 可选项,为空则为程序运行目录 20 | logFilePath: alertmanager-webhook.log # 必须配置 21 | 22 | # 服务监听配置 23 | port: 9095 # 可选项,为空则默认为9095 24 | host: 0.0.0.0 # 可选项,为空默认监听 127.0.0.1 25 | -------------------------------------------------------------------------------- /conf/config.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v2" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | ) 10 | 11 | type Config struct { 12 | QyWechatKey string `yaml:"qywechatKey"` 13 | FeishuKey string `yaml:"feishuKey"` 14 | DingdingKey string `yaml:"dingdingKey"` 15 | LogFileDir string `yaml:"logFileDir"` 16 | LogFilePath string `yaml:"logFilePath"` 17 | Port string `yaml:"port"` 18 | Host string `yaml:"host"` 19 | RedisServer string `yaml:"redisServer"` 20 | RedisPort string `yaml:"redisPort"` 21 | RedisPassword string `yaml:"redisPassword"` 22 | } 23 | 24 | func GetConf(workdir, configFile string) Config { 25 | var webhookConf Config 26 | yamlFile, err := ioutil.ReadFile(configFile) 27 | if err != nil { 28 | fmt.Println("The program configuration file does not exist, please configure it correctly!") 29 | os.Exit(100) 30 | } 31 | 32 | // 读取配置文件,从yaml解析为struct 33 | err = yaml.Unmarshal(yamlFile, &webhookConf) 34 | if err != nil { 35 | fmt.Println(err.Error()) 36 | } 37 | if webhookConf.LogFileDir != "" { 38 | webhookConf.LogFilePath = path.Join(webhookConf.LogFileDir, webhookConf.LogFilePath) 39 | } else { 40 | webhookConf.LogFilePath = path.Join(workdir, webhookConf.LogFilePath) 41 | } 42 | if webhookConf.Port == "" { 43 | webhookConf.Port = "9095" 44 | } 45 | if webhookConf.Host == "" { 46 | webhookConf.Host = "127.0.0.1" 47 | } 48 | if webhookConf.RedisPort == "" { 49 | webhookConf.RedisPort = "6379" 50 | } 51 | 52 | return webhookConf 53 | } 54 | -------------------------------------------------------------------------------- /example/alertmanager: -------------------------------------------------------------------------------- 1 | "alerts": [ 2 | { 3 | "status": "firing", 4 | "labels": { 5 | "alertname": "机器宕机监测", 6 | "instance": "10.180.48.2", 7 | "job": "node_exporter", 8 | "site": "beijing", 9 | "usage": "monitor" 10 | }, 11 | "annotations": { 12 | "description": "机器:10.180.48.2 所属 job:node_exporter 宕机超过1分钟,请检查!", 13 | "summary": "机器发生宕机" 14 | }, 15 | "startsAt": "2023-10-11T11:59:09.775Z", 16 | "endsAt": "0001-01-01T00:00:00Z", 17 | "fingerprint": "02f13394997e5211" 18 | } 19 | ] -------------------------------------------------------------------------------- /example/default.tmpl: -------------------------------------------------------------------------------- 1 | {{- if eq .Status `firing` -}} 2 | {{- /* 自定义触发告警时的内容格式 */ -}} 3 | 告警主题: {{.Annotations.summary}} 4 | 告警级别: {{.Labels.serverity}} 5 | 告警次数: {{.Count}} 6 | 告警主机: {{.Labels.instance}} 7 | 告警详情: {{.Annotations.description}} 8 | 触发时间: {{.StartTime}} 9 | 10 | {{- else if eq .Status `resolved` -}} 11 | {{- /* 自定义告警恢复时的内容格式 */ -}} 12 | 告警主题: {{.Annotations.summary}} 13 | 告警主机: {{.Labels.instance}} 14 | 开始时间: {{.StartTime}} 15 | 恢复时间: {{.EndTime}} 16 | {{- end -}} -------------------------------------------------------------------------------- /example/dingding.tmpl: -------------------------------------------------------------------------------- 1 | {{- if eq .Status `firing` -}} 2 | {{- /* 自定义触发告警时的内容格式 */ -}} 3 | >**告警主题: {{.Annotations.summary}}** 4 | >告警级别: {{.Labels.serverity}} 5 | >告警次数: {{.Count}} 6 | >告警主机: {{.Labels.instance}} 7 | >告警详情: {{.Annotations.description}} 8 | >触发时间: {{.StartTime}} 9 | 10 | {{- else if eq .Status `resolved` -}} 11 | {{- /* 自定义告警恢复时的内容格式 */ -}} 12 | >**告警主题: {{.Annotations.summary}}** 13 | >告警主机: {{.Labels.instance}} 14 | >开始时间: {{.StartTime}} 15 | >恢复时间: {{.EndTime}} 16 | {{- end -}} 17 | -------------------------------------------------------------------------------- /example/feishu.tmpl: -------------------------------------------------------------------------------- 1 | {{- if eq .Status `firing` -}} 2 | {{- /* 自定义触发告警时的内容格式 */ -}} 3 | **告警主题: {{.Annotations.summary}}** 4 | 告警级别: {{.Labels.serverity}} 5 | 告警次数: {{.Count}} 6 | 告警主机: {{.Labels.instance}} 7 | 告警详情: {{.Annotations.description}} 8 | 触发时间: {{.StartTime}} 9 | 10 | {{- else if eq .Status `resolved` -}} 11 | {{- /* 自定义告警恢复时的内容格式 */ -}} 12 | **告警主题: {{.Annotations.summary}}** 13 | 告警主机: {{.Labels.instance}} 14 | 开始时间: {{.StartTime}} 15 | 恢复时间: {{.EndTime}} 16 | {{- end -}} -------------------------------------------------------------------------------- /example/image/dingding.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rainbowhhy/alertmanager-webhook/e19f5752cca7c2d064816973c3d7831f9f1fea2f/example/image/dingding.jpg -------------------------------------------------------------------------------- /example/image/feishu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rainbowhhy/alertmanager-webhook/e19f5752cca7c2d064816973c3d7831f9f1fea2f/example/image/feishu.jpg -------------------------------------------------------------------------------- /example/image/qywechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rainbowhhy/alertmanager-webhook/e19f5752cca7c2d064816973c3d7831f9f1fea2f/example/image/qywechat.jpg -------------------------------------------------------------------------------- /example/qywechat.tmpl: -------------------------------------------------------------------------------- 1 | {{- if eq .Status `firing` -}} 2 | {{- /* 自定义触发告警时的内容格式 */ -}} 3 | >**告警主题: {{.Annotations.summary}}** 4 | >告警级别: {{.Labels.serverity}} 5 | >告警次数: {{.Count}} 6 | >告警主机: {{.Labels.instance}} 7 | >告警详情: {{.Annotations.description}} 8 | >触发时间: {{.StartTime}} 9 | 10 | {{- else if eq .Status `resolved` -}} 11 | {{- /* 自定义告警恢复时的内容格式 */ -}} 12 | >**告警主题: {{.Annotations.summary}}** 13 | >告警主机: {{.Labels.instance}} 14 | >开始时间: {{.StartTime}} 15 | >恢复时间: {{.EndTime}} 16 | {{- end -}} -------------------------------------------------------------------------------- /example/systemd/alertmanager-webhook.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Alertmanager Webhook service 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | 8 | ExecReload=/bin/kill -HUP $MAINPID 9 | ExecStart=/usr/local/bin/alertmanager-webhook 10 | SuccessExitStatus=143 11 | TimeoutStopSec=1 12 | Restart=always 13 | 14 | [Install] 15 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module golangcode/alertmanager-webhook 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.8.1 7 | github.com/gomodule/redigo/redis v0.0.1 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 5 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 6 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 7 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 8 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 9 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 10 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 11 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 12 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 13 | github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= 14 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 15 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 16 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 17 | github.com/gomodule/redigo/redis v0.0.1 h1:tQQSZyg4O0N0Dh2hli1pOrRdj+WHl1xf3w/x7olDgu0= 18 | github.com/gomodule/redigo/redis v0.0.1/go.mod h1:QhGMo2EGfdSsmrYDENZq12/Y23fRP6X6nyFZ4TGSUvM= 19 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 20 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 21 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 22 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 23 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 24 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 25 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 26 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 27 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 30 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 31 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 32 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 33 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 34 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 35 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 36 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 37 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 38 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 41 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 42 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 43 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 44 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 45 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 46 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 47 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 48 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 50 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 51 | github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= 52 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 53 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 54 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 55 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= 56 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 57 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 58 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 59 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 60 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 61 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 65 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 67 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 69 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 70 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 71 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 72 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 73 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 75 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 76 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 80 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 81 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 82 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 83 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 86 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "golangcode/alertmanager-webhook/conf" 8 | "golangcode/alertmanager-webhook/model" 9 | "golangcode/alertmanager-webhook/sender" 10 | "io" 11 | "log" 12 | "os" 13 | "time" 14 | ) 15 | 16 | var h bool 17 | var workdir string 18 | var configFile string 19 | var qywechatKey string 20 | var feishuKey string 21 | var dingdingKey string 22 | var logFilePath string 23 | var port string 24 | var host string 25 | var server string 26 | var redisServer string 27 | var redisPort string 28 | var redisPassword string 29 | 30 | func initConf() { 31 | if dir, err := os.Getwd(); err == nil { 32 | workdir = dir 33 | } 34 | var webhookConf conf.Config 35 | webhookConf = conf.GetConf(workdir, configFile) 36 | qywechatKey = webhookConf.QyWechatKey 37 | feishuKey = webhookConf.FeishuKey 38 | dingdingKey = webhookConf.DingdingKey 39 | logFilePath = webhookConf.LogFilePath 40 | port = webhookConf.Port 41 | host = webhookConf.Host 42 | server = fmt.Sprintf("%s:%s", host, port) 43 | redisServer = webhookConf.RedisServer 44 | redisPort = webhookConf.RedisPort 45 | redisPassword = webhookConf.RedisPassword 46 | } 47 | 48 | func main() { 49 | flag.BoolVar(&h, "h", false, "display this help and exit") 50 | flag.StringVar(&configFile, "c", "", "please input webhook configuration file path!") 51 | flag.Parse() 52 | if h { 53 | flag.Usage() 54 | return 55 | } 56 | initConf() 57 | gin.DisableConsoleColor() 58 | var f *os.File 59 | _, err := os.Lstat(logFilePath) 60 | if err != nil { 61 | f, _ = os.Create(logFilePath) 62 | } else { 63 | f, _ = os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 64 | } 65 | gin.DefaultWriter = io.MultiWriter(f) 66 | router := gin.New() 67 | 68 | router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 69 | // the client access log format 70 | return fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %s \"%s\" \"%s\"\n", 71 | param.ClientIP, 72 | param.TimeStamp.Format(time.RFC1123), 73 | param.Method, 74 | param.Path, 75 | param.Request.Proto, 76 | param.StatusCode, 77 | param.Latency, 78 | param.Request.UserAgent(), 79 | param.ErrorMessage, 80 | ) 81 | })) 82 | router.Use(gin.Recovery()) 83 | 84 | router.POST("/qywechat", func(c *gin.Context) { 85 | var notification model.Notification 86 | err := c.BindJSON(¬ification) 87 | if err != nil { 88 | log.Println("request error:", err.Error()) 89 | return 90 | } 91 | sender.SendToQywechat(notification, qywechatKey, redisServer, redisPort, redisPassword) 92 | 93 | }) 94 | 95 | router.POST("/feishu", func(c *gin.Context) { 96 | var notification model.Notification 97 | err := c.BindJSON(¬ification) 98 | if err != nil { 99 | log.Println("request error:", err.Error()) 100 | return 101 | } 102 | sender.SendToFeishu(notification, feishuKey, redisServer, redisPort, redisPassword) 103 | 104 | }) 105 | 106 | router.POST("/dingding", func(c *gin.Context) { 107 | var notification model.Notification 108 | err := c.BindJSON(¬ification) 109 | if err != nil { 110 | log.Println("request error:", err.Error()) 111 | return 112 | } 113 | sender.SendToDingding(notification, dingdingKey, redisServer, redisPort, redisPassword) 114 | 115 | }) 116 | 117 | log.Println("the Process is Running") 118 | router.Run(server) 119 | } 120 | -------------------------------------------------------------------------------- /model/dingding.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type DingDingMarkdown struct { 4 | MsgType string `json:"msgtype"` 5 | Dmarkdown Dmarkdown `json:"markdown"` 6 | } 7 | 8 | type Dmarkdown struct { 9 | Title string `json:"title"` 10 | Text string `json:"text"` 11 | } 12 | 13 | func NewDingDingMarkdown(title, text string) *DingDingMarkdown { 14 | return &DingDingMarkdown{ 15 | MsgType: "markdown", 16 | Dmarkdown: Dmarkdown{ 17 | Title: title, 18 | Text: text, 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /model/feishu.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type CardMsg struct { 4 | MsgType string `json:"msg_type"` 5 | Card Card `json:"card"` 6 | } 7 | 8 | type Card struct { 9 | Elements []Element `json:"elements"` 10 | Header Header `json:"header"` 11 | } 12 | 13 | type Element struct { 14 | Tag string `json:"tag"` 15 | Text Body `json:"text"` 16 | } 17 | 18 | type Header struct { 19 | Template string `json:"template"` 20 | Title Body `json:"title"` 21 | } 22 | 23 | type Body struct { 24 | Content string `json:"content"` 25 | Tag string `json:"tag"` 26 | } 27 | 28 | func NewCardMsg(color, title string) *CardMsg { 29 | return &CardMsg{ 30 | MsgType: "interactive", 31 | Card: Card{ 32 | Header: Header{ 33 | Template: color, 34 | Title: Body{ 35 | Tag: "plain_text", 36 | Content: title, 37 | }, 38 | }, 39 | }, 40 | } 41 | } 42 | 43 | func (c *CardMsg) AddElement(content string) { 44 | element := Element{ 45 | Tag: "div", 46 | Text: Body{ 47 | Content: content, 48 | Tag: "lark_md", 49 | }, 50 | } 51 | c.Card.Elements = append(c.Card.Elements, element) 52 | } 53 | -------------------------------------------------------------------------------- /model/message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Message struct { 4 | QywechatMessage QyWechatMessage 5 | FeishuMessage FeiShuMessage 6 | DingdingMessage DingDingMessage 7 | } 8 | 9 | type QyWechatMessage struct { 10 | MarkdownFiring *QyWeChatMarkdown 11 | MarkdownResolved *QyWeChatMarkdown 12 | } 13 | 14 | type FeiShuMessage struct { 15 | CardFiring *CardMsg 16 | CardResolved *CardMsg 17 | } 18 | 19 | type DingDingMessage struct { 20 | DmarkdownFiring *DingDingMarkdown 21 | DmarkdownResolved *DingDingMarkdown 22 | } 23 | 24 | func NewMessage(markdownFiring, markdownResolved *QyWeChatMarkdown, cardFiring, cardResolved *CardMsg, dmarkdownFiring, dmarkdownResolved *DingDingMarkdown) *Message { 25 | return &Message{ 26 | QywechatMessage: QyWechatMessage{ 27 | MarkdownFiring: markdownFiring, 28 | MarkdownResolved: markdownResolved, 29 | }, 30 | FeishuMessage: FeiShuMessage{ 31 | CardFiring: cardFiring, 32 | CardResolved: cardResolved, 33 | }, 34 | DingdingMessage: DingDingMessage{ 35 | DmarkdownFiring: dmarkdownFiring, 36 | DmarkdownResolved: dmarkdownResolved, 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /model/prometheus.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type Alert struct { 6 | Status string `json:"status"` 7 | Labels map[string]string `json:"labels"` 8 | Annotations map[string]string `json:annotations` 9 | StartsAt time.Time `json:"startsAt"` 10 | EndsAt time.Time `json:"endsAt"` 11 | StartTime string `json:"startTime"` 12 | EndTime string `json:"endTime"` 13 | Fingerprint string `json:"fingerprint"` 14 | Count int `json:count` 15 | } 16 | 17 | type Notification struct { 18 | Version string `json:"version"` 19 | GroupKey string `json:"groupKey"` 20 | Status string `json:"status"` 21 | Receiver string `json:receiver` 22 | GroupLabels map[string]string `json:groupLabels` 23 | CommonLabels map[string]string `json:commonLabels` 24 | ExternalURL string `json:externalURL` 25 | Alerts []Alert `json:alerts` 26 | } 27 | -------------------------------------------------------------------------------- /model/qywechat.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type QyWeChatMarkdown struct { 4 | MsgType string `json:"msgtype"` 5 | Markdown Markdown `json:"markdown"` 6 | } 7 | 8 | type Markdown struct { 9 | Content string `json:"content"` 10 | } 11 | 12 | func NewQyWeChatMarkdown(content string) *QyWeChatMarkdown { 13 | return &QyWeChatMarkdown{ 14 | MsgType: "markdown", 15 | Markdown: Markdown{ 16 | Content: content, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sender/dingdingnotify.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "golangcode/alertmanager-webhook/model" 7 | "golangcode/alertmanager-webhook/transformer" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func SendToDingding(notification model.Notification, dingdingKey string, redisServer, redisPort, redisPassword string) { 14 | message, err := transformer.TransformToMarkdown(notification, redisServer, redisPort, redisPassword) 15 | if err != nil { 16 | log.Println(err) 17 | return 18 | } 19 | if dingdingKey != "" { 20 | var dingdingRobotURL string 21 | dingdingRobotURL = "https://oapi.dingtalk.com/robot/send?access_token=" + dingdingKey 22 | 23 | // 如果有告警信息才发送 24 | if (message.DingdingMessage.DmarkdownFiring.Dmarkdown != model.Dmarkdown{}) { 25 | dataFiring, err := json.Marshal(message.DingdingMessage.DmarkdownFiring) 26 | if err != nil { 27 | log.Println(err) 28 | return 29 | } 30 | 31 | reqFiring, err := http.NewRequest( 32 | "POST", 33 | dingdingRobotURL, 34 | bytes.NewBuffer(dataFiring)) 35 | 36 | if err != nil { 37 | log.Println(err) 38 | return 39 | } 40 | reqFiring.Header.Set("Content-Type", "application/json") 41 | client := &http.Client{} 42 | respFiring, err := client.Do(reqFiring) 43 | if err != nil { 44 | log.Println(err) 45 | return 46 | } 47 | defer respFiring.Body.Close() 48 | 49 | firingbody, err := ioutil.ReadAll(respFiring.Body) 50 | if err != nil { 51 | log.Println(err) 52 | } 53 | firingresponse := make(map[string]interface{}) 54 | err = json.Unmarshal(firingbody, &firingresponse) 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | if int(firingresponse["errcode"].(float64)) != 0 { 59 | log.Println("send alert message to dingding error: ", firingresponse) 60 | } 61 | } 62 | 63 | // 如果有恢复信息才发送 64 | if (message.DingdingMessage.DmarkdownResolved.Dmarkdown != model.Dmarkdown{}) { 65 | dataResolved, err := json.Marshal(message.DingdingMessage.DmarkdownResolved) 66 | if err != nil { 67 | log.Println(err) 68 | return 69 | } 70 | 71 | reqResolved, err := http.NewRequest( 72 | "POST", 73 | dingdingRobotURL, 74 | bytes.NewBuffer(dataResolved)) 75 | 76 | if err != nil { 77 | log.Println(err) 78 | return 79 | } 80 | 81 | reqResolved.Header.Set("Content-Type", "application/json") 82 | client := &http.Client{} 83 | respResolved, err := client.Do(reqResolved) 84 | if err != nil { 85 | log.Println(err) 86 | return 87 | } 88 | defer respResolved.Body.Close() 89 | 90 | resolvedbody, err := ioutil.ReadAll(respResolved.Body) 91 | if err != nil { 92 | log.Println(err) 93 | } 94 | resolvedresponse := make(map[string]interface{}) 95 | err = json.Unmarshal(resolvedbody, &resolvedresponse) 96 | if err != nil { 97 | log.Println(err) 98 | } 99 | if int(resolvedresponse["errcode"].(float64)) != 0 { 100 | log.Println("send resolved message to dingding error: ", resolvedresponse) 101 | } 102 | } 103 | } else { 104 | log.Println("dingding key doesn't exist") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sender/feishunotify.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "golangcode/alertmanager-webhook/model" 7 | "golangcode/alertmanager-webhook/transformer" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func SendToFeishu(notification model.Notification, feishuKey string, redisServer, redisPort, redisPassword string) { 14 | message, err := transformer.TransformToMarkdown(notification, redisServer, redisPort, redisPassword) 15 | if err != nil { 16 | log.Println(err) 17 | return 18 | } 19 | if feishuKey != "" { 20 | var feishuRobotURL string 21 | feishuRobotURL = "https://open.feishu.cn/open-apis/bot/v2/hook/" + feishuKey 22 | 23 | // 如果有告警信息才发送 24 | if message.FeishuMessage.CardFiring.Card.Elements[0].Text.Content != "" { 25 | dataFiring, err := json.Marshal(message.FeishuMessage.CardFiring) 26 | if err != nil { 27 | log.Println(err) 28 | return 29 | } 30 | 31 | reqFiring, err := http.NewRequest( 32 | "POST", 33 | feishuRobotURL, 34 | bytes.NewBuffer(dataFiring)) 35 | 36 | if err != nil { 37 | log.Println(err) 38 | return 39 | } 40 | reqFiring.Header.Set("Content-Type", "application/json") 41 | client := &http.Client{} 42 | respFiring, err := client.Do(reqFiring) 43 | if err != nil { 44 | log.Println(err) 45 | return 46 | } 47 | defer respFiring.Body.Close() 48 | 49 | firingbody, err := ioutil.ReadAll(respFiring.Body) 50 | if err != nil { 51 | log.Println(err) 52 | } 53 | firingresponse := make(map[string]interface{}) 54 | err = json.Unmarshal(firingbody, &firingresponse) 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | if int(firingresponse["code"].(float64)) != 0 { 59 | log.Println("send alert message to feishu error: ", firingresponse) 60 | } 61 | } 62 | 63 | // 如果有恢复信息才发送 64 | if message.FeishuMessage.CardResolved.Card.Elements[0].Text.Content != "" { 65 | dataResolved, err := json.Marshal(message.FeishuMessage.CardResolved) 66 | if err != nil { 67 | log.Println(err) 68 | return 69 | } 70 | 71 | reqResolved, err := http.NewRequest( 72 | "POST", 73 | feishuRobotURL, 74 | bytes.NewBuffer(dataResolved)) 75 | 76 | if err != nil { 77 | log.Println(err) 78 | return 79 | } 80 | 81 | reqResolved.Header.Set("Content-Type", "application/json") 82 | client := &http.Client{} 83 | respResolved, err := client.Do(reqResolved) 84 | if err != nil { 85 | log.Println(err) 86 | return 87 | } 88 | defer respResolved.Body.Close() 89 | 90 | resolvedbody, err := ioutil.ReadAll(respResolved.Body) 91 | if err != nil { 92 | log.Println(err) 93 | } 94 | resolvedresponse := make(map[string]interface{}) 95 | err = json.Unmarshal(resolvedbody, &resolvedresponse) 96 | if err != nil { 97 | log.Println(err) 98 | } 99 | if int(resolvedresponse["code"].(float64)) != 0 { 100 | log.Println("send resolved message to feishu error: ", resolvedresponse) 101 | } 102 | } 103 | } else { 104 | log.Println("feishu key doesn't exist") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sender/qywechatnotify.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "golangcode/alertmanager-webhook/model" 7 | "golangcode/alertmanager-webhook/transformer" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func SendToQywechat(notification model.Notification, qywechatKey string, redisServer, redisPort, redisPassword string) { 14 | message, err := transformer.TransformToMarkdown(notification, redisServer, redisPort, redisPassword) 15 | if err != nil { 16 | log.Println(err) 17 | return 18 | } 19 | if qywechatKey != "" { 20 | var qywechatRobotURL string 21 | qywechatRobotURL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + qywechatKey 22 | 23 | // 如果有告警信息才发送 24 | if (message.QywechatMessage.MarkdownFiring.Markdown != model.Markdown{}) { 25 | dataFiring, err := json.Marshal(message.QywechatMessage.MarkdownFiring) 26 | if err != nil { 27 | log.Println(err) 28 | return 29 | } 30 | 31 | reqFiring, err := http.NewRequest( 32 | "POST", 33 | qywechatRobotURL, 34 | bytes.NewBuffer(dataFiring)) 35 | 36 | if err != nil { 37 | log.Println(err) 38 | return 39 | } 40 | reqFiring.Header.Set("Content-Type", "application/json") 41 | client := &http.Client{} 42 | respFiring, err := client.Do(reqFiring) 43 | if err != nil { 44 | log.Println(err) 45 | return 46 | } 47 | defer respFiring.Body.Close() 48 | 49 | firingbody, err := ioutil.ReadAll(respFiring.Body) 50 | if err != nil { 51 | log.Println(err) 52 | } 53 | firingresponse := make(map[string]interface{}) 54 | err = json.Unmarshal(firingbody, &firingresponse) 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | if int(firingresponse["errcode"].(float64)) != 0 { 59 | log.Println("send alert message to qywechat error: ", firingresponse) 60 | } 61 | } 62 | 63 | // 如果有恢复信息才发送 64 | if (message.QywechatMessage.MarkdownResolved.Markdown != model.Markdown{}) { 65 | dataResolved, err := json.Marshal(message.QywechatMessage.MarkdownResolved) 66 | if err != nil { 67 | log.Println(err) 68 | return 69 | } 70 | 71 | reqResolved, err := http.NewRequest( 72 | "POST", 73 | qywechatRobotURL, 74 | bytes.NewBuffer(dataResolved)) 75 | 76 | if err != nil { 77 | log.Println(err) 78 | return 79 | } 80 | 81 | reqResolved.Header.Set("Content-Type", "application/json") 82 | client := &http.Client{} 83 | respResolved, err := client.Do(reqResolved) 84 | if err != nil { 85 | log.Println(err) 86 | return 87 | } 88 | defer respResolved.Body.Close() 89 | 90 | resolvedbody, err := ioutil.ReadAll(respResolved.Body) 91 | if err != nil { 92 | log.Println(err) 93 | } 94 | resolvedresponse := make(map[string]interface{}) 95 | err = json.Unmarshal(resolvedbody, &resolvedresponse) 96 | if err != nil { 97 | log.Println(err) 98 | } 99 | if int(resolvedresponse["errcode"].(float64)) != 0 { 100 | log.Println("send resolved message to qywechat error: ", resolvedresponse) 101 | } 102 | } 103 | } else { 104 | log.Println("qywechat key doesn't exist") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /template/alert.tmpl: -------------------------------------------------------------------------------- 1 | {{- if eq .Status `firing` -}} 2 | {{- /* 自定义触发告警时的内容格式 */ -}} 3 | 告警主题: {{.Annotations.summary}} 4 | 告警级别: {{.Labels.serverity}} 5 | 告警次数: {{.Count}} 6 | 告警主机: {{.Labels.instance}} 7 | 告警详情: {{.Annotations.description}} 8 | 触发时间: {{.StartTime}} 9 | 10 | {{- else if eq .Status `resolved` -}} 11 | {{- /* 自定义告警恢复时的内容格式 */ -}} 12 | 告警主题: {{.Annotations.summary}} 13 | 告警主机: {{.Labels.instance}} 14 | 开始时间: {{.StartTime}} 15 | 恢复时间: {{.EndTime}} 16 | {{- end -}} -------------------------------------------------------------------------------- /transformer/transformer.go: -------------------------------------------------------------------------------- 1 | package transformer 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/gomodule/redigo/redis" 7 | "golangcode/alertmanager-webhook/model" 8 | "log" 9 | "reflect" 10 | "text/template" 11 | "time" 12 | ) 13 | 14 | func TransformToMarkdown(notification model.Notification, redisServer, redisPort, redisPassword string) (message *model.Message, err error) { 15 | c, err := redis.Dial("tcp", fmt.Sprintf("%s:%s", redisServer, redisPort)) 16 | if err != nil { 17 | log.Println("connect redis failed: ", err) 18 | return 19 | } 20 | defer c.Close() 21 | 22 | if redisPassword != "" { 23 | _, err = c.Do("AUTH", redisPassword) 24 | if err != nil { 25 | log.Println("Redis auth error: ", err) 26 | return 27 | } 28 | } 29 | 30 | var notificationFiring model.Notification 31 | var notificationResolved model.Notification 32 | 33 | var cstZone = time.FixedZone("CST", 8*3600) 34 | 35 | var bufferFiring bytes.Buffer 36 | var bufferResolved bytes.Buffer 37 | 38 | for _, alert := range notification.Alerts { 39 | status := alert.Status 40 | if status == "firing" { 41 | notificationFiring.Version = notification.Version 42 | notificationFiring.GroupKey = notification.GroupKey 43 | notificationFiring.Status = "firing" 44 | notificationFiring.Receiver = notification.Receiver 45 | notificationFiring.GroupLabels = notification.GroupLabels 46 | notificationFiring.CommonLabels = notification.CommonLabels 47 | notificationFiring.ExternalURL = notification.ExternalURL 48 | notificationFiring.Alerts = append(notificationFiring.Alerts, alert) 49 | } else if status == "resolved" { 50 | notificationResolved.Version = notification.Version 51 | notificationResolved.GroupKey = notification.GroupKey 52 | notificationResolved.Status = "resolved" 53 | notificationResolved.Receiver = notification.Receiver 54 | notificationResolved.GroupLabels = notification.GroupLabels 55 | notificationResolved.CommonLabels = notification.CommonLabels 56 | notificationResolved.ExternalURL = notification.ExternalURL 57 | notificationResolved.Alerts = append(notificationResolved.Alerts, alert) 58 | } 59 | } 60 | 61 | if !reflect.DeepEqual(notificationFiring, model.Notification{}) { 62 | //bufferFiring.WriteString(fmt.Sprintf("# 触发告警\n")) 63 | for _, alert := range notificationFiring.Alerts { 64 | //annotations := alert.Annotations 65 | alert.StartTime = alert.StartsAt.In(cstZone).Format("2006-01-02 15:04:05") 66 | fingerprint := alert.Fingerprint 67 | _, err = c.Do("HSet", fingerprint, "startTime", alert.StartTime) 68 | if err != nil { 69 | log.Println(err) 70 | return 71 | } 72 | _, err = c.Do("Hincrby", fingerprint, "count", 1) 73 | if err != nil { 74 | log.Println(err) 75 | return 76 | } 77 | _, err = c.Do("HSet", fingerprint, "message", alert) 78 | if err != nil { 79 | log.Println(err) 80 | return 81 | } 82 | 83 | count, err := redis.Int(c.Do("HGet", fingerprint, "count")) 84 | if err != nil { 85 | log.Println("get alert count error: ", err) 86 | //return 87 | } 88 | alert.Count = count 89 | tmpl, err := template.ParseFiles("./template/alert.tmpl") 90 | err = tmpl.Execute(&bufferFiring, alert) 91 | if err != nil { 92 | log.Println("get firing message error:\n", err) 93 | } 94 | } 95 | } 96 | if !reflect.DeepEqual(notificationResolved, model.Notification{}) { 97 | //bufferResolved.WriteString(fmt.Sprintf("# 告警恢复\n")) 98 | for _, alert := range notificationResolved.Alerts { 99 | //annotations := alert.Annotations 100 | fingerprint := alert.Fingerprint 101 | alert.StartTime, err = redis.String(c.Do("HGet", fingerprint, "startTime")) 102 | if err != nil { 103 | log.Println("get alert startTime error: ", err) 104 | //return 105 | } 106 | alert.EndTime = alert.EndsAt.In(cstZone).Format("2006-01-02 15:04:05") 107 | tmpl, err := template.ParseFiles("./template/alert.tmpl") 108 | err = tmpl.Execute(&bufferResolved, alert) 109 | if err != nil { 110 | log.Println("get resolved message error:\n", err) 111 | } 112 | _, err = c.Do("Del", fingerprint) 113 | if err != nil { 114 | log.Println("delete key error: ", err) 115 | } 116 | } 117 | } 118 | 119 | // 转换为企业微信可以识别的格式 120 | var markdownFiring, markdownResolved *model.QyWeChatMarkdown 121 | var title string 122 | title = "# 触发告警\n" 123 | if bufferFiring.String() != "" { 124 | markdownFiring = model.NewQyWeChatMarkdown(title + bufferFiring.String()) 125 | } else { 126 | markdownFiring = model.NewQyWeChatMarkdown("") 127 | } 128 | title = "# 告警恢复\n" 129 | if bufferResolved.String() != "" { 130 | markdownResolved = model.NewQyWeChatMarkdown(title + bufferResolved.String()) 131 | } else { 132 | markdownResolved = model.NewQyWeChatMarkdown("") 133 | } 134 | 135 | // 转换为飞书可以识别的格式 136 | var cardFiring, cardResolved *model.CardMsg 137 | cardFiring = model.NewCardMsg("red", "触发告警") 138 | if bufferFiring.String() != "" { 139 | cardFiring.AddElement(bufferFiring.String()) 140 | } else { 141 | cardFiring.AddElement("") 142 | } 143 | 144 | cardResolved = model.NewCardMsg("green", "告警恢复") 145 | if bufferResolved.String() != "" { 146 | cardResolved.AddElement(bufferResolved.String()) 147 | } else { 148 | cardResolved.AddElement("") 149 | } 150 | 151 | // 转换为钉钉可以识别的格式 152 | var dmarkdownFiring, dmarkdownResolved *model.DingDingMarkdown 153 | title = "# 触发告警\n\n" 154 | if bufferFiring.String() != "" { 155 | dmarkdownFiring = model.NewDingDingMarkdown("触发告警", title+bufferFiring.String()) 156 | } else { 157 | dmarkdownFiring = model.NewDingDingMarkdown("", "") 158 | } 159 | 160 | title = "# 告警恢复\n\n" 161 | if bufferResolved.String() != "" { 162 | dmarkdownResolved = model.NewDingDingMarkdown("告警恢复", title+bufferResolved.String()) 163 | } else { 164 | dmarkdownResolved = model.NewDingDingMarkdown("", "") 165 | } 166 | 167 | // 将企业微信、飞书、钉钉消息进行封装 168 | message = model.NewMessage(markdownFiring, markdownResolved, cardFiring, cardResolved, dmarkdownFiring, dmarkdownResolved) 169 | 170 | return 171 | } 172 | --------------------------------------------------------------------------------