├── cmd └── alertmanager-webhook-adapter │ ├── .gitignore │ ├── main.go │ └── app │ ├── rootcmd.go │ └── options │ └── options.go ├── docs ├── feishu_en.png ├── feishu_zh.png ├── slack_en.png ├── slack_zh.png ├── weixin_en.png ├── weixin_zh.png ├── dingtalk_en.png ├── dingtalk_zh.png ├── feishu_en_2.png ├── feishu_zh_2.png ├── weixinapp_en.png ├── weixinapp_zh.png └── screenshot-zh.md ├── pkg ├── version │ └── version.go ├── senders │ ├── all.go │ ├── feishu.go │ ├── weixin.go │ ├── dingtalk.go │ ├── discord.go │ ├── slack.go │ └── weixinapp.go ├── models │ ├── templates │ │ ├── README.md │ │ ├── templates.go │ │ ├── default.zh.tmpl │ │ ├── slack.zh.tmpl │ │ ├── default.tmpl │ │ ├── discord-webhook.tmpl │ │ ├── dingtalk.zh.tmpl │ │ ├── slack.tmpl │ │ ├── dingtalk.tmpl │ │ ├── feishu.zh.tmpl │ │ ├── weixinapp.zh.tmpl │ │ ├── weixin.zh.tmpl │ │ ├── feishu.tmpl │ │ ├── weixin.tmpl │ │ └── weixinapp.tmpl │ ├── kv.go │ ├── alert.go │ └── tmpl.go └── api │ └── api.go ├── deploy ├── charts │ └── alertmanager-webhook-adapter │ │ ├── values.yaml │ │ ├── Chart.yaml │ │ ├── artifacthub-repo.yml │ │ └── templates │ │ ├── service.yaml │ │ ├── _helpers.tpl │ │ └── deployment.yaml ├── k8s │ ├── service.yaml │ └── deployment.yaml ├── alertmanaget-webhook-adapter.service └── alertmanager-webhook-adapter.init.sh ├── Dockerfile ├── .dockerignore ├── .gitignore ├── .github └── workflows │ ├── release-binary.yaml │ └── release-docker.yaml ├── go.mod ├── tests ├── grafana-alert.json ├── alert-test.sh └── alert.json ├── Makefile ├── LICENSE ├── README.md └── go.sum /cmd/alertmanager-webhook-adapter/.gitignore: -------------------------------------------------------------------------------- 1 | alertmanager-webhook-adapter 2 | templates 3 | -------------------------------------------------------------------------------- /docs/feishu_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/feishu_en.png -------------------------------------------------------------------------------- /docs/feishu_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/feishu_zh.png -------------------------------------------------------------------------------- /docs/slack_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/slack_en.png -------------------------------------------------------------------------------- /docs/slack_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/slack_zh.png -------------------------------------------------------------------------------- /docs/weixin_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/weixin_en.png -------------------------------------------------------------------------------- /docs/weixin_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/weixin_zh.png -------------------------------------------------------------------------------- /docs/dingtalk_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/dingtalk_en.png -------------------------------------------------------------------------------- /docs/dingtalk_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/dingtalk_zh.png -------------------------------------------------------------------------------- /docs/feishu_en_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/feishu_en_2.png -------------------------------------------------------------------------------- /docs/feishu_zh_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/feishu_zh_2.png -------------------------------------------------------------------------------- /docs/weixinapp_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/weixinapp_en.png -------------------------------------------------------------------------------- /docs/weixinapp_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bougou/alertmanager-webhook-adapter/HEAD/docs/weixinapp_zh.png -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version string 5 | Commit string 6 | BuildAt string 7 | ) 8 | -------------------------------------------------------------------------------- /deploy/charts/alertmanager-webhook-adapter/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | name: bougou/alertmanager-webhook-adapter 3 | tag: v1.1.10 4 | signature: MyIDC 5 | lang: en 6 | timezone: Asia/Shanghai 7 | -------------------------------------------------------------------------------- /deploy/charts/alertmanager-webhook-adapter/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: 1.1.10 3 | description: helm chart for alertmanager webhook adapter 4 | name: alertmanager-webhook-adapter 5 | version: 1.0.1 6 | type: application 7 | maintainers: 8 | - name: bougou 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18 2 | 3 | COPY / /src 4 | 5 | RUN cd /src \ 6 | && make build \ 7 | && mv /src/_output/alertmanager-webhook-adapter /alertmanager-webhook-adapter \ 8 | && rm -rf /src \ 9 | && true 10 | 11 | ENTRYPOINT [ "/alertmanager-webhook-adapter" ] 12 | CMD ["--debug"] 13 | -------------------------------------------------------------------------------- /deploy/k8s/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: alertmanager-webhook-adapter 6 | namespace: infra 7 | spec: 8 | ports: 9 | - port: 80 10 | targetPort: 8090 11 | protocol: TCP 12 | selector: 13 | app: alertmanager-webhook-adapter 14 | sessionAffinity: None 15 | -------------------------------------------------------------------------------- /deploy/charts/alertmanager-webhook-adapter/artifacthub-repo.yml: -------------------------------------------------------------------------------- 1 | # Artifact Hub repository metadata file 2 | # 3 | # ref: https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml 4 | 5 | repositoryID: alertmanager-webhook-adapter 6 | owners: 7 | - name: bougou 8 | email: bougou@126.com 9 | ignore: [] 10 | -------------------------------------------------------------------------------- /deploy/alertmanaget-webhook-adapter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=alertmanager-webhook-adapter 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | ExecStart=/usr/local/bin/alertmanager-webhook-adapter \ 8 | --listen-address=:8060 9 | Restart=always 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /deploy/charts/alertmanager-webhook-adapter/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "awa.fullname" . }} 6 | namespace: {{ include "awa.namespace" . }} 7 | spec: 8 | ports: 9 | - port: 80 10 | targetPort: 8090 11 | protocol: TCP 12 | selector: 13 | app: alertmanager-webhook-adapter 14 | sessionAffinity: None 15 | -------------------------------------------------------------------------------- /pkg/senders/all.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "github.com/bougou/webhook-adapter/models" 5 | restful "github.com/emicklei/go-restful/v3" 6 | ) 7 | 8 | type ChannelSenderCreator func(request *restful.Request) (models.Sender, error) 9 | 10 | var ChannelsSenderCreatorMap = map[string]ChannelSenderCreator{} 11 | 12 | func RegisterChannelsSenderCreator(channel string, creator ChannelSenderCreator) { 13 | ChannelsSenderCreatorMap[channel] = creator 14 | } 15 | -------------------------------------------------------------------------------- /cmd/alertmanager-webhook-adapter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/bougou/alertmanager-webhook-adapter/cmd/alertmanager-webhook-adapter/app" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func init() { 12 | cobra.OnInitialize(initConfig) 13 | } 14 | 15 | func initConfig() { 16 | } 17 | 18 | func main() { 19 | command := app.NewRootCommand() 20 | if err := command.Execute(); err != nil { 21 | fmt.Fprintln(os.Stderr, err) 22 | os.Exit(1) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Unit test / coverage reports 2 | htmlcov/ 3 | .tox/ 4 | .coverage 5 | .coverage.* 6 | .cache 7 | nosetests.xml 8 | coverage.xml 9 | *.cover 10 | .hypothesis/ 11 | cover.out 12 | e2e-reports/ 13 | 14 | # Translations 15 | *.mo 16 | *.pot 17 | 18 | # emacs 19 | .\#* 20 | 21 | # vim 22 | *.swp 23 | 24 | # ignore bin 25 | /bin/ 26 | _output 27 | dist 28 | 29 | # ignore vscode 30 | .vscode 31 | 32 | # goland 33 | .idea 34 | 35 | # Test binary, build with `go test -c` 36 | *.test 37 | 38 | # Output of the go coverage tool, specifically when used with LiteIDE 39 | *.out 40 | 41 | .DS_Store 42 | 43 | _output 44 | build 45 | output 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Unit test / coverage reports 2 | htmlcov/ 3 | .tox/ 4 | .coverage 5 | .coverage.* 6 | .cache 7 | nosetests.xml 8 | coverage.xml 9 | *.cover 10 | .hypothesis/ 11 | cover.out 12 | e2e-reports/ 13 | 14 | # Translations 15 | *.mo 16 | *.pot 17 | 18 | # emacs 19 | .\#* 20 | 21 | # vim 22 | *.swp 23 | 24 | # ignore bin 25 | /bin/ 26 | _output 27 | dist 28 | 29 | # ignore vscode 30 | .vscode 31 | 32 | # goland 33 | .idea 34 | 35 | # Test binary, build with `go test -c` 36 | *.test 37 | 38 | # Output of the go coverage tool, specifically when used with LiteIDE 39 | *.out 40 | 41 | .DS_Store 42 | deploy/charts/alertmanager-webhook-adapter-*.tgz 43 | -------------------------------------------------------------------------------- /pkg/models/templates/README.md: -------------------------------------------------------------------------------- 1 | # Templates 2 | 3 | ## Reference 4 | 5 | ### feishu 6 | 7 | - https://open.feishu.cn/document/common-capabilities/message-card/message-cards-content/using-markdown-tags#abc9b025 8 | 9 | ### weixin (bot) 企业微信群机器人 10 | 11 | - https://developer.work.weixin.qq.com/document/path/91770#markdown%E7%B1%BB%E5%9E%8B 12 | 13 | ### weixinapp 企业微信应用 14 | 15 | - https://developer.work.weixin.qq.com/document/path/90250#markdown%E6%B6%88%E6%81%AF 16 | 17 | ### dingtalk 18 | 19 | - https://open.dingtalk.com/document/orgapp/enterprise-internal-robots-send-markdown-messages 20 | 21 | ### slack 22 | 23 | - https://api.slack.com/reference/surfaces/formatting#basic-formatting 24 | -------------------------------------------------------------------------------- /docs/screenshot-zh.md: -------------------------------------------------------------------------------- 1 | ## 内置模板通知截图 2 | 3 | | 企业微信机器人 | 企业微信应用 | 钉钉群机器人 | 飞书群机器人 | 4 | | ---------------------------------------- | ------------------------------------------- | ------------------------------------------ | ---------------------------------------- | 5 | | | | | | 6 | 7 | | Slack App | 8 | | --------------------------------------- | 9 | | | 10 | -------------------------------------------------------------------------------- /deploy/charts/alertmanager-webhook-adapter/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "awa.namespace" -}} 2 | {{- if .Values.namespaceOverride -}} 3 | {{- .Values.namespaceOverride -}} 4 | {{- else -}} 5 | {{- .Release.Namespace -}} 6 | {{- end -}} 7 | {{- end -}} 8 | 9 | 10 | {{- define "awa.fullname" -}} 11 | {{- if .Values.fullnameOverride -}} 12 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 13 | {{- else -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- if contains $name .Release.Name -}} 16 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 19 | {{- end -}} 20 | {{- end -}} 21 | {{- end -}} 22 | -------------------------------------------------------------------------------- /pkg/senders/feishu.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bougou/webhook-adapter/channels/feishu" 7 | "github.com/bougou/webhook-adapter/models" 8 | restful "github.com/emicklei/go-restful/v3" 9 | ) 10 | 11 | const ( 12 | ChannelTypeFeishu = "feishu" 13 | ) 14 | 15 | func init() { 16 | RegisterChannelsSenderCreator(ChannelTypeFeishu, createFeishuSender) 17 | } 18 | 19 | func createFeishuSender(request *restful.Request) (models.Sender, error) { 20 | token := request.QueryParameter("token") 21 | if token == "" { 22 | return nil, fmt.Errorf("not token found for feishu channel") 23 | } 24 | 25 | msgType := request.QueryParameter("msg_type") 26 | if msgType == "" { 27 | msgType = "markdown" 28 | } 29 | 30 | var sender models.Sender = feishu.NewSender(token, msgType) 31 | return sender, nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/senders/weixin.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bougou/webhook-adapter/channels/weixin" 7 | "github.com/bougou/webhook-adapter/models" 8 | restful "github.com/emicklei/go-restful/v3" 9 | ) 10 | 11 | const ( 12 | ChannelTypeWeixin = "weixin" 13 | ) 14 | 15 | func init() { 16 | RegisterChannelsSenderCreator(ChannelTypeWeixin, createWeixinSender) 17 | } 18 | 19 | func createWeixinSender(request *restful.Request) (models.Sender, error) { 20 | token := request.QueryParameter("token") 21 | if token == "" { 22 | return nil, fmt.Errorf("not token found for weixin channel") 23 | } 24 | 25 | msgType := request.QueryParameter("msg_type") 26 | if msgType == "" { 27 | msgType = "markdown" 28 | } 29 | 30 | var sender models.Sender = weixin.NewSender(token, msgType) 31 | return sender, nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/senders/dingtalk.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bougou/webhook-adapter/channels/dingtalk" 7 | "github.com/bougou/webhook-adapter/models" 8 | restful "github.com/emicklei/go-restful/v3" 9 | ) 10 | 11 | const ( 12 | ChannelTypeDingtalk = "dingtalk" 13 | ) 14 | 15 | func init() { 16 | RegisterChannelsSenderCreator(ChannelTypeDingtalk, createDingtalkSender) 17 | } 18 | 19 | func createDingtalkSender(request *restful.Request) (models.Sender, error) { 20 | token := request.QueryParameter("token") 21 | if token == "" { 22 | return nil, fmt.Errorf("not token found for dingtalk channel") 23 | } 24 | 25 | msgType := request.QueryParameter("msg_type") 26 | if msgType == "" { 27 | msgType = "markdown" 28 | } 29 | 30 | var sender models.Sender = dingtalk.NewSender(token, msgType) 31 | return sender, nil 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/release-binary.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release binary 3 | 4 | on: 5 | push: 6 | branches: 7 | - "main" 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | 13 | binary-release: 14 | name: binary release 15 | runs-on: ubuntu-latest 16 | if: startsWith(github.event.ref, 'refs/tags/v') 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: set up Go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: 1.18 25 | 26 | - name: build & test 27 | run: | 28 | make dependencies 29 | make build-all 30 | 31 | - name: release 32 | uses: marvinpinto/action-automatic-releases@latest 33 | with: 34 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 35 | prerelease: false 36 | files: | 37 | _output/* 38 | -------------------------------------------------------------------------------- /deploy/k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: alertmanager-webhook-adapter 6 | namespace: infra 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: alertmanager-webhook-adapter 12 | template: 13 | metadata: 14 | labels: 15 | app: alertmanager-webhook-adapter 16 | spec: 17 | containers: 18 | - name: webhook 19 | image: bougou/alertmanager-webhook-adapter:v1.1.10 20 | command: 21 | - /alertmanager-webhook-adapter 22 | - --listen-address=:8090 23 | - --signature=MyIDC 24 | - --tmpl-lang=zh 25 | env: 26 | - name: TZ 27 | value: Asia/Shanghai 28 | resources: 29 | requests: 30 | memory: 50Mi 31 | cpu: 100m 32 | limits: 33 | memory: 250Mi 34 | cpu: 500m 35 | restartPolicy: Always 36 | -------------------------------------------------------------------------------- /pkg/senders/discord.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bougou/webhook-adapter/channels/discord" 7 | "github.com/bougou/webhook-adapter/models" 8 | restful "github.com/emicklei/go-restful/v3" 9 | ) 10 | 11 | const ( 12 | ChannelTypeDiscordWebhook = "discord-webhook" 13 | ) 14 | 15 | func init() { 16 | RegisterChannelsSenderCreator(ChannelTypeDiscordWebhook, createDiscordWebhookSender) 17 | } 18 | 19 | func createDiscordWebhookSender(request *restful.Request) (models.Sender, error) { 20 | id := request.QueryParameter("id") 21 | if id == "" { 22 | return nil, fmt.Errorf("not id found for discord-webhook channel") 23 | } 24 | 25 | token := request.QueryParameter("token") 26 | if token == "" { 27 | return nil, fmt.Errorf("not token found for discord-webhook channel") 28 | } 29 | 30 | msgType := request.QueryParameter("msg_type") 31 | if msgType == "" { 32 | msgType = "markdown" 33 | } 34 | 35 | var sender models.Sender = discord.NewWebhookSender(id, token) 36 | return sender, nil 37 | } 38 | -------------------------------------------------------------------------------- /deploy/charts/alertmanager-webhook-adapter/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "awa.fullname" . }} 6 | namespace: {{ include "awa.namespace" . }} 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: alertmanager-webhook-adapter 12 | template: 13 | metadata: 14 | labels: 15 | app: alertmanager-webhook-adapter 16 | spec: 17 | containers: 18 | - name: webhook 19 | image: {{ .Values.image.name }}:{{ .Values.image.tag }} 20 | command: 21 | - /alertmanager-webhook-adapter 22 | - --listen-address=:8090 23 | - --signature={{ .Values.signature }} 24 | - --tmpl-lang={{ .Values.lang | default "en" }} 25 | env: 26 | - name: TZ 27 | value: {{ .Values.timezone }} 28 | resources: 29 | requests: 30 | memory: 50Mi 31 | cpu: 100m 32 | limits: 33 | memory: 250Mi 34 | cpu: 500m 35 | restartPolicy: Always 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bougou/alertmanager-webhook-adapter 2 | 3 | go 1.20 4 | 5 | replace github.com/bougou/alertmanager-webhook-adapter v0.0.0 => ./ 6 | 7 | require ( 8 | github.com/bougou/webhook-adapter v0.1.1 9 | github.com/emicklei/go-restful/v3 v3.4.0 10 | github.com/kr/pretty v0.1.0 11 | github.com/spf13/cobra v1.1.3 12 | golang.org/x/text v0.3.3 13 | ) 14 | 15 | require ( 16 | github.com/bwmarrin/discordgo v0.28.1 // indirect 17 | github.com/gorilla/websocket v1.4.2 // indirect 18 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 19 | github.com/json-iterator/go v1.1.10 // indirect 20 | github.com/kr/text v0.1.0 // indirect 21 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 22 | github.com/modern-go/reflect2 v1.0.1 // indirect 23 | github.com/pkg/errors v0.9.1 // indirect 24 | github.com/slack-go/slack v0.9.4 // indirect 25 | github.com/spf13/pflag v1.0.5 // indirect 26 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect 27 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /pkg/senders/slack.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/bougou/webhook-adapter/channels/slack" 8 | "github.com/bougou/webhook-adapter/models" 9 | restful "github.com/emicklei/go-restful/v3" 10 | ) 11 | 12 | const ( 13 | ChannelTypeSlack = "slack" 14 | ) 15 | 16 | func init() { 17 | RegisterChannelsSenderCreator(ChannelTypeSlack, createSlackSender) 18 | } 19 | 20 | func createSlackSender(request *restful.Request) (models.Sender, error) { 21 | token := request.QueryParameter("token") 22 | if token == "" { 23 | return nil, fmt.Errorf("not token found for slack") 24 | } 25 | channel := request.QueryParameter("channel") 26 | if channel == "" { 27 | return nil, fmt.Errorf("not channel found for slack") 28 | } 29 | // add # if channel not begin with # 30 | if !strings.HasPrefix(channel, "#") { 31 | channel = "#" + channel 32 | } 33 | 34 | msgType := request.QueryParameter("msg_type") 35 | if msgType == "" { 36 | msgType = "markdown" 37 | } 38 | 39 | var sender models.Sender = slack.NewSender(token, channel, msgType) 40 | return sender, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/senders/weixinapp.go: -------------------------------------------------------------------------------- 1 | package senders 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/bougou/webhook-adapter/channels/weixinapp" 8 | "github.com/bougou/webhook-adapter/models" 9 | restful "github.com/emicklei/go-restful/v3" 10 | ) 11 | 12 | const ( 13 | ChannelTypeWeixinApp = "weixinapp" 14 | ) 15 | 16 | func init() { 17 | RegisterChannelsSenderCreator(ChannelTypeWeixinApp, createWeixinappSender) 18 | } 19 | 20 | func createWeixinappSender(request *restful.Request) (models.Sender, error) { 21 | corpID := request.QueryParameter("corp_id") 22 | if corpID == "" { 23 | return nil, fmt.Errorf("not core_id found for weixin channel") 24 | } 25 | 26 | agentID := request.QueryParameter("agent_id") 27 | if agentID == "" { 28 | return nil, fmt.Errorf("not agent_id found for weixin channel") 29 | } 30 | 31 | aID, err := strconv.Atoi(agentID) 32 | if err != nil { 33 | return nil, fmt.Errorf("agent_id must be integer") 34 | } 35 | 36 | agentSecret := request.QueryParameter("agent_secret") 37 | if agentSecret == "" { 38 | return nil, fmt.Errorf("not agent_secret found for weixin channel") 39 | } 40 | 41 | toUser := request.QueryParameter("to_user") 42 | toParty := request.QueryParameter("to_party") 43 | toTag := request.QueryParameter("to_tag") 44 | 45 | if toUser == "" && toParty == "" && toTag == "" { 46 | return nil, fmt.Errorf("must specify one of to_user,to_party,to_tag") 47 | } 48 | 49 | msgType := request.QueryParameter("msg_type") 50 | if msgType == "" { 51 | msgType = "markdown" 52 | } 53 | 54 | var sender models.Sender = weixinapp.NewSender(corpID, aID, agentSecret, msgType, toUser, toParty, toTag) 55 | return sender, nil 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/release-docker.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release docker 3 | 4 | on: 5 | push: 6 | branches: 7 | - "main" 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | docker-release: 13 | name: docker release 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: AutoModality/action-clean@v1 18 | 19 | - uses: actions/checkout@v2 20 | 21 | - name: set up Docker Buildx 22 | uses: docker/setup-buildx-action@v1 23 | 24 | - name: docker login 25 | uses: docker/login-action@v1.10.0 26 | with: 27 | # registry: ${{ secrets.DOCKERHUB_ADDR }} 28 | username: ${{ secrets.DOCKERHUB_USER }} 29 | password: ${{ secrets.DOCKERHUB_PASS }} 30 | 31 | - name: prepare short tag 32 | id: prepare_short_tag 33 | run: | 34 | SHORT_TAG=`git describe --abbrev=5 --dirty --tags --always` 35 | echo "::set-output name=image_short_tag::$SHORT_TAG" 36 | echo "::notice title=Build Image Short Tag::$SHORT_TAG" 37 | 38 | - name: prepare tag 39 | id: prepare_tag 40 | run: | 41 | TIME_TAG=`TZ=Zero date +"%y%m%d%H%M%S"` 42 | COMMIT_TAG=`echo $GITHUB_SHA | cut -c 1-7` 43 | TAG="$TIME_TAG-$COMMIT_TAG" 44 | echo "::set-output name=image_tag::$TAG" 45 | echo "::notice title=Build Image Tag::$TAG" 46 | 47 | - name: build and push 48 | uses: docker/build-push-action@v2 49 | with: 50 | push: true 51 | context: . 52 | platforms: linux/amd64,linux/arm64 53 | tags: | 54 | bougou/alertmanager-webhook-adapter:latest 55 | bougou/alertmanager-webhook-adapter:${{ steps.prepare_tag.outputs.image_tag }} 56 | bougou/alertmanager-webhook-adapter:${{ steps.prepare_short_tag.outputs.image_short_tag }} 57 | -------------------------------------------------------------------------------- /deploy/alertmanager-webhook-adapter.init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # alertmanager-webhook-adapter 3 | # chkconfig: 345 20 80 4 | # description: alertmanager-webhook-adapter 5 | # processname: alertmanager-webhook-adapter 6 | 7 | listen_address=":8090" 8 | signature="Unknown" 9 | 10 | DAEMON_PATH="/data/alertmanager-webhook-adapter/bin" 11 | DAEMON=alertmanager-webhook-adapter 12 | DAEMONOPTS="--listen-address=$listen_address --signature=$signature" 13 | 14 | NAME=alertmanager-webhook-adapter 15 | DESC="alertmanager-webhook-adapter" 16 | PIDFILE=/var/run/$NAME.pid 17 | SCRIPTNAME=/etc/init.d/$NAME 18 | 19 | case "$1" in 20 | start) 21 | printf "%-50s" "Starting $NAME..." 22 | cd $DAEMON_PATH 23 | PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!` 24 | #echo "Saving PID" $PID " to " $PIDFILE 25 | if [ -z $PID ]; then 26 | printf "%s\n" "Fail" 27 | else 28 | echo $PID > $PIDFILE 29 | printf "%s\n" "Ok" 30 | fi 31 | ;; 32 | status) 33 | printf "%-50s" "Checking $NAME..." 34 | if [ -f $PIDFILE ]; then 35 | PID=`cat $PIDFILE` 36 | if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then 37 | printf "%s\n" "Process dead but pidfile exists" 38 | else 39 | echo "Running" 40 | fi 41 | else 42 | printf "%s\n" "Service not running" 43 | fi 44 | ;; 45 | stop) 46 | printf "%-50s" "Stopping $NAME" 47 | PID=`cat $PIDFILE` 48 | cd $DAEMON_PATH 49 | if [ -f $PIDFILE ]; then 50 | kill -HUP $PID 51 | printf "%s\n" "Ok" 52 | rm -f $PIDFILE 53 | else 54 | printf "%s\n" "pidfile not found" 55 | fi 56 | ;; 57 | 58 | restart) 59 | $0 stop 60 | $0 start 61 | ;; 62 | 63 | *) 64 | echo "Usage: $0 {status|start|stop|restart}" 65 | exit 1 66 | esac 67 | -------------------------------------------------------------------------------- /cmd/alertmanager-webhook-adapter/app/rootcmd.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/bougou/alertmanager-webhook-adapter/cmd/alertmanager-webhook-adapter/app/options" 9 | "github.com/bougou/alertmanager-webhook-adapter/pkg/version" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func NewRootCommand() *cobra.Command { 14 | o := options.NewAppOptions() 15 | 16 | rootCmd := &cobra.Command{ 17 | Use: "alertmanager-webhook-adapter", 18 | Short: "alertmanager-webhook-adapter", 19 | Long: `alertmanager-webhook-adapter`, 20 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 21 | if o.Version { 22 | fmt.Printf("Version: %s\n", version.Version) 23 | fmt.Printf("Commit: %s\n", version.Commit) 24 | fmt.Printf("BuildAt: %s\n", version.BuildAt) 25 | return nil 26 | } 27 | return nil 28 | }, 29 | 30 | Run: func(cmd *cobra.Command, args []string) { 31 | if o.Version { 32 | return 33 | } 34 | 35 | if err := o.Run(); err != nil { 36 | fmt.Println("Error:", err) 37 | os.Exit(1) 38 | return 39 | } 40 | }, 41 | } 42 | 43 | rootCmd.Flags().StringVarP(&o.Addr, "listen-address", "l", "0.0.0.0:8090", "the address to listen") 44 | rootCmd.Flags().StringVarP(&o.Signature, "signature", "s", "未知", "the signature") 45 | rootCmd.Flags().StringVarP(&o.TmplDir, "tmpl-dir", "d", "", "the tmpl dir") 46 | rootCmd.Flags().StringVarP(&o.TmplName, "tmpl-name", "t", "", "the tmpl name") 47 | rootCmd.Flags().StringVarP(&o.TmplDefault, "tmpl-default", "n", "", "the default tmpl name") 48 | rootCmd.Flags().StringVarP(&o.TmplLang, "tmpl-lang", "", "", "the language for template filename") 49 | rootCmd.Flags().BoolVarP(&o.Version, "version", "v", false, "show version") 50 | rootCmd.Flags().BoolVarP(&o.Debug, "debug", "", false, "enable verbose output ") 51 | 52 | rootCmd.Flags().AddGoFlagSet(flag.CommandLine) 53 | 54 | return rootCmd 55 | } 56 | -------------------------------------------------------------------------------- /pkg/models/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | //go:embed default.tmpl 8 | var DefaultTmpl string 9 | 10 | //go:embed default.zh.tmpl 11 | var DefaultTmplZH string 12 | 13 | //go:embed weixin.tmpl 14 | var DefaultTmplWeixin string 15 | 16 | //go:embed weixin.zh.tmpl 17 | var DefaultTmplWeixinZH string 18 | 19 | //go:embed weixinapp.tmpl 20 | var DefaultTmplWeixinapp string 21 | 22 | //go:embed weixinapp.zh.tmpl 23 | var DefaultTmplWeixinappZH string 24 | 25 | //go:embed dingtalk.tmpl 26 | var DefaultTmplDingTalk string 27 | 28 | //go:embed dingtalk.zh.tmpl 29 | var DefaultTmplDingTalkZH string 30 | 31 | //go:embed feishu.tmpl 32 | var DefaultTmplFeishu string 33 | 34 | //go:embed feishu.zh.tmpl 35 | var DefaultTmplFeishuZH string 36 | 37 | //go:embed slack.tmpl 38 | var DefaultTmplSlack string 39 | 40 | //go:embed slack.zh.tmpl 41 | var DefaultTmplSlackZH string 42 | 43 | //go:embed discord-webhook.tmpl 44 | var DefaultTmplDiscordWebhook string 45 | 46 | var DefaultTmplByLang = map[string]string{ 47 | "en": DefaultTmpl, 48 | "zh": DefaultTmplZH, 49 | } 50 | 51 | // Must define for every supported channel 52 | var ChannelsDefaultTmplMapByLang = map[string]map[string]string{ 53 | "en": { 54 | "dingtalk": DefaultTmplDingTalk, 55 | "feishu": DefaultTmplFeishu, 56 | "slack": DefaultTmplSlack, 57 | "weixin": DefaultTmplWeixin, 58 | "weixinapp": DefaultTmplWeixinapp, 59 | "discord-webhook": DefaultTmplDiscordWebhook, 60 | }, 61 | "zh": { 62 | "dingtalk": DefaultTmplDingTalkZH, 63 | "feishu": DefaultTmplFeishuZH, 64 | "slack": DefaultTmplSlackZH, 65 | "weixin": DefaultTmplWeixinZH, 66 | "weixinapp": DefaultTmplWeixinappZH, 67 | }, 68 | } 69 | 70 | func DefaultSupportedLangs() []string { 71 | res := make([]string, 0) 72 | for k := range DefaultTmplByLang { 73 | res = append(res, k) 74 | } 75 | return res 76 | } 77 | -------------------------------------------------------------------------------- /pkg/models/kv.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "sort" 4 | 5 | // Pair is a key/value string pair. 6 | type Pair struct { 7 | Name, Value string 8 | } 9 | 10 | // Pairs is a list of key/value string pairs. 11 | type Pairs []Pair 12 | 13 | // Names returns a list of names of the pairs. 14 | func (ps Pairs) Names() []string { 15 | ns := make([]string, 0, len(ps)) 16 | for _, p := range ps { 17 | ns = append(ns, p.Name) 18 | } 19 | return ns 20 | } 21 | 22 | // Values returns a list of values of the pairs. 23 | func (ps Pairs) Values() []string { 24 | vs := make([]string, 0, len(ps)) 25 | for _, p := range ps { 26 | vs = append(vs, p.Value) 27 | } 28 | return vs 29 | } 30 | 31 | // KV is a set of key/value string pairs. 32 | type KV map[string]string 33 | 34 | // SortedPairs returns a sorted list of key/value pairs. 35 | func (kv KV) SortedPairs() Pairs { 36 | var ( 37 | pairs = make([]Pair, 0, len(kv)) 38 | keys = make([]string, 0, len(kv)) 39 | sortStart = 0 40 | ) 41 | for k := range kv { 42 | if k == "alertname" { 43 | keys = append([]string{k}, keys...) 44 | sortStart = 1 45 | } else { 46 | keys = append(keys, k) 47 | } 48 | } 49 | sort.Strings(keys[sortStart:]) 50 | 51 | for _, k := range keys { 52 | pairs = append(pairs, Pair{k, kv[k]}) 53 | } 54 | return pairs 55 | } 56 | 57 | // Remove returns a copy of the key/value set without the given keys. 58 | func (kv KV) Remove(keys []string) KV { 59 | keySet := make(map[string]struct{}, len(keys)) 60 | for _, k := range keys { 61 | keySet[k] = struct{}{} 62 | } 63 | 64 | res := KV{} 65 | for k, v := range kv { 66 | if _, ok := keySet[k]; !ok { 67 | res[k] = v 68 | } 69 | } 70 | return res 71 | } 72 | 73 | // Names returns the names of the label names in the LabelSet. 74 | func (kv KV) Names() []string { 75 | return kv.SortedPairs().Names() 76 | } 77 | 78 | // Values returns a list of the values in the LabelSet. 79 | func (kv KV) Values() []string { 80 | return kv.SortedPairs().Values() 81 | } 82 | -------------------------------------------------------------------------------- /tests/grafana-alert.json: -------------------------------------------------------------------------------- 1 | { 2 | "receiver": "test-webhook", 3 | "status": "firing", 4 | "alerts": [ 5 | { 6 | "status": "firing", 7 | "labels": { 8 | "alertname": "CPU Usage High", 9 | "grafana_folder": "alert", 10 | "testkey": "testvalue" 11 | }, 12 | "annotations": {}, 13 | "startsAt": "2023-03-29T09:41:10Z", 14 | "endsAt": "0001-01-01T00:00:00Z", 15 | "generatorURL": "http://localhost/grafana/alerting/grafana/fOO_kffVz/view?orgId=1", 16 | "fingerprint": "68d4e65db3ad9208", 17 | "silenceURL": "http://localhost/grafana/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DCPU+Usage+High\u0026matcher=grafana_folder%3Dalert\u0026matcher=testkey%3Dtestvalue", 18 | "dashboardURL": "http://localhost/grafana/d/200ac8fdbfbb74b39aff88118e4d1c2c?orgId=1", 19 | "panelURL": "http://localhost/grafana/d/200ac8fdbfbb74b39aff88118e4d1c2c?orgId=1\u0026viewPanel=1", 20 | "values": { 21 | "C": 12, 22 | "D": 1 23 | }, 24 | "valueString": "[ var='D' labels={} value=1 ], [ var='C' labels={} value=12 ]" 25 | } 26 | ], 27 | "groupLabels": { 28 | "alertname": "CPU Usage High", 29 | "grafana_folder": "alert" 30 | }, 31 | "commonLabels": { 32 | "alertname": "CPU Usage High", 33 | "grafana_folder": "alert", 34 | "testkey": "testvalue" 35 | }, 36 | "commonAnnotations": {}, 37 | "externalURL": "http://localhost/grafana/", 38 | "version": "1", 39 | "groupKey": "{}/{}:{alertname=\"CPU Usage High\", grafana_folder=\"alert\"}", 40 | "truncatedAlerts": 0, 41 | "orgId": 1, 42 | "title": "[FIRING:1] CPU Usage High alert (testvalue)", 43 | "state": "alerting", 44 | "message": "**Firing**\n\nValue: C=12, D=1\nLabels:\n - alertname = CPU Usage High\n - grafana_folder = alert\n - testkey = testvalue\nAnnotations:\nSource: http://localhost/grafana/alerting/grafana/fOO_kffVz/view?orgId=1\nSilence: http://localhost/grafana/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DCPU+Usage+High\u0026matcher=grafana_folder%3Dalert\u0026matcher=testkey%3Dtestvalue\nDashboard: http://localhost/grafana/d/200ac8fdbfbb74b39aff88118e4d1c2c?orgId=1\nPanel: http://localhost/grafana/d/200ac8fdbfbb74b39aff88118e4d1c2c?orgId=1\u0026viewPanel=1\n" 45 | } 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_VERSION ?= $(shell git describe --abbrev=5 --dirty --tags --always) 2 | GIT_COMMIT := $(shell git rev-parse --short=8 HEAD) 3 | BUILD_TIME := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') 4 | 5 | BINDIR := $(PWD)/bin 6 | OUTPUT_DIR := $(PWD)/_output 7 | 8 | GOOS ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') 9 | GOARCH ?= amd64 10 | 11 | LDFLAGS := $(LDFLAGS) -X github.com/bougou/alertmanager-webhook-adapter/pkg/version.Version=$(APP_VERSION) 12 | LDFLAGS := $(LDFLAGS) -X github.com/bougou/alertmanager-webhook-adapter/pkg/version.Commit=$(GIT_COMMIT) 13 | LDFLAGS := $(LDFLAGS) -X github.com/bougou/alertmanager-webhook-adapter/pkg/version.BuildAt=$(BUILD_TIME) 14 | 15 | PATH := $(BINDIR):$(PATH) 16 | SHELL := env PATH='$(PATH)' /bin/sh 17 | 18 | all: build 19 | 20 | # Run tests 21 | test: fmt vet 22 | @# Disable --race until https://github.com/kubernetes-sigs/controller-runtime/issues/1171 is fixed. 23 | ginkgo --randomizeAllSpecs --randomizeSuites --failOnPending --flakeAttempts=2 \ 24 | --cover --coverprofile cover.out --trace --progress $(TEST_ARGS)\ 25 | ./pkg/... ./cmd/... 26 | 27 | # Build alertmanager-webhook-adapter binary 28 | build: fmt vet 29 | go build -ldflags "$(LDFLAGS)" -o $(OUTPUT_DIR)/alertmanager-webhook-adapter ./cmd/alertmanager-webhook-adapter 30 | 31 | # Cross compiler 32 | build-all: fmt vet 33 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -a -o $(OUTPUT_DIR)/alertmanager-webhook-adapter-$(APP_VERSION)-linux-amd64 ./cmd/alertmanager-webhook-adapter 34 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -a -o $(OUTPUT_DIR)/alertmanager-webhook-adapter-$(APP_VERSION)-linux-arm64 ./cmd/alertmanager-webhook-adapter 35 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -a -o $(OUTPUT_DIR)/alertmanager-webhook-adapter-$(APP_VERSION)-darwin-amd64 ./cmd/alertmanager-webhook-adapter 36 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -a -o $(OUTPUT_DIR)/alertmanager-webhook-adapter-$(APP_VERSION)-darwin-arm64 ./cmd/alertmanager-webhook-adapter 37 | 38 | # Run go fmt against code 39 | fmt: 40 | go fmt ./pkg/... ./cmd/... 41 | 42 | # Run go vet against code 43 | vet: 44 | go vet ./pkg/... ./cmd/... 45 | 46 | lint: 47 | $(BINDIR)/golangci-lint run --timeout 2m0s ./pkg/... ./cmd/... 48 | 49 | dependencies: 50 | test -d $(BINDIR) || mkdir $(BINDIR) 51 | GOBIN=$(BINDIR) go install github.com/onsi/ginkgo/ginkgo@v1.16.4 52 | 53 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $(BINDIR) latest 54 | -------------------------------------------------------------------------------- /cmd/alertmanager-webhook-adapter/app/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | restful "github.com/emicklei/go-restful/v3" 13 | 14 | "github.com/bougou/alertmanager-webhook-adapter/pkg/api" 15 | "github.com/bougou/alertmanager-webhook-adapter/pkg/models" 16 | "github.com/bougou/alertmanager-webhook-adapter/pkg/models/templates" 17 | ) 18 | 19 | type AppOptions struct { 20 | Addr string 21 | Signature string 22 | TmplDir string 23 | TmplName string 24 | TmplDefault string 25 | TmplLang string 26 | Version bool 27 | Debug bool 28 | } 29 | 30 | func NewAppOptions() *AppOptions { 31 | return &AppOptions{} 32 | } 33 | 34 | func (o *AppOptions) Run() error { 35 | execFile, err := os.Executable() 36 | if err != nil { 37 | panic("fatal") 38 | } 39 | 40 | // If using builtin templates (o.TmplDir == ""), then we must check whether or not the specified lang is supported. 41 | if o.TmplLang != "" && o.TmplDir == "" { 42 | if _, exists := templates.DefaultTmplByLang[o.TmplLang]; !exists { 43 | return fmt.Errorf("the builtin templates does not support specified lang (%s), builtin supported langs: (%s)", o.TmplLang, strings.Join(templates.DefaultSupportedLangs(), ",")) 44 | } 45 | if err := models.LoadDefaultTemplate(o.TmplLang); err != nil { 46 | return fmt.Errorf("load default template for lang (%s) failed, err: %s", o.TmplLang, err) 47 | } 48 | } 49 | 50 | if o.TmplDir == "" && (o.TmplName != "" || o.TmplDefault != "") { 51 | fmt.Println("Warning, there is no meaning to specify --tmpl-name or --tmpl-default option without specify --tmpl-dir option, just ignored.") 52 | } 53 | 54 | if o.TmplDir != "" { 55 | if o.TmplName != "" && o.TmplDefault != "" { 56 | fmt.Println("Warning, there is no meaning to specify --tmpl-name and --tmpl-default options together, --tmpl-default is ignored.") 57 | o.TmplDefault = "" 58 | } 59 | 60 | if !filepath.IsAbs(o.TmplDir) { 61 | o.TmplDir = filepath.Join(filepath.Dir(execFile), o.TmplDir) 62 | } 63 | 64 | if err := models.LoadTemplate(o.TmplDir, o.TmplName, o.TmplDefault, o.TmplLang); err != nil { 65 | msg := fmt.Sprintf("Load templates from dir (%s) failed, err: %s", o.TmplDir, err) 66 | return errors.New(msg) 67 | } 68 | } 69 | 70 | fmt.Println("Signature: ", o.Signature) 71 | if o.Signature == "未知" { 72 | fmt.Println("Warn, you are using the default signature, we suggest to specify a custom signature by --signature option.") 73 | } 74 | 75 | httpProxy := os.Getenv("HTTP_PROXY") 76 | httpsProxy := os.Getenv("HTTPS_PROXY") 77 | noProxy := os.Getenv("NO_PROXY") 78 | if httpProxy != "" || httpsProxy != "" { 79 | fmt.Println("Found http proxy from environment variables:") 80 | fmt.Printf(" HTTP_PROXY: (%s)\n", httpProxy) 81 | fmt.Printf(" HTTPS_PROXY: (%s)\n", httpsProxy) 82 | fmt.Printf(" NO_PROXY: (%s)\n", noProxy) 83 | } 84 | 85 | container := restful.DefaultContainer 86 | 87 | controller := api.NewController(o.Signature) 88 | controller.WithDebug(o.Debug) 89 | 90 | controller.Install(container) 91 | 92 | s := &http.Server{ 93 | Addr: o.Addr, 94 | Handler: container, 95 | } 96 | log.Printf("start listening at %s", s.Addr) 97 | return s.ListenAndServe() 98 | } 99 | -------------------------------------------------------------------------------- /tests/alert-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dingtalk() { 4 | token="${DINGTALK_TOKEN}" 5 | channel_type="dingtalk" 6 | msg_type="markdown" 7 | payload=$(cat ./alert.json) 8 | 9 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&token=${token}&msg_type=${msg_type}" -d @- 10 | } 11 | 12 | feishu() { 13 | token="${FEISHU_TOKEN}" 14 | channel_type="feishu" 15 | msg_type="markdown" 16 | 17 | payload=$(cat ./alert.json) 18 | 19 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&token=${token}&msg_type=${msg_type}" -d @- 20 | 21 | # curl -X POST -H "Content-Type: application/json" -d '{"msg_type":"interactive","card":{"elements":[{"tag":"div","text":{"tag":"lark_md","content":"Hello"}}]}}' 22 | } 23 | 24 | slack() { 25 | token="${SLACK_APP_TOKEN}" 26 | channel_type="slack" 27 | msg_type="markdown" 28 | channel="jenkins-ci" 29 | 30 | payload=$(cat ./alert.json) 31 | 32 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&token=${token}&channel=${channel}" -d @- 33 | 34 | ## Invite the slack app to the channel, then the slack app can send messages to this channel. 35 | # /invite @BOT_NAME 36 | } 37 | 38 | weixin() { 39 | token="${WEIXIN_TOKEN}" 40 | channel_type="weixin" 41 | msg_type="markdown" 42 | 43 | payload=$(cat ./alert.json) 44 | 45 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&token=${token}&msg_type=${msg_type}" -d @- 46 | } 47 | 48 | weixinapp() { 49 | corpID="${WEIXIN_APP_CORP_ID}" 50 | agentID=${WEIXIN_APP_AGENT_ID} 51 | agentSecret="${WEIXIN_APP_SECRET}" 52 | 53 | toParty=2 54 | 55 | channel_type="weixinapp" 56 | msg_type="markdown" 57 | 58 | payload=$(cat ./alert.json) 59 | 60 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&msg_type=${msg_type}&corp_id=${corpID}&agent_id=${agentID}&agent_secret=${agentSecret}&to_party=${toParty}" -d @- 61 | } 62 | 63 | discord-webhook() { 64 | id="${DISCORD_WEBHOOK_ID}" 65 | token="${DISCORD_WEBHOOK_TOKEN}" 66 | 67 | channel_type="discord-webhook" 68 | msg_type="markdown" 69 | payload=$(cat ./alert.json) 70 | 71 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&msg_type=${msg_type}&id=${id}&token=${token}" -d @- 72 | } 73 | 74 | 75 | failed-test-1() { 76 | corpID="${WEIXIN_APP_CORP_ID}" 77 | agentID=${WEIXIN_APP_AGENT_ID} 78 | agentSecret="${WEIXIN_APP_SECRET}" 79 | 80 | toParty=2 81 | 82 | channel_type="notsupported" 83 | msg_type="markdown" 84 | 85 | payload=$(cat ./alert.json) 86 | 87 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&msg_type=${msg_type}&corp_id=${corpID}&agent_id=${agentID}&agent_secret=${agentSecret}&to_party=${toParty}" -d @- 88 | } 89 | 90 | weixin_fail_msg_type() { 91 | token="${WEIXIN_TOKEN}" 92 | channel_type="weixin" 93 | msg_type="type-not-exist" 94 | 95 | payload=$(cat ./alert.json) 96 | 97 | echo "$payload" | curl -s -H "Content-Type: application/json" -v -XPOST "http://127.0.0.1:8090/webhook/send?channel_type=${channel_type}&token=${token}&msg_type=${msg_type}" -d @- 98 | } 99 | -------------------------------------------------------------------------------- /pkg/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | promModels "github.com/bougou/alertmanager-webhook-adapter/pkg/models" 10 | "github.com/bougou/alertmanager-webhook-adapter/pkg/senders" 11 | restful "github.com/emicklei/go-restful/v3" 12 | "github.com/kr/pretty" 13 | ) 14 | 15 | type Controller struct { 16 | signature string 17 | debug bool 18 | } 19 | 20 | func NewController(signature string) *Controller { 21 | return &Controller{ 22 | signature: signature, 23 | } 24 | } 25 | 26 | func (c *Controller) WithDebug(debug bool) *Controller { 27 | if debug { 28 | fmt.Println("debug mode enabled") 29 | } 30 | c.debug = debug 31 | return c 32 | } 33 | 34 | func (c *Controller) Install(container *restful.Container) { 35 | 36 | ws := new(restful.WebService) 37 | ws.Path("/webhook/send") 38 | 39 | ws.Route( 40 | ws.POST("/").To(c.send), 41 | ) 42 | 43 | container.Add(ws) 44 | } 45 | 46 | func (c *Controller) logf(format string, a ...any) error { 47 | if c.debug { 48 | _, err := fmt.Printf(format, a...) 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (c *Controller) log(a ...any) error { 56 | if c.debug { 57 | _, err := fmt.Println(a...) 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (c *Controller) send(request *restful.Request, response *restful.Response) { 65 | c.logf("Got request : %s\n", request.Request.URL.String()) 66 | 67 | raw, err := io.ReadAll(request.Request.Body) 68 | if err != nil { 69 | errmsg := fmt.Sprintf("Err: read request body failed, err: %s", err) 70 | c.log(errmsg) 71 | response.WriteHeaderAndJson(http.StatusBadRequest, errmsg, restful.MIME_JSON) 72 | return 73 | } 74 | 75 | promMsg := &promModels.AlertmanagerWebhookMessage{} 76 | if err := json.Unmarshal(raw, promMsg); err != nil { 77 | errmsg := fmt.Sprintf("Err: unmarshal body failed, err: %s", err) 78 | c.log(errmsg) 79 | response.WriteHeaderAndJson(http.StatusBadRequest, errmsg, restful.MIME_JSON) 80 | return 81 | } 82 | promMsg.SetMessageAt().SetSignature(c.signature) 83 | 84 | channelType := request.QueryParameter("channel_type") 85 | if channelType == "" { 86 | errmsg := "Err: no channel_type found" 87 | c.log(errmsg) 88 | response.WriteHeaderAndJson(http.StatusBadRequest, errmsg, restful.MIME_JSON) 89 | return 90 | } 91 | 92 | senderCreator, exists := senders.ChannelsSenderCreatorMap[channelType] 93 | if !exists { 94 | errmsg := fmt.Sprintf("Err: not supported channel_type of (%s)", channelType) 95 | c.log(errmsg) 96 | response.WriteHeaderAndJson(http.StatusBadRequest, errmsg, restful.MIME_JSON) 97 | return 98 | } 99 | 100 | sender, err := senderCreator(request) 101 | if err != nil { 102 | errmsg := fmt.Sprintf("Err: create sender failed, %v", err) 103 | c.log(errmsg) 104 | response.WriteHeaderAndJson(http.StatusBadRequest, errmsg, restful.MIME_JSON) 105 | return 106 | } 107 | 108 | payload, err := promMsg.ToPayload(channelType, raw) 109 | if err != nil { 110 | errmsg := fmt.Sprintf("Err: create msg payload failed, %v", err) 111 | c.log(errmsg) 112 | response.WriteHeaderAndJson(http.StatusInternalServerError, errmsg, restful.MIME_JSON) 113 | return 114 | } 115 | if c.debug { 116 | pretty.Println(payload) 117 | 118 | fmt.Println(">>> Payload Markdown") 119 | fmt.Print(payload.Markdown) 120 | } 121 | 122 | if err := sender.Send(payload); err != nil { 123 | errmsg := fmt.Sprintf("Err: sender send failed, %v", err) 124 | c.log(errmsg) 125 | response.WriteHeaderAndJson(http.StatusInternalServerError, errmsg, restful.MIME_JSON) 126 | return 127 | } 128 | 129 | c.logf("Send succeed: %s\n", request.Request.URL.String()) 130 | response.WriteHeader(http.StatusNoContent) 131 | } 132 | -------------------------------------------------------------------------------- /pkg/models/templates/default.zh.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}告警中:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}已恢复:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end -}} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{ range . }} 39 | > 告警名称: {{ if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 40 | > 41 | > Severity:{{ ` ` }} 42 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 43 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 44 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 45 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 46 | > 47 | > 告警实例: `{{ template "__alertinstance" . }}` 48 | > 49 | {{- if .Labels.region }} 50 | > 地域: {{ .Labels.region }} 51 | > 52 | {{- end }} 53 | {{- if .Labels.zone }} 54 | > 可用区: {{ .Labels.zone }} 55 | > 56 | {{- end }} 57 | {{- if .Labels.product }} 58 | > 产品: {{ .Labels.product }} 59 | > 60 | {{- end }} 61 | {{- if .Labels.component }} 62 | > 组件: {{ .Labels.component }} 63 | > 64 | {{- end }} 65 | > 告警状态: {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }}{{ .Status | toUpper }} 66 | > 67 | > 开始时间: {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 68 | > 69 | > 结束时间: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 70 | {{- if eq .Status "firing" }} 71 | > 72 | > 告警描述: {{ .Annotations.description }} 73 | {{- end }} 74 | 75 | {{ end }} 76 | {{ end }} 77 | 78 | {{ define "__alert_summary" -}} 79 | {{ range . }} 80 | {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }}{{ template "__alertinstance" . }} 81 | {{ end }} 82 | {{ end }} 83 | 84 | {{ define "prom.title" -}} 85 | {{ template "__subject" . }} 86 | {{ end }} 87 | 88 | 89 | {{ define "prom.markdown" }} 90 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 91 | #### 摘要 92 | 93 | {{ if gt (.Alerts.Firing|len ) 0 }} 94 | ##### 🚨 Firing {{ .Alerts.Firing|len }} alerts 95 | {{ template "__alert_summary" .Alerts.Firing }} 96 | {{ end }} 97 | 98 | 99 | {{ if gt (.Alerts.Resolved|len) 0 }} 100 | ##### ✅ Resolved {{ .Alerts.Resolved|len }} alerts 101 | {{ template "__alert_summary" .Alerts.Resolved }} 102 | {{ end }} 103 | 104 | #### 详情 105 | 106 | {{ if gt (.Alerts.Firing|len ) 0 }} 107 | ##### 🚨 Firing {{ .Alerts.Firing|len }} alerts 108 | {{ template "__alert_list" .Alerts.Firing }} 109 | {{ end }} 110 | 111 | {{ if gt (.Alerts.Resolved|len) 0 }} 112 | ##### ✅ Resolved {{ .Alerts.Resolved|len }} alerts 113 | {{ template "__alert_list" .Alerts.Resolved }} 114 | {{ end }} 115 | 116 | {{ end }} 117 | 118 | {{ define "prom.text" }} 119 | {{ template "prom.markdown" . }} 120 | {{ end }} 121 | -------------------------------------------------------------------------------- /pkg/models/templates/slack.zh.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}告警中:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}已恢复:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end -}} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{ range . }} 39 | > 告警名称: {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 40 | > 告警级别:{{ ` ` }} 41 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 42 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 43 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 44 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 45 | > 实例: `{{ template "__alertinstance" . }}` 46 | {{- if .Labels.region }} 47 | > 地域: {{ .Labels.region }} 48 | {{- end }} 49 | {{- if .Labels.zone }} 50 | > 可用区: {{ .Labels.zone }} 51 | {{- end }} 52 | {{- if .Labels.product }} 53 | > 产品: {{ .Labels.product }} 54 | {{- end }} 55 | {{- if .Labels.component }} 56 | > 组件: {{ .Labels.component }} 57 | {{- end }} 58 | > 告警状态: {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 59 | > 开始时间: {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 60 | > 结束时间: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 61 | {{- if eq .Status "firing" }} 62 | > 告警描述: {{ if .Annotations.description_cn }}{{ .Annotations.description_cn }}{{ else }}{{ .Annotations.description }}{{ end }} 63 | {{- end }} 64 | {{ end }} 65 | {{- end }} 66 | 67 | 68 | {{ define "__alert_summary" -}} 69 | {{ range . -}} 70 | • {{ template "__alertinstance" . }} 71 | {{ end }} 72 | {{ end }} 73 | 74 | {{ define "prom.title" -}} 75 | {{ template "__subject" . }} 76 | {{ end }} 77 | 78 | {{ define "prom.markdown" }} 79 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 80 | 81 | *摘要* 82 | 83 | {{ if gt (.Alerts.Firing|len ) 0 -}} 84 | 🚨 触发中告警 [{{ .Alerts.Firing|len }}] 85 | 86 | {{ template "__alert_summary" .Alerts.Firing }} 87 | {{- end -}} 88 | 89 | {{ if gt (.Alerts.Resolved|len) 0 -}} 90 | ✅ 已恢复告警 [{{ .Alerts.Resolved|len }}] 91 | 92 | {{ template "__alert_summary" .Alerts.Resolved }} 93 | {{- end -}} 94 | 95 | *详请* 96 | 97 | {{ if gt (.Alerts.Firing|len ) 0 -}} 98 | 🚨 触发中告警 [{{ .Alerts.Firing|len }}] 99 | 100 | {{- template "__alert_list" .Alerts.Firing }} 101 | {{- end -}} 102 | 103 | {{ if gt (.Alerts.Resolved|len) 0 }} 104 | ✅ 已恢复告警 [{{ .Alerts.Resolved|len }}] 105 | 106 | {{- template "__alert_list" .Alerts.Resolved }} 107 | {{- end -}} 108 | {{ end }} 109 | 110 | {{ define "prom.text" }} 111 | {{ template "prom.markdown" . }} 112 | {{ end }} 113 | -------------------------------------------------------------------------------- /pkg/models/templates/default.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}Firing:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}Resolved:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end -}} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{ range . }} 39 | > Alert Name: {{ if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 40 | > 41 | > Severity:{{ ` ` }} 42 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 43 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 44 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 45 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 46 | > 47 | > Instance: `{{ template "__alertinstance" . }}` 48 | > 49 | {{- if .Labels.region }} 50 | > Region: {{ .Labels.region }} 51 | > 52 | {{- end }} 53 | {{- if .Labels.zone }} 54 | > Zone: {{ .Labels.zone }} 55 | > 56 | {{- end }} 57 | {{- if .Labels.product }} 58 | > Product: {{ .Labels.product }} 59 | > 60 | {{- end }} 61 | {{- if .Labels.component }} 62 | > Component: {{ .Labels.component }} 63 | > 64 | {{- end }} 65 | > Status: {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }}"> {{ .Status | toUpper }} 66 | > 67 | > Starts At: {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 68 | > 69 | > Ends At: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 70 | {{- if eq .Status "firing" }} 71 | > 72 | > Description : {{ .Annotations.description }} 73 | {{- end }} 74 | 75 | {{ end }} 76 | {{ end }} 77 | 78 | {{ define "__alert_summary" -}} 79 | {{ range . }} 80 | {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }}"> {{ template "__alertinstance" . }} 81 | {{ end }} 82 | {{ end }} 83 | 84 | {{ define "prom.title" -}} 85 | {{ template "__subject" . }} 86 | {{ end }} 87 | 88 | 89 | {{ define "prom.markdown" }} 90 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 91 | #### Summary 92 | 93 | {{ if gt (.Alerts.Firing|len ) 0 }} 94 | ##### 🚨 Firing {{ .Alerts.Firing|len }} alerts 95 | {{ template "__alert_summary" .Alerts.Firing }} 96 | {{ end }} 97 | 98 | 99 | {{ if gt (.Alerts.Resolved|len) 0 }} 100 | ##### ✅ Resolved {{ .Alerts.Resolved|len }} alerts 101 | {{ template "__alert_summary" .Alerts.Resolved }} 102 | {{ end }} 103 | 104 | #### Detail 105 | 106 | {{ if gt (.Alerts.Firing|len ) 0 }} 107 | ##### 🚨 Firing {{ .Alerts.Firing|len }} alerts 108 | {{ template "__alert_list" .Alerts.Firing }} 109 | {{ end }} 110 | 111 | {{ if gt (.Alerts.Resolved|len) 0 }} 112 | ##### ✅ Resolved {{ .Alerts.Resolved|len }} alerts 113 | {{ template "__alert_list" .Alerts.Resolved }} 114 | {{ end }} 115 | 116 | {{ end }} 117 | 118 | {{ define "prom.text" }} 119 | {{ template "prom.markdown" . }} 120 | {{ end }} 121 | -------------------------------------------------------------------------------- /pkg/models/templates/discord-webhook.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}Firing:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}Resolved:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{ if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end }} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{- range . }} 39 | 40 | > Alert Name: {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 41 | > Alert Level:{{ ` ` }} 42 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 43 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 44 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 45 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 46 | > Instance: `{{ template "__alertinstance" . }}` 47 | {{- if .Labels.region }} 48 | > Region: {{ .Labels.region }} 49 | {{- end }} 50 | {{- if .Labels.zone }} 51 | > Zone: {{ .Labels.zone }} 52 | {{- end }} 53 | {{- if .Labels.product }} 54 | > Product: {{ .Labels.product }} 55 | {{- end }} 56 | {{- if .Labels.component }} 57 | > Component: {{ .Labels.component }} 58 | {{- end }} 59 | > Alert Status: {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 60 | > Start At: {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 61 | > End At: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 62 | {{- if eq .Status "firing" }} 63 | > Description: {{ if .Annotations.description_en }}{{ .Annotations.description_en }}{{ else }}{{ .Annotations.description }}{{ end }} 64 | {{- end }} 65 | {{- end }} 66 | {{ end }} 67 | 68 | 69 | {{ define "__alert_summary" -}} 70 | {{- range . }} 71 | - {{ template "__alertinstance" . }} 72 | {{- end }} 73 | {{ end }} 74 | 75 | {{ define "prom.title" -}} 76 | {{ template "__subject" . }} 77 | {{ end }} 78 | 79 | 80 | {{ define "prom.markdown" }} 81 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 82 | ## Summary 83 | 84 | {{ if gt (.Alerts.Firing|len ) 0 }} 85 | ### 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 86 | {{ template "__alert_summary" .Alerts.Firing }} 87 | {{ end -}} 88 | 89 | {{ if gt (.Alerts.Resolved|len) 0 -}} 90 | ### ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 91 | {{ template "__alert_summary" .Alerts.Resolved }} 92 | {{- end }} 93 | ## Detail 94 | 95 | {{ if gt (.Alerts.Firing|len ) 0 -}} 96 | ### 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 97 | {{ template "__alert_list" .Alerts.Firing }} 98 | {{ end -}} 99 | 100 | {{- if gt (.Alerts.Resolved|len) 0 -}} 101 | ### ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 102 | {{ template "__alert_list" .Alerts.Resolved }} 103 | {{ end }} 104 | {{ end }} 105 | 106 | {{ define "prom.text" }} 107 | {{ template "prom.markdown" . }} 108 | {{ end }} 109 | -------------------------------------------------------------------------------- /pkg/models/templates/dingtalk.zh.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 4 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 7 | {{- ` • ` }} 8 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 9 | {{- ` • ` }} 10 | {{- if gt (.Alerts.Firing|len) 0 }}告警中:{{ .Alerts.Firing|len }}{{ end }} 11 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 12 | {{- if gt (.Alerts.Resolved|len) 0 }}已恢复:{{ .Alerts.Resolved|len }}{{ end }} 13 | {{ end }} 14 | 15 | 16 | {{ define "__externalURL" -}} 17 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 18 | {{- end }} 19 | 20 | {{ define "__alertinstance" -}} 21 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 22 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 23 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 24 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 25 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 26 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 27 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 28 | {{- end -}} 29 | {{- end }} 30 | 31 | {{ define "__alert_list" }} 32 | {{ range . }} 33 | --- 34 | > **告警名称**: {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 35 | > 36 | > **告警级别**: {{ ` ` }} 37 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 38 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 39 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 40 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 41 | > 42 | > **告警实例**: `{{ template "__alertinstance" . }}` 43 | > 44 | {{- if .Labels.region }} 45 | > **地域**: {{ .Labels.region }} 46 | > 47 | {{- end }} 48 | {{- if .Labels.zone }} 49 | > **可用区**: {{ .Labels.zone }} 50 | > 51 | {{- end }} 52 | {{- if .Labels.product }} 53 | > **产品**: {{ .Labels.product }} 54 | > 55 | {{- end }} 56 | {{- if .Labels.component }} 57 | > **组件**: {{ .Labels.component }} 58 | > 59 | {{- end }} 60 | > **告警状态**: {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 61 | > 62 | > **开始时间**: {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 63 | > 64 | > **结束时间**: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 65 | > 66 | {{- if eq .Status "firing" }} 67 | > 告警描述: {{ if .Annotations.description_cn }}{{ .Annotations.description_cn }}{{ else }}{{ .Annotations.description }}{{ end }} 68 | > 69 | {{- end }} 70 | {{ end }} 71 | {{ end }} 72 | 73 | 74 | {{ define "__alert_summary" }} 75 | {{ range . }}- {{ template "__alertinstance" . }} 76 | {{ end }} 77 | {{ end }} 78 | 79 | {{ define "prom.title" }} 80 | {{ template "__subject" . }} 81 | {{ end }} 82 | 83 | 84 | {{ define "prom.markdown" }} 85 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 86 | #### **摘要** 87 | 88 | {{ if gt (.Alerts.Firing|len ) 0 }} 89 | ##### **🚨 触发中告警 [{{ .Alerts.Firing|len }}]** 90 | {{ template "__alert_summary" .Alerts.Firing }} 91 | {{ end }} 92 | 93 | 94 | {{ if gt (.Alerts.Resolved|len) 0 }} 95 | ##### **✅ 已恢复告警 [{{ .Alerts.Resolved|len }}]** 96 | {{ template "__alert_summary" .Alerts.Resolved }} 97 | {{ end }} 98 | 99 | #### **详请** 100 | 101 | {{ if gt (.Alerts.Firing|len ) 0 }} 102 | ##### **🚨 触发中告警 [{{ .Alerts.Firing|len }}]** 103 | {{ template "__alert_list" .Alerts.Firing }} 104 | {{ end }} 105 | 106 | 107 | {{ if gt (.Alerts.Resolved|len) 0 }} 108 | ##### **✅ 已恢复告警 [{{ .Alerts.Resolved|len }}]** 109 | {{ template "__alert_list" .Alerts.Resolved }} 110 | {{ end }} 111 | {{ end }} 112 | 113 | {{ define "prom.text" }} 114 | {{ template "prom.markdown" . }} 115 | {{ end }} 116 | -------------------------------------------------------------------------------- /pkg/models/templates/slack.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}Firing:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}Resolved:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end -}} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{ range . }} 39 | > Alert Name: {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 40 | > Alert Level:{{ ` ` }} 41 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 42 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 43 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 44 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 45 | > Instance: `{{ template "__alertinstance" . }}` 46 | {{- if .Labels.region }} 47 | > Region: {{ .Labels.region }} 48 | {{- end }} 49 | {{- if .Labels.zone }} 50 | > Zone: {{ .Labels.zone }} 51 | {{- end }} 52 | {{- if .Labels.product }} 53 | > Product: {{ .Labels.product }} 54 | {{- end }} 55 | {{- if .Labels.component }} 56 | > Component: {{ .Labels.component }} 57 | {{- end }} 58 | > Alert Status: {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 59 | > Start At: {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 60 | > End At: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 61 | {{- if eq .Status "firing" }} 62 | > Description: {{ if .Annotations.description_en }}{{ .Annotations.description_en }}{{ else }}{{ .Annotations.description }}{{ end }} 63 | {{- end }} 64 | {{ end }} 65 | {{- end }} 66 | 67 | 68 | {{ define "__alert_summary" -}} 69 | {{ range . -}} 70 | • {{ template "__alertinstance" . }} 71 | {{ end }} 72 | {{ end }} 73 | 74 | {{ define "prom.title" -}} 75 | {{ template "__subject" . }} 76 | {{ end }} 77 | 78 | 79 | {{ define "prom.markdown" }} 80 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 81 | 82 | *Summary* 83 | 84 | {{ if gt (.Alerts.Firing|len ) 0 -}} 85 | 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 86 | 87 | {{ template "__alert_summary" .Alerts.Firing }} 88 | {{- end -}} 89 | 90 | {{ if gt (.Alerts.Resolved|len) 0 -}} 91 | ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 92 | 93 | {{ template "__alert_summary" .Alerts.Resolved }} 94 | {{- end -}} 95 | 96 | *Detail* 97 | 98 | {{ if gt (.Alerts.Firing|len ) 0 -}} 99 | 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 100 | 101 | {{- template "__alert_list" .Alerts.Firing }} 102 | {{- end -}} 103 | 104 | {{ if gt (.Alerts.Resolved|len) 0 }} 105 | ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 106 | 107 | {{- template "__alert_list" .Alerts.Resolved }} 108 | {{- end -}} 109 | {{ end }} 110 | 111 | {{ define "prom.text" }} 112 | {{ template "prom.markdown" . }} 113 | {{ end }} 114 | -------------------------------------------------------------------------------- /pkg/models/templates/dingtalk.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 4 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 7 | {{- ` • ` }} 8 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 9 | {{- ` • ` }} 10 | {{- if gt (.Alerts.Firing|len) 0 }}Firing:{{ .Alerts.Firing|len }}{{ end }} 11 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 12 | {{- if gt (.Alerts.Resolved|len) 0 }}Resolved:{{ .Alerts.Resolved|len }}{{ end }} 13 | {{ end }} 14 | 15 | 16 | {{ define "__externalURL" -}} 17 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 18 | {{- end }} 19 | 20 | {{ define "__alertinstance" -}} 21 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 22 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 23 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 24 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 25 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 26 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 27 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 28 | {{- end -}} 29 | {{- end }} 30 | 31 | {{ define "__alert_list" }} 32 | {{ range . }} 33 | --- 34 | > **Alert Name**: {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 35 | > 36 | > **Alert Level**: {{ ` ` }} 37 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 38 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 39 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 40 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 41 | > 42 | > **Instance**: `{{ template "__alertinstance" . }}` 43 | > 44 | {{- if .Labels.region }} 45 | > **Region**: {{ .Labels.region }} 46 | > 47 | {{- end }} 48 | {{- if .Labels.zone }} 49 | > **Zone**: {{ .Labels.zone }} 50 | > 51 | {{- end }} 52 | {{- if .Labels.product }} 53 | > **Product**: {{ .Labels.product }} 54 | > 55 | {{- end }} 56 | {{- if .Labels.component }} 57 | > **Component**: {{ .Labels.component }} 58 | > 59 | {{- end }} 60 | > **Alert Status**: {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 61 | > 62 | > **Start At**: {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 63 | > 64 | > **End At**: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 65 | > 66 | {{- if eq .Status "firing" }} 67 | > **Description**: {{ if .Annotations.description_en }}{{ .Annotations.description_en }}{{ else }}{{ .Annotations.description }}{{ end }} 68 | > 69 | {{- end }} 70 | {{ end }} 71 | {{ end }} 72 | 73 | 74 | {{ define "__alert_summary" }} 75 | {{ range . }}- {{ template "__alertinstance" . }} 76 | {{ end }} 77 | {{ end }} 78 | 79 | {{ define "prom.title" }} 80 | {{ template "__subject" . }} 81 | {{ end }} 82 | 83 | 84 | {{ define "prom.markdown" }} 85 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 86 | #### **Summary** 87 | 88 | {{ if gt (.Alerts.Firing|len ) 0 }} 89 | ##### **🚨 Firing [{{ .Alerts.Firing|len }}] alerts** 90 | {{ template "__alert_summary" .Alerts.Firing }} 91 | {{ end }} 92 | 93 | 94 | {{ if gt (.Alerts.Resolved|len) 0 }} 95 | ##### **✅ Resolved [{{ .Alerts.Resolved|len }}] alerts** 96 | {{ template "__alert_summary" .Alerts.Resolved }} 97 | {{ end }} 98 | 99 | #### **Detail** 100 | 101 | {{ if gt (.Alerts.Firing|len ) 0 }} 102 | ##### **🚨 Firing [{{ .Alerts.Firing|len }}] alerts** 103 | {{ template "__alert_list" .Alerts.Firing }} 104 | {{ end }} 105 | 106 | 107 | {{ if gt (.Alerts.Resolved|len) 0 }} 108 | ##### **✅ Resolved [{{ .Alerts.Resolved|len }}] alerts** 109 | {{ template "__alert_list" .Alerts.Resolved }} 110 | {{ end }} 111 | {{ end }} 112 | 113 | {{ define "prom.text" }} 114 | {{ template "prom.markdown" . }} 115 | {{ end }} 116 | -------------------------------------------------------------------------------- /tests/alert.json: -------------------------------------------------------------------------------- 1 | { 2 | "receiver": "devops_team", 3 | "status": "firing", 4 | "alerts": [ 5 | { 6 | "status": "firing", 7 | "labels": { 8 | "alertgroup": "Linux", 9 | "alertname": "CPUHigh", 10 | "component": "component-x", 11 | "cpu": "cpu-total", 12 | "db": "telegraf", 13 | "description": "cpu usage is high than 20% for 5 minutes", 14 | "host": "10.30.1.160", 15 | "instance": "10.30.1.160", 16 | "product": "product-x", 17 | "region": "region-x", 18 | "severity": "warning", 19 | "vmalert": "10.30.12.22", 20 | "zone": "zone-x" 21 | }, 22 | "annotations": { 23 | "description": "cpu usage is high than 20% for 5 minutes" 24 | }, 25 | "startsAt": "2021-03-30T20:17:50.814674209+08:00", 26 | "endsAt": "0001-01-01T00:00:00Z", 27 | "generatorURL": "http://10.30.12.22:8429/api/v1/2026138838271548684/17025577897922373885/status", 28 | "fingerprint": "83eb2c81e0da3faf" 29 | }, 30 | { 31 | "status": "resolved", 32 | "labels": { 33 | "alertgroup": "Linux", 34 | "alertname": "CPUHigh", 35 | "component": "component-y", 36 | "cpu": "cpu-total", 37 | "db": "telegraf", 38 | "description": "cpu usage is high than 20% for 5 minutes", 39 | "host": "10.30.1.161", 40 | "instance": "10.30.1.161", 41 | "product": "product-y", 42 | "region": "region-y", 43 | "severity": "warning", 44 | "vmalert": "10.30.12.22", 45 | "zone": "zone-y" 46 | }, 47 | "annotations": { 48 | "description": "cpu usage is high than 20% for 5 minutes" 49 | }, 50 | "startsAt": "2021-03-30T20:17:50.814674209+08:00", 51 | "endsAt": "2021-03-30T21:17:50.814674209+08:00", 52 | "generatorURL": "http://10.30.12.22:8429/api/v1/2026138838271548684/17025577897922373885/status", 53 | "fingerprint": "83eb2c81e0da3faf" 54 | }, 55 | { 56 | "status": "firing", 57 | "labels": { 58 | "alertgroup": "Linux", 59 | "alertname": "CPUHigh1", 60 | "component": "component-x", 61 | "cpu": "cpu-total", 62 | "db": "telegraf", 63 | "description": "cpu usage is high than 20% for 5 minutes", 64 | "host": "10.30.1.162", 65 | "instance": "10.30.1.162", 66 | "product": "product-x", 67 | "region": "region-x", 68 | "severity": "critical", 69 | "vmalert": "10.30.12.22", 70 | "zone": "zone-x" 71 | }, 72 | "annotations": { 73 | "description": "cpu usage is high than 20% for 5 minutes" 74 | }, 75 | "startsAt": "2021-03-30T20:17:50.814674209+08:00", 76 | "endsAt": "0001-01-01T00:00:00Z", 77 | "generatorURL": "http://10.30.12.22:8429/api/v1/2026138838271548684/17025577897922373885/status", 78 | "fingerprint": "83eb2c81e0da3faf" 79 | }, 80 | { 81 | "status": "firing", 82 | "labels": { 83 | "alertgroup": "Linux", 84 | "alertname": "CPUHigh2", 85 | "component": "component-x", 86 | "cpu": "cpu-total", 87 | "db": "telegraf", 88 | "description": "cpu usage is high than 20% for 5 minutes", 89 | "host": "10.30.1.163", 90 | "instance": "10.30.1.163", 91 | "product": "product-x", 92 | "region": "region-x", 93 | "severity": "info", 94 | "vmalert": "10.30.12.22", 95 | "zone": "zone-x" 96 | }, 97 | "annotations": { 98 | "description": "cpu usage is high than 20% for 5 minutes" 99 | }, 100 | "startsAt": "2021-03-30T20:17:50.814674209+08:00", 101 | "endsAt": "0001-01-01T00:00:00Z", 102 | "generatorURL": "http://10.30.12.22:8429/api/v1/2026138838271548684/17025577897922373885/status", 103 | "fingerprint": "83eb2c81e0da3faf" 104 | } 105 | ], 106 | "groupLabels": { 107 | "alertname": "CPUHigh" 108 | }, 109 | "commonLabels": { 110 | "alertgroup": "Linux", 111 | "alertname": "CPUHigh", 112 | "component": "component-x", 113 | "cpu": "cpu-total", 114 | "db": "telegraf", 115 | "description": "cpu usage is high than 20% for 5 minutes", 116 | "host": "10.30.1.160", 117 | "product": "product-x", 118 | "region": "region-x", 119 | "severity": "warning", 120 | "vmalert": "10.30.12.22", 121 | "zone": "zone-x" 122 | }, 123 | "commonAnnotations": {}, 124 | "externalURL": "http://10.30.12.22:9193", 125 | "version": "4", 126 | "groupKey": "{}:{alertname=\"CPUHigh\"}", 127 | "truncatedAlerts": 0 128 | } 129 | -------------------------------------------------------------------------------- /pkg/models/templates/feishu.zh.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 4 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 7 | {{- ` • ` }} 8 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 9 | {{- ` • ` }} 10 | {{- if gt (.Alerts.Firing|len) 0 }}告警中:{{ .Alerts.Firing|len }}{{ end }} 11 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 12 | {{- if gt (.Alerts.Resolved|len) 0 }}已恢复:{{ .Alerts.Resolved|len }}{{ end }} 13 | {{ end }} 14 | 15 | 16 | {{ define "__externalURL" -}} 17 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 18 | {{- end }} 19 | 20 | {{ define "__alertinstance" -}} 21 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 22 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 23 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 24 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 25 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 26 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 27 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 28 | {{- end -}} 29 | {{- end }} 30 | 31 | {{ define "__alert_list" }} 32 | {{ $timeFormat:="2006-01-02 15:04:05" }} 33 | {{ range . }} 34 | - 告警名称: {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 35 | 告警级别:{{ ` ` }} 36 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 37 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 38 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 39 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 40 | 实例: {{ template "__alertinstance" . }} 41 | {{- if .Labels.region }} 42 | 地域: {{ .Labels.region }} 43 | {{- end }} 44 | {{- if .Labels.zone }} 45 | 可用区: {{ .Labels.zone }} 46 | {{- end }} 47 | {{- if .Labels.product }} 48 | 产品: {{ .Labels.product }} 49 | {{- end }} 50 | {{- if .Labels.component }} 51 | 组件: {{ .Labels.component }} 52 | {{- end }} 53 | 告警状态: {{ if eq .Status "firing" }}🚨 {{ .Status | toUpper }}{{ else }}✅ {{ .Status | toUpper }}{{ end }} 54 | 开始: {{ .StartsAt.Format $timeFormat }} 55 | 结束: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format $timeFormat }}{{ else }}Not End{{ end }} 56 | {{- if eq .Status "firing" }} 57 | 告警描述: {{ if .Annotations.description_cn }}{{ .Annotations.description_cn }}{{ else }}{{ .Annotations.description }}{{ end }} 58 | {{ end }} 59 | {{ end }}{{ end }} 60 | 61 | {{ define "__alert_summary" }} 62 | {{ range . -}} 63 | - {{ if eq .Status "firing" -}} 64 | {{ template "__alertinstance" . }} 65 | {{- else -}} 66 | {{ template "__alertinstance" . }} 67 | {{- end }} 68 | {{ end }} 69 | {{- end }} 70 | 71 | {{ define "prom.title" -}} 72 | {{ template "__subject" . -}}{{ end }} 73 | 74 | {{ define "prom.markdown" }} 75 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 76 | --- 77 | **摘要** 78 | 79 | {{ if gt (.Alerts.Firing|len ) 0 -}} 80 | **🚨 触发中告警 [{{ .Alerts.Firing|len }}]** 81 | 82 | {{ template "__alert_summary" .Alerts.Firing }}{{ end }} 83 | 84 | {{ if gt (.Alerts.Resolved|len) 0 -}} 85 | **✅ 已恢复告警 [{{ .Alerts.Resolved|len }}]** 86 | 87 | {{ template "__alert_summary" .Alerts.Resolved }}{{ end }} 88 | 89 | --- 90 | **详请** 91 | 92 | {{ if gt (.Alerts.Firing|len ) 0 -}} 93 | **🚨 触发中告警 [{{ .Alerts.Firing|len }}]** 94 | {{ template "__alert_list" .Alerts.Firing }}{{ end }} 95 | 96 | {{ if gt (.Alerts.Resolved|len) 0 -}} 97 | **✅ 已恢复告警 [{{ .Alerts.Resolved|len }}]** 98 | {{ template "__alert_list" .Alerts.Resolved }}{{ end }} 99 | {{ end }} 100 | 101 | {{ define "prom.text" }} 102 | {{ template "prom.markdown" . }} 103 | {{ end }} 104 | -------------------------------------------------------------------------------- /pkg/models/templates/weixinapp.zh.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}告警中:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}已恢复:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end -}} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{ range . }} 39 | ⁣ 40 | 41 | > 告警名称 : {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 42 | 告警级别 :{{ ` ` }} 43 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 44 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 45 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 46 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 47 | 告警实例 : `{{ template "__alertinstance" . }}` 48 | {{- if .Labels.region }} 49 | 地域 : {{ .Labels.region }} 50 | {{- end }} 51 | {{- if .Labels.zone }} 52 | 可用区 : {{ .Labels.zone }} 53 | {{- end }} 54 | {{- if .Labels.product }} 55 | 产品 : {{ .Labels.product }} 56 | {{- end }} 57 | {{- if .Labels.component }} 58 | 组件 : {{ .Labels.component }} 59 | {{- end }} 60 | 告警状态 : {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 61 | 开始时间 : {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 62 | 结束时间 : {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 63 | {{- if eq .Status "firing" }} 64 | 告警描述 : {{ if .Annotations.description_cn }}{{ .Annotations.description_cn }}{{ else }}{{ .Annotations.description }}{{ end }} 65 | {{- end }} 66 | 67 | {{ end }} 68 | {{ end }} 69 | 70 | {{ define "__alert_summary" -}} 71 | {{ range . }} 72 | {{ template "__alertinstance" . }} 73 | {{- end }} 74 | {{- end }} 75 | 76 | {{ define "prom.title" -}} 77 | {{ template "__subject" . }} 78 | {{ end }} 79 | 80 | 81 | {{ define "prom.markdown" }} 82 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 83 | ### 告警摘要 84 | 85 | {{ if gt (.Alerts.Firing|len ) 0 }} 86 | #### 🚨 触发中告警 [{{ .Alerts.Firing|len }}] 87 | {{ template "__alert_summary" .Alerts.Firing }} 88 | {{ end }} 89 | 90 | 91 | {{ if gt (.Alerts.Resolved|len) 0 }} 92 | #### ✅ 已恢复告警 [{{ .Alerts.Resolved|len }}] 93 | {{ template "__alert_summary" .Alerts.Resolved }} 94 | {{ end }} 95 | 96 | ### 告警详请 97 | 98 | {{ if gt (.Alerts.Firing|len ) 0 }} 99 | #### 🚨 触发中告警 [{{ .Alerts.Firing|len }}] 100 | {{ template "__alert_list" .Alerts.Firing }} 101 | {{ end }} 102 | 103 | 104 | {{ if gt (.Alerts.Resolved|len) 0 }} 105 | ##### ✅ 已恢复告警 [{{ .Alerts.Resolved|len }}] 106 | {{ template "__alert_list" .Alerts.Resolved }} 107 | {{ end }} 108 | {{ end }} 109 | 110 | {{ define "prom.text" }} 111 | {{ template "prom.markdown" . }} 112 | {{ end }} 113 | -------------------------------------------------------------------------------- /pkg/models/templates/weixin.zh.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}告警中:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}已恢复:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end -}} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{ range . }} 39 | > 告警名称 : {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 40 | > 41 | > 告警级别 :{{ ` ` }} 42 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 43 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 44 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 45 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 46 | > 47 | > 实例 : `{{ template "__alertinstance" . }}` 48 | > 49 | {{- if .Labels.region }} 50 | > 地域 : {{ .Labels.region }} 51 | > 52 | {{- end }} 53 | {{- if .Labels.zone }} 54 | > 可用区 : {{ .Labels.zone }} 55 | > 56 | {{- end }} 57 | {{- if .Labels.product }} 58 | > 产品 : {{ .Labels.product }} 59 | > 60 | {{- end }} 61 | {{- if .Labels.component }} 62 | > 组件 : {{ .Labels.component }} 63 | > 64 | {{- end }} 65 | > 告警状态 : {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 66 | > 67 | > 开始时间 : {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 68 | > 69 | > 结束时间 : {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 70 | {{- if eq .Status "firing" }} 71 | > 72 | > 告警描述 : {{ if .Annotations.description_cn }}{{ .Annotations.description_cn }}{{ else }}{{ .Annotations.description }}{{ end }} 73 | {{- end }} 74 | 75 | {{ end }} 76 | {{ end }} 77 | 78 | 79 | {{ define "__alert_summary" -}} 80 | {{ range . }} 81 | {{ template "__alertinstance" . }} 82 | {{ end }} 83 | {{ end }} 84 | 85 | {{ define "prom.title" -}} 86 | {{ template "__subject" . }} 87 | {{ end }} 88 | 89 | 90 | {{ define "prom.markdown" }} 91 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 92 | #### 摘要 93 | 94 | {{ if gt (.Alerts.Firing|len ) 0 }} 95 | ##### 🚨 触发中告警 [{{ .Alerts.Firing|len }}] 96 | {{ template "__alert_summary" .Alerts.Firing }} 97 | {{ end }} 98 | 99 | 100 | {{ if gt (.Alerts.Resolved|len) 0 }} 101 | ##### ✅ 已恢复告警 [{{ .Alerts.Resolved|len }}] 102 | {{ template "__alert_summary" .Alerts.Resolved }} 103 | {{ end }} 104 | 105 | #### 详请 106 | 107 | {{ if gt (.Alerts.Firing|len ) 0 }} 108 | ##### 🚨 触发中告警 [{{ .Alerts.Firing|len }}] 109 | {{ template "__alert_list" .Alerts.Firing }} 110 | {{ end }} 111 | 112 | 113 | {{ if gt (.Alerts.Resolved|len) 0 }} 114 | ##### ✅ 已恢复告警 [{{ .Alerts.Resolved|len }}] 115 | {{ template "__alert_list" .Alerts.Resolved }} 116 | {{ end }} 117 | {{ end }} 118 | 119 | {{ define "prom.text" }} 120 | {{ template "prom.markdown" . }} 121 | {{ end }} 122 | -------------------------------------------------------------------------------- /pkg/models/templates/feishu.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 4 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 7 | {{- ` • ` }} 8 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 9 | {{- ` • ` }} 10 | {{- if gt (.Alerts.Firing|len) 0 }}Firing:{{ .Alerts.Firing|len }}{{ end }} 11 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 12 | {{- if gt (.Alerts.Resolved|len) 0 }}Resolved:{{ .Alerts.Resolved|len }}{{ end }} 13 | {{ end }} 14 | 15 | 16 | {{ define "__externalURL" -}} 17 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 18 | {{- end }} 19 | 20 | {{ define "__alertinstance" -}} 21 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 22 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 23 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 24 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 25 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 26 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 27 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 28 | {{- end -}} 29 | {{- end }} 30 | 31 | {{ define "__alert_list" }} 32 | {{ $timeFormat:="2006-01-02 15:04:05" }} 33 | {{ range . }} 34 | - **Alert Name**: {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 35 | **Alert Level**:{{ ` ` }} 36 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 37 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 38 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 39 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 40 | **Instance**: {{ template "__alertinstance" . }} 41 | {{- if .Labels.region }} 42 | **Region**: {{ .Labels.region }} 43 | {{- end }} 44 | {{- if .Labels.zone }} 45 | **Zone**: {{ .Labels.zone }} 46 | {{- end }} 47 | {{- if .Labels.product }} 48 | **Product**: {{ .Labels.product }} 49 | {{- end }} 50 | {{- if .Labels.component }} 51 | **Component**: {{ .Labels.component }} 52 | {{- end }} 53 | **Alert Status**: {{ if eq .Status "firing" }}🚨 {{ .Status | toUpper }}{{ else }}✅ {{ .Status | toUpper }}{{ end }} 54 | **Start At**: {{ .StartsAt.Format $timeFormat }} 55 | **End At**: {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format $timeFormat }}{{ else }}Not End{{ end }} 56 | {{- if eq .Status "firing" }} 57 | **Description**: {{ if .Annotations.description_en }}{{ .Annotations.description_en }}{{ else }}{{ .Annotations.description }}{{ end }} 58 | {{ end }} 59 | {{ end }}{{ end }} 60 | 61 | {{ define "__alert_summary" }} 62 | {{ range . -}} 63 | - {{ if eq .Status "firing" -}} 64 | {{ template "__alertinstance" . }} 65 | {{- else -}} 66 | {{ template "__alertinstance" . }} 67 | {{- end }} 68 | {{ end }} 69 | {{- end }} 70 | 71 | {{ define "prom.title" -}} 72 | {{ template "__subject" . -}}{{ end }} 73 | 74 | {{ define "prom.markdown" }} 75 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 76 | --- 77 | **Summary** 78 | 79 | {{ if gt (.Alerts.Firing|len ) 0 -}} 80 | **🚨 Firing [{{ .Alerts.Firing|len }}] alerts** 81 | 82 | {{ template "__alert_summary" .Alerts.Firing }}{{ end }} 83 | 84 | {{ if gt (.Alerts.Resolved|len) 0 -}} 85 | **✅ Resolved [{{ .Alerts.Resolved|len }}] alerts** 86 | 87 | {{ template "__alert_summary" .Alerts.Resolved }}{{ end }} 88 | 89 | --- 90 | **Detail** 91 | 92 | {{ if gt (.Alerts.Firing|len ) 0 -}} 93 | **🚨 Firing [{{ .Alerts.Firing|len }}] alerts** 94 | {{ template "__alert_list" .Alerts.Firing }}{{ end }} 95 | 96 | {{ if gt (.Alerts.Resolved|len) 0 -}} 97 | **✅ Resolved [{{ .Alerts.Resolved|len }}] alerts** 98 | {{ template "__alert_list" .Alerts.Resolved }}{{ end }} 99 | {{ end }} 100 | 101 | {{ define "prom.text" }} 102 | {{ template "prom.markdown" . }} 103 | {{ end }} 104 | -------------------------------------------------------------------------------- /pkg/models/templates/weixin.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}Firing:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}Resolved:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | {{ define "__alertinstance" -}} 27 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 28 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 29 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 30 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 31 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 32 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 33 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 34 | {{- end -}} 35 | {{- end }} 36 | 37 | {{ define "__alert_list" }} 38 | {{ range . }} 39 | > Alert Name : {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 40 | > 41 | > Alert Level :{{ ` ` }} 42 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 43 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 44 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 45 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 46 | > 47 | > Instance : `{{ template "__alertinstance" . }}` 48 | > 49 | {{- if .Labels.region }} 50 | > Region : {{ .Labels.region }} 51 | > 52 | {{- end }} 53 | {{- if .Labels.zone }} 54 | > Zone : {{ .Labels.zone }} 55 | > 56 | {{- end }} 57 | {{- if .Labels.product }} 58 | > Product : {{ .Labels.product }} 59 | > 60 | {{- end }} 61 | {{- if .Labels.component }} 62 | > Component : {{ .Labels.component }} 63 | > 64 | {{- end }} 65 | > Alert Status : {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 66 | > 67 | > Start At : {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 68 | > 69 | > End At : {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 70 | {{- if eq .Status "firing" }} 71 | > 72 | > Description : {{ if .Annotations.description_en }}{{ .Annotations.description_en }}{{ else }}{{ .Annotations.description }}{{ end }} 73 | {{- end }} 74 | 75 | {{ end }} 76 | {{ end }} 77 | 78 | 79 | {{ define "__alert_summary" -}} 80 | {{ range . }} 81 | {{ template "__alertinstance" . }} 82 | {{ end }} 83 | {{ end }} 84 | 85 | {{ define "prom.title" -}} 86 | {{ template "__subject" . }} 87 | {{ end }} 88 | 89 | 90 | {{ define "prom.markdown" }} 91 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 92 | #### Summary 93 | 94 | {{ if gt (.Alerts.Firing|len ) 0 }} 95 | ##### 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 96 | {{ template "__alert_summary" .Alerts.Firing }} 97 | {{ end }} 98 | 99 | 100 | {{ if gt (.Alerts.Resolved|len) 0 }} 101 | ##### ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 102 | {{ template "__alert_summary" .Alerts.Resolved }} 103 | {{ end }} 104 | 105 | #### Detail 106 | 107 | {{ if gt (.Alerts.Firing|len ) 0 }} 108 | ##### 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 109 | {{ template "__alert_list" .Alerts.Firing }} 110 | {{ end }} 111 | 112 | 113 | {{ if gt (.Alerts.Resolved|len) 0 }} 114 | ##### ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 115 | {{ template "__alert_list" .Alerts.Resolved }} 116 | {{ end }} 117 | {{ end }} 118 | 119 | {{ define "prom.text" }} 120 | {{ template "prom.markdown" . }} 121 | {{ end }} 122 | -------------------------------------------------------------------------------- /pkg/models/templates/weixinapp.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "__subject" -}} 2 | 【{{ .Signature }}】 3 | 4 | {{- if eq (index .Alerts 0).Labels.severity "ok" }} OK{{ end }} 5 | {{- if eq (index .Alerts 0).Labels.severity "info" }} INFO{{ end }} 6 | {{- if eq (index .Alerts 0).Labels.severity "warning" }} WARNING{{ end }} 7 | {{- if eq (index .Alerts 0).Labels.severity "critical" }} CRITICAL{{ end }} 8 | 9 | {{- ` • ` }} 10 | 11 | {{- if .CommonLabels.alertname_cn }}{{ .CommonLabels.alertname_cn }}{{ else if .CommonLabels.alertname_custom }}{{ .CommonLabels.alertname_custom }}{{ else if .CommonAnnotations.alertname }}{{ .CommonAnnotations.alertname }}{{ else }}{{ .GroupLabels.alertname }}{{ end }} 12 | 13 | {{- ` • ` }} 14 | 15 | {{- if gt (.Alerts.Firing|len) 0 }}Firing:{{ .Alerts.Firing|len }}{{ end }} 16 | {{- if and (gt (.Alerts.Firing|len) 0) (gt (.Alerts.Resolved|len) 0) }}/{{ end }} 17 | {{- if gt (.Alerts.Resolved|len) 0 }}Resolved:{{ .Alerts.Resolved|len }}{{ end }} 18 | 19 | {{ end }} 20 | 21 | 22 | {{ define "__externalURL" -}} 23 | {{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }} 24 | {{- end }} 25 | 26 | 27 | {{ define "__alertinstance" -}} 28 | {{- if ne .Labels.alertinstance nil -}}{{ .Labels.alertinstance }} 29 | {{- else if ne .Labels.instance nil -}}{{ .Labels.instance }} 30 | {{- else if ne .Labels.node nil -}}{{ .Labels.node }} 31 | {{- else if ne .Labels.nodename nil -}}{{ .Labels.nodename }} 32 | {{- else if ne .Labels.host nil -}}{{ .Labels.host }} 33 | {{- else if ne .Labels.hostname nil -}}{{ .Labels.hostname }} 34 | {{- else if ne .Labels.ip nil -}}{{ .Labels.ip }} 35 | {{- end -}} 36 | {{- end }} 37 | 38 | {{ define "__alert_list" }} 39 | {{ range . }} 40 | ⁣ 41 | 42 | > Alert Name : {{ if .Labels.alertname_cn }}{{ .Labels.alertname_cn }}{{ else if .Labels.alertname_custom }}{{ .Labels.alertname_custom }}{{ else if .Annotations.alertname }}{{ .Annotations.alertname }}{{ else }}{{ .Labels.alertname }}{{ end }} 43 | Alert Level :{{ ` ` }} 44 | {{- if eq .Labels.severity "ok" }}OK{{ end -}} 45 | {{- if eq .Labels.severity "info" }}INFO{{ end -}} 46 | {{- if eq .Labels.severity "warning" }}WARNING{{ end -}} 47 | {{- if eq .Labels.severity "critical" }}CRITICAL{{ end }} 48 | Alert Instance : `{{ template "__alertinstance" . }}` 49 | {{- if .Labels.region }} 50 | Region : {{ .Labels.region }} 51 | {{- end }} 52 | {{- if .Labels.zone }} 53 | Zone : {{ .Labels.zone }} 54 | {{- end }} 55 | {{- if .Labels.product }} 56 | Product : {{ .Labels.product }} 57 | {{- end }} 58 | {{- if .Labels.component }} 59 | Component : {{ .Labels.component }} 60 | {{- end }} 61 | Alert Status : {{ if eq .Status "firing" }}🚨{{ else }}✅{{ end }} {{ .Status | toUpper }} 62 | Start At : {{ .StartsAt.Format "2006-01-02T15:04:05Z07:00" }} 63 | End At : {{ if .EndsAt.After .StartsAt }}{{ .EndsAt.Format "2006-01-02T15:04:05Z07:00" }}{{ else }}Not End{{ end }} 64 | {{- if eq .Status "firing" }} 65 | Description : {{ if .Annotations.description_en }}{{ .Annotations.description_en }}{{ else }}{{ .Annotations.description }}{{ end }} 66 | {{- end }} 67 | 68 | {{ end }} 69 | {{ end }} 70 | 71 | {{ define "__alert_summary" -}} 72 | {{ range . }} 73 | {{ template "__alertinstance" . }} 74 | {{- end }} 75 | {{- end }} 76 | 77 | {{ define "prom.title" -}} 78 | {{ template "__subject" . }} 79 | {{ end }} 80 | 81 | 82 | {{ define "prom.markdown" }} 83 | {{ .MessageAt.Format "2006-01-02T15:04:05Z07:00" }} 84 | 85 | ### Alerts Summary 86 | 87 | {{ if gt (.Alerts.Firing|len ) 0 }} 88 | #### 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 89 | {{ template "__alert_summary" .Alerts.Firing }} 90 | {{ end }} 91 | 92 | 93 | {{ if gt (.Alerts.Resolved|len) 0 }} 94 | #### ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 95 | {{ template "__alert_summary" .Alerts.Resolved }} 96 | {{ end }} 97 | 98 | ### Alerts Detail 99 | 100 | {{ if gt (.Alerts.Firing|len ) 0 }} 101 | #### 🚨 Firing [{{ .Alerts.Firing|len }}] alerts 102 | {{ template "__alert_list" .Alerts.Firing }} 103 | {{ end }} 104 | 105 | 106 | {{ if gt (.Alerts.Resolved|len) 0 }} 107 | #### ✅ Resolved [{{ .Alerts.Resolved|len }}] alerts 108 | {{ template "__alert_list" .Alerts.Resolved }} 109 | {{ end }} 110 | {{ end }} 111 | 112 | {{ define "prom.text" }} 113 | {{ template "prom.markdown" . }} 114 | {{ end }} 115 | -------------------------------------------------------------------------------- /pkg/models/alert.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/bougou/webhook-adapter/models" 11 | ) 12 | 13 | // ref: https://prometheus.io/docs/alerting/latest/configuration/#webhook_config 14 | // The Alertmanager will send HTTP POST requests in the following JSON format to the configured endpoint: 15 | // { 16 | // "version": "4", 17 | // "groupKey": , // key identifying the group of alerts (e.g. to deduplicate) 18 | // "truncatedAlerts": , // how many alerts have been truncated due to "max_alerts" 19 | // "status": "", 20 | // "receiver": , 21 | // "groupLabels": , 22 | // "commonLabels": , 23 | // "commonAnnotations": , 24 | // "externalURL": , // backlink to the Alertmanager. 25 | // "alerts": [ 26 | // { 27 | // "status": "", 28 | // "labels": , 29 | // "annotations": , 30 | // "startsAt": "", 31 | // "endsAt": "", 32 | // "generatorURL": // identifies the entity that caused the alert 33 | // }, 34 | // ... 35 | // ] 36 | // } 37 | 38 | // AlertManagerWebhookMessage holds data that alertmanager passed to webhook server. 39 | // https://pkg.go.dev/github.com/prometheus/alertmanager/template#Data already defines 40 | // a struct type, but here we re-defined it, cause we will fill extra fields into it. 41 | type AlertmanagerWebhookMessage struct { 42 | Version string `json:"version"` 43 | GroupKey *json.RawMessage `json:"groupKey"` 44 | TruncatedAlerts int `json:"truncatedAlerts"` 45 | 46 | Status string `json:"status"` 47 | Receiver string `json:"receiver"` 48 | Alerts Alerts `json:"alerts"` 49 | GroupLabels KV `json:"groupLabels"` 50 | CommonLabels KV `json:"commonLabels"` 51 | CommonAnnotations KV `json:"commonAnnotations"` 52 | ExternalURL string `json:"externalURL"` 53 | 54 | // extra fields added by us 55 | MessageAt time.Time `json:"messageAt"` // the time the webhook message was received 56 | Signature string `json:"signature"` // 签名,如发送短信时出现在内容最前面【】 57 | } 58 | 59 | type Alerts []Alert 60 | 61 | type Alert struct { 62 | Status string `json:"status"` 63 | Labels KV `json:"labels"` 64 | Annotations KV `json:"annotations"` 65 | StartsAt time.Time `json:"startsAt"` 66 | EndsAt time.Time `json:"endsAt"` 67 | GeneratorURL string `json:"generatorURL"` 68 | } 69 | 70 | // Firing returns the subset of alerts that are firing. 71 | func (as Alerts) Firing() []Alert { 72 | res := []Alert{} 73 | for _, a := range as { 74 | if a.Status == "firing" { 75 | res = append(res, a) 76 | } 77 | } 78 | return res 79 | } 80 | 81 | // Resolved returns the subset of alerts that are resolved. 82 | func (as Alerts) Resolved() []Alert { 83 | res := []Alert{} 84 | for _, a := range as { 85 | if a.Status == "resolved" { 86 | res = append(res, a) 87 | } 88 | } 89 | return res 90 | } 91 | 92 | func (alert *Alert) UnmarshalJSON(data []byte) error { 93 | m := make(map[string]interface{}) 94 | 95 | if err := json.Unmarshal(data, &m); err != nil { 96 | return err 97 | } 98 | 99 | for k, v := range m { 100 | switch k { 101 | case "startsAt": 102 | _t := v.(string) 103 | t, err := parseTimeFromStr(_t) 104 | if err != nil { 105 | return err 106 | } 107 | alert.StartsAt = t 108 | 109 | case "endsAt": 110 | _t := v.(string) 111 | t, err := parseTimeFromStr(_t) 112 | if err != nil { 113 | return err 114 | } 115 | alert.EndsAt = t 116 | 117 | case "status": 118 | alert.Status = v.(string) 119 | 120 | case "generatorURL": 121 | alert.GeneratorURL = v.(string) 122 | 123 | case "labels": 124 | s, err := json.Marshal(v) 125 | if err != nil { 126 | return err 127 | } 128 | kv := KV{} 129 | if err := json.Unmarshal(s, &kv); err != nil { 130 | return err 131 | } 132 | alert.Labels = kv 133 | 134 | case "annotations": 135 | s, err := json.Marshal(v) 136 | if err != nil { 137 | return err 138 | } 139 | kv := KV{} 140 | if err := json.Unmarshal(s, &kv); err != nil { 141 | return err 142 | } 143 | alert.Annotations = kv 144 | } 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func parseTimeFromStr(timeStr string) (time.Time, error) { 151 | t, err := time.Parse(time.RFC3339, timeStr) 152 | if err != nil { 153 | return t, err 154 | } 155 | 156 | return t.In(time.Local), nil 157 | } 158 | 159 | func (m *AlertmanagerWebhookMessage) SetMessageAt() *AlertmanagerWebhookMessage { 160 | m.MessageAt = time.Now() 161 | return m 162 | } 163 | 164 | func (m *AlertmanagerWebhookMessage) SetSignature(s string) *AlertmanagerWebhookMessage { 165 | m.Signature = s 166 | return m 167 | } 168 | 169 | func (m *AlertmanagerWebhookMessage) RenderTmpl(channel string, tmplName string) (string, error) { 170 | var safetmpl *safeTemplate 171 | 172 | if t, exists := promMsgTemplatesMap[channel]; !exists { 173 | safetmpl = promMsgTemplateDefault 174 | } else { 175 | safetmpl = t 176 | } 177 | 178 | tmpl, err := safetmpl.Clone() 179 | if err != nil { 180 | msg := fmt.Sprintf("Clone template failed, err: %s", err) 181 | return "", errors.New(msg) 182 | } 183 | 184 | var buf bytes.Buffer 185 | if err := tmpl.ExecuteTemplate(&buf, tmplName, m); err != nil { 186 | msg := fmt.Sprintf("ExecuteTemplate failed, err: %s", err) 187 | return "<<<< template error >>>>", errors.New(msg) 188 | } 189 | 190 | return buf.String(), nil 191 | } 192 | 193 | func (m *AlertmanagerWebhookMessage) ToPayload(channel string, raw []byte) (*models.Payload, error) { 194 | payload := &models.Payload{Raw: string(raw)} 195 | 196 | title, err := m.RenderTmpl(channel, "prom.title") 197 | if err != nil { 198 | return nil, err 199 | } 200 | payload.Title = title 201 | 202 | text, err := m.RenderTmpl(channel, "prom.text") 203 | if err != nil { 204 | return nil, err 205 | } 206 | payload.Text = text 207 | 208 | markdown, err := m.RenderTmpl(channel, "prom.markdown") 209 | if err != nil { 210 | return nil, err 211 | } 212 | payload.Markdown = markdown 213 | 214 | return payload, nil 215 | } 216 | -------------------------------------------------------------------------------- /pkg/models/tmpl.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path" 9 | "strings" 10 | "sync" 11 | "text/template" 12 | 13 | "golang.org/x/text/cases" 14 | 15 | "github.com/bougou/alertmanager-webhook-adapter/pkg/models/templates" 16 | ) 17 | 18 | var ( 19 | topLevelTemplateName = "prom" 20 | 21 | // store the default templates 22 | promMsgTemplateDefault *safeTemplate 23 | 24 | // store templates for different channels 25 | promMsgTemplatesMap = make(map[string]*safeTemplate) 26 | 27 | defaultFuncs = map[string]interface{}{ 28 | "toUpper": strings.ToUpper, 29 | "toLower": strings.ToLower, 30 | "title": cases.Title, 31 | "markdown": markdownEscapeString, 32 | } 33 | isMarkdownSpecial [128]bool 34 | ) 35 | 36 | func init() { 37 | var err error 38 | 39 | if err = LoadDefaultTemplate("en"); err != nil { 40 | panic(err) 41 | } 42 | 43 | for _, c := range "_*`" { 44 | isMarkdownSpecial[c] = true 45 | } 46 | } 47 | 48 | // LoadDefaultTemplate set default for the package level variables: promMsgTemplate and promMsgTemplatesMap. 49 | func LoadDefaultTemplate(tmplLang string) error { 50 | defaultTmpl := templates.DefaultTmplByLang[tmplLang] 51 | promMsgTemplateDefault = &safeTemplate{} 52 | if err := promMsgTemplateDefault.UpdateTemplate(defaultTmpl); err != nil { 53 | msg := fmt.Sprintf("UpdateTemplate for default failed, err: %s", err) 54 | return errors.New(msg) 55 | } 56 | 57 | channelsDefaultTmpls := templates.ChannelsDefaultTmplMapByLang[tmplLang] 58 | for k, v := range channelsDefaultTmpls { 59 | t := &safeTemplate{} 60 | if err := t.UpdateTemplate(v); err != nil { 61 | msg := fmt.Sprintf("UpdateTemplate for (%s) failed, err: %s", k, err) 62 | return errors.New(msg) 63 | } 64 | promMsgTemplatesMap[k] = t 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // LoadTemplate loads external templates from specified template dir. 71 | func LoadTemplate(tmplDir, tmplName, tmplDefault, tmplLang string) error { 72 | 73 | // If tmplName is not empty, use the specified tmpl to update the default promMsgTemplate 74 | // and clear the promMsgTemplatesMap, thus will use the specified tmpl for all notification channels. 75 | if tmplName != "" { 76 | for k := range promMsgTemplatesMap { 77 | delete(promMsgTemplatesMap, k) 78 | } 79 | 80 | tmplFile := path.Join(tmplDir, fmt.Sprintf("%s.%s", tmplName, "tmpl")) 81 | if tmplLang != "" && tmplLang != "en" { 82 | tmplFile = path.Join(tmplDir, fmt.Sprintf("%s.%s.%s", tmplName, tmplLang, "tmpl")) 83 | } 84 | b, err := os.ReadFile(tmplFile) 85 | if err != nil { 86 | msg := fmt.Sprintf("read file (%s) failed, err: %s", tmplFile, err) 87 | return errors.New(msg) 88 | } 89 | 90 | if err := promMsgTemplateDefault.UpdateTemplate(string(b)); err != nil { 91 | msg := fmt.Sprintf("UpdateTemplate for default failed, err: %s", err) 92 | return errors.New(msg) 93 | } 94 | 95 | return nil 96 | } 97 | 98 | var customDefaultTmpl string 99 | if tmplDefault != "" { 100 | tmplFile := path.Join(tmplDir, fmt.Sprintf("%s.%s", tmplDefault, "tmpl")) 101 | if tmplLang != "" && tmplLang != "en" { 102 | tmplFile = path.Join(tmplDir, fmt.Sprintf("%s.%s.%s", tmplDefault, tmplLang, "tmpl")) 103 | } 104 | b, err := os.ReadFile(tmplFile) 105 | if err != nil { 106 | msg := fmt.Sprintf("read file (%s) failed, err: %s", tmplFile, err) 107 | return errors.New(msg) 108 | } 109 | customDefaultTmpl = string(b) 110 | } 111 | 112 | // try to find template file named "[.].tmpl" and update the promTemplatesMap 113 | for channel, t := range promMsgTemplatesMap { 114 | var channelTmpl string 115 | 116 | tmplFile := path.Join(tmplDir, fmt.Sprintf("%s.%s", channel, "tmpl")) 117 | if tmplLang != "" && tmplLang != "en" { 118 | tmplFile = path.Join(tmplDir, fmt.Sprintf("%s.%s.%s", channel, tmplLang, "tmpl")) 119 | } 120 | b, err := os.ReadFile(tmplFile) 121 | if os.IsNotExist(err) { 122 | // case 1: [.].tmpl file does not exist, and not specified custom default 123 | // then will use the builtin default, continue the next loop 124 | if tmplDefault == "" { 125 | continue 126 | } 127 | // case 2: [.].tmpl file does not exist, but specified custom default 128 | // then will use custom default as tmpl 129 | channelTmpl = customDefaultTmpl 130 | } else { 131 | // case 3: [.].tmpl exists, but read failed, error and return 132 | if err != nil { 133 | msg := fmt.Sprintf("read file (%s) failed, err: %s", tmplFile, err) 134 | return errors.New(msg) 135 | } 136 | // case 4: [.].tmpl exists, and read succeeded, use file content as tmpl 137 | channelTmpl = string(b) 138 | } 139 | 140 | if err := t.UpdateTemplate(channelTmpl); err != nil { 141 | msg := fmt.Sprintf("UpdateTemplate for (%s) failed, err: %s", channel, err) 142 | return errors.New(msg) 143 | } 144 | } 145 | 146 | return nil 147 | } 148 | 149 | type safeTemplate struct { 150 | *template.Template 151 | current string 152 | mu sync.RWMutex 153 | } 154 | 155 | func (t *safeTemplate) UpdateTemplate(newTpl string) (err error) { 156 | t.mu.Lock() 157 | defer t.mu.Unlock() 158 | 159 | tpl, err := template.New(topLevelTemplateName). 160 | Funcs(defaultFuncs). 161 | Option("missingkey=zero"). 162 | Parse(newTpl) 163 | if err != nil { 164 | return 165 | } 166 | 167 | _ = t.current // old template 168 | t.Template = tpl 169 | t.current = newTpl 170 | return 171 | } 172 | 173 | func (t *safeTemplate) Clone() (*template.Template, error) { 174 | t.mu.RLock() 175 | defer t.mu.RUnlock() 176 | 177 | return t.Template.Clone() 178 | } 179 | 180 | func markdownEscapeString(s string) string { 181 | b := make([]byte, 0, len(s)) 182 | buf := bytes.NewBuffer(b) 183 | 184 | for _, c := range s { 185 | if c < 128 && isMarkdownSpecial[c] { 186 | buf.WriteByte('\\') 187 | } 188 | buf.WriteRune(c) 189 | } 190 | return buf.String() 191 | } 192 | 193 | func ExecuteTextString(text string, data interface{}) (string, error) { 194 | if text == "" { 195 | return "", nil 196 | } 197 | 198 | tmpl, err := promMsgTemplateDefault.Clone() 199 | if err != nil { 200 | return "", err 201 | } 202 | 203 | tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(text) 204 | if err != nil { 205 | return "", err 206 | } 207 | 208 | // reserve a buffer in 1k 209 | var buf bytes.Buffer 210 | err = tmpl.Execute(&buf, data) 211 | return buf.String(), err 212 | } 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alertmanager-webhook-adapter 2 | 3 | A general webhook server for receiving [Prometheus AlertManager](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config)'s notifications and send them through different channel types. 4 | 5 | 6 | ## Supported Notification Channels 7 | 8 | - `weixin`, Weixin Group Bot / 企业微信群机器人 9 | 10 | ``` 11 | http(s)://{this-webhook-server-addr}/webhook/send?channel_type=weixin&token={token} 12 | ``` 13 | 14 | - `dingtalk`, Dingtalk Group Bot / 钉钉群机器人 15 | 16 | ``` 17 | http(s)://{this-webhook-server-addr}/webhook/send?channel_type=dingtalk&token={token} 18 | ``` 19 | 20 | - `feishu`, Feishu Group Bot / 飞书群机器人 21 | 22 | ``` 23 | http(s)://{this-webhook-server-addr}/webhook/send?channel_type=feishu&token={token} 24 | ``` 25 | 26 | - `weixinapp`, Weixin Application / 企业微信应用 27 | 28 | ```bash 29 | # Must specify one of to_user, to_party, to_tag parameter 30 | http(s)://{this-webhook-server-addr}/webhook/send?channel_type=weixinapp&corp_id={corp_id}&agent_id={agent_id}&agent_secret={agent_secret}&to_user={user_id}&to_party={party_id}&to_tag={tag_id} 31 | 32 | # to_user 指定接收消息的成员,成员 ID 列表(多个接收者用 '|' 分隔,最多支持 1000 个) 33 | # 指定为 "@all",则向该企业应用的全部成员发送 34 | 35 | # to_party 指定接收消息的部门,部门 ID 列表,多个接收者用 '|' 分隔,最多支持 100 个 36 | # 当 to_user 为 "@all" 时忽略本参数 37 | 38 | # to_tag 指定接收消息的标签,标签 ID 列表,多个接收者用 '|' 分隔,最多支持 100 个 39 | # 当 to_user 为 "@all" 时忽略本参数 40 | ``` 41 | 42 | - `slack`, Slack App 43 | 44 | ``` 45 | http(s)://{this-webhook-server-addr}/webhook/send?channel_type=slack&token=&channel= 46 | ``` 47 | 48 | - `discord-webhook`, Discord Webhook 49 | 50 | ```bash 51 | http(s)://{this-webhook-server-addr}/webhook/send?channel_type=discord-webhook&id={id}&token={token} 52 | 53 | # discord webhook url: 54 | # https://discord.com/api/webhooks/{id}/{token} 55 | # https://discord.com/api/webhooks/12673xx/adruxxx 56 | ``` 57 | 58 | > More is comming... 59 | 60 | ## Run 61 | 62 | ### Build and Run 63 | 64 | ```bash 65 | $ cd cmd/alertmanager-webhook-adapter 66 | $ go build -v -x 67 | 68 | $ ./alertmanager-webhook-adapter 69 | 70 | # see help 71 | $ ./alertmanager-webhook-adapter -h 72 | 73 | # Add signature for sent messages 74 | $ ./alertmanager-webhook-adapter --listen-address=:8060 --signature "Anything-You-Like" 75 | # the signature will be added to the beginning of the message: 76 | # 【Anything-You-Like】this-is-the-xxxxxxxxxx-message 77 | ``` 78 | 79 | ### Start as systemd service 80 | 81 | ```bash 82 | # Install the binary alertmanager-webhook-adapter file to some directory 83 | # like /usr/local/bin/alertmanager-webhook-adapter 84 | # and chmod +x /usr/local/bin/alertmanager-webhook-adapter 85 | 86 | $ cp deploy/alertmanager-webhook-adapter.service /etc/systemd/system/ 87 | 88 | # make sure the bin path to be consistent 89 | # ExecStart= 90 | $ vim /etc/systemd/system/alertmanager-webhook-adapter.service 91 | 92 | $ systemctl daemon-reload 93 | $ systemctl start 94 | ``` 95 | 96 | ### Run as Docker container 97 | 98 | ```bash 99 | # expose the host port (127.0.0.1:8090) to container port (8090) 100 | docker run --rm -it -p 127.0.0.1:8090:8090 bougou/alertmanager-webhook-adapter:v1.1.10 101 | 102 | docker run --rm -it -p 127.0.0.1:8090:8090 bougou/alertmanager-webhook-adapter:v1.1.10 --signature MyIDC --lang zh --debug 103 | ``` 104 | 105 | ### Run in K8S 106 | 107 | Apply manifests: 108 | 109 | ```bash 110 | cd deploy/k8s 111 | kubectl apply -f deployment.yaml 112 | kubectl apply -f service.yaml 113 | ``` 114 | 115 | Or Deploy with Helm 116 | 117 | ```bash 118 | # prepare custom values.yaml 119 | # see: https://github.com/bougou/alertmanager-webhook-adapter/blob/main/deploy/charts/alertmanager-webhook-adapter/values.yaml 120 | vim values.yaml 121 | 122 | helm repo add bougoucharts https://bougou.github.io/charts 123 | helm repo update 124 | 125 | helm upgrade alertmanager-webhook-adapter \ 126 | bougoucharts/alertmanager-webhook-adapter \ 127 | --install \ 128 | --namespace infra \ 129 | --version v1.0.1 \ 130 | --values values.yaml 131 | ``` 132 | 133 | ## Configure Alertmanager to send alert messages to this webhook server 134 | 135 | ```yaml 136 | - name: 'sre-team' 137 | webhook_configs: 138 | - url: "http://10.0.0.1:8090/webhook/send?channel_type=weixin&token=1234-1234-1234" 139 | ``` 140 | 141 | ## Command 142 | 143 | ``` 144 | $ ./alertmanager-webhook-adapter -h 145 | alertmanager-webhook-adapter 146 | 147 | Usage: 148 | alertmanager-webhook-adapter [flags] 149 | 150 | Flags: 151 | -h, --help help for alertmanager-webhook-adapter 152 | -l, --listen-address string the address to listen (default "0.0.0.0:8090") 153 | -s, --signature string the signature (default "未知") 154 | -n, --tmpl-default string the default tmpl name 155 | -d, --tmpl-dir string the tmpl dir 156 | --tmpl-lang string the language for template filename 157 | -t, --tmpl-name string the tmpl name 158 | ``` 159 | 160 | ## Builtin Templates Notification Screenshots 161 | 162 | - [Chinese](./docs/screenshot-zh.md) 163 | 164 | | WeixinGroupBot | WeixinApp | DingTalkGroupBot | FeishuGroupBot | 165 | | --------------------------------------------- | ------------------------------------------------ | ----------------------------------------------- | --------------------------------------------- | 166 | | | | | | 167 | 168 | | Slack App | 169 | | -------------------------------------------- | 170 | | | 171 | 172 | ## Custom Templates 173 | 174 | The project already has builtin templates for all supported notification channels. 175 | But you can use your own template file(s) to override those defaults. 176 | 177 | You can use the following three options. 178 | 179 | - `--tmpl-dir (-d)` 180 | - `--tmpl-name (-t)` 181 | - `--tmpl-default (-n)` 182 | 183 | The `--tmpl-dir` is a MUST if you want to load your custom templates. `--tmpl-name` and `--tmpl-default` is optional. So, there are THREE use cases when combining those options. 184 | 185 | 1. `--tmpl-dir ` 186 | 2. `--tmpl-dir --tmpl-name ` 187 | 3. `--tmpl-dir --tmpl-default ` 188 | 189 | > If `--tmpl-name` and `--tmpl-default` are both specified, `--tmpl-default` will be ignored. 190 | 191 | These three use cases are used for different purposes. 192 | 193 | ### `--tmpl-dir` 194 | 195 | > **Purpose**: Use different template files for different channels 196 | 197 | First, create a dir to store your template files, like `templates`. And then put your template files under the template dir. 198 | 199 | The program will **try to search `.tmpl` files** under the tmpl dir for all supported channels, 200 | and use the founded file as the template for the corresponding channel. If not found, use builtin template. 201 | 202 | ```bash 203 | $ touch templates/feishu.tmpl 204 | $ touch templates/weixin.tmpl 205 | 206 | # use templates/feishu.tmpl for feishu channel 207 | # use templates/weixin.tmpl for weixin channel, 208 | # use builtin templates for other channels. 209 | $ ./alertmanager-webhook-adapter -s Bougou --tmpl-dir ./templates/ 210 | ``` 211 | 212 | ### `--tmpl-dir` and `--tmpl-name` 213 | 214 | > **Purpose**: Use one custom template for all channels. 215 | 216 | Create your own template file, like `custom.tmpl`, and put it under the template dir. 217 | The filename with suffix removed will be the template name and be used as value of the `--tmpl-name` parameter. 218 | 219 | The program will **try to search `.tmpl` file** under the tmpl dir. 220 | The selected tmpl file will be used for all notification channels. If not found, error and exit. 221 | 222 | ```bash 223 | # use templates/custom.tmpl for all channels. 224 | $ ./alertmanager-webhook-adapter -s Bougou --tmpl-dir ./templates/ --tmpl-name custom 225 | ``` 226 | 227 | ### `--tmpl-dir` and `--tmpl-default` 228 | 229 | > **Purpose**: Use different template files for only several channels, and use an extra template file for all other channels. 230 | 231 | ```bash 232 | $ touch templates/feishu.tmpl 233 | $ touch templates/weixin.tmpl 234 | 235 | $ touch templates/default.tmpl 236 | 237 | # use templates/feishu.tmpl for feishu channel 238 | # use templates/weixin.tmpl for weixin channel, 239 | # use templates/default.tmpl for other channels. 240 | $ ./alertmanager-webhook-adapter -s Bougou --tmpl-dir ./templates/ --tmpl-default default 241 | ``` 242 | 243 | ### Template Content 244 | 245 | The template file should use an [`AlertmanagerWebhookMessage`](./pkg/models/alert.go) object as the input data. 246 | 247 | ```go 248 | type AlertmanagerWebhookMessage struct { 249 | Version string `json:"version"` 250 | GroupKey *json.RawMessage `json:"groupKey"` 251 | TruncatedAlerts int `json:"truncatedAlerts"` 252 | 253 | Status string `json:"status"` 254 | Receiver string `json:"receiver"` 255 | Alerts Alerts `json:"alerts"` 256 | GroupLabels KV `json:"groupLabels"` 257 | CommonLabels KV `json:"commonLabels"` 258 | CommonAnnotations KV `json:"commonAnnotations"` 259 | ExternalURL string `json:"externalURL"` 260 | 261 | MessageAt time.Time `json:"messageAt"` // the time the webhook message was received 262 | Signature string `json:"signature"` // 签名,如发送短信时出现在内容最前面【】 263 | } 264 | ``` 265 | 266 | All template files MUST define the following template parts in the template file. 267 | 268 | - `prom.title` 269 | - `prom.text` 270 | - `prom.markdown` 271 | 272 | ## Language for template files 273 | 274 | When loading template files, the program defaults to try to load files with name `.tmpl` or `.tmpl` or `.tmpl`. 275 | 276 | But you can specify the option `--tmpl-lang ` to change the loading rule. 277 | 278 | If `--tmpl-lang ` is specified, **and the specified lang is NOT equal to `en`**, the program will try to load files with name `..tmpl` or `..tmpl` or `..tmpl`. 279 | If `` equals to `en`, the default loading rule is NOT changed. 280 | 281 | The `` can be any string, just make sure it matches your desired file names. 282 | 283 | This project already builtin supports two languages, `en` for english, `zh` for chinese. It defaults to `en` if `--tmpl-lang` is not specified. 284 | 285 | > The `--tmpl-lang` only impacts which files will be loaded, it does not care the contents of the files. 286 | 287 | ## How AlertInstance is determined? 288 | 289 | The default notification templates will try its best to print the alert instance information for each alert. 290 | The alert instance is determined from the labels of the alerts. 291 | 292 | The following labels of the alerts are sought by priority order and selected as "alert instance" if the label is found. 293 | 294 | - `alertinstance` 295 | - `instance` 296 | - `node` 297 | - `nodename` 298 | - `host` 299 | - `hostname` 300 | - `ip` 301 | 302 | In prometheus, most metrics may provide `instance`, or `node` or `ip` label, but its value may not be suitable for alert information. Then, I recommend to use 303 | the following two methods to add an extra `alertinstance` label when writing alert rules. 304 | 305 | 1. Use PromQL function [`label_join`](https://prometheus.io/docs/prometheus/latest/querying/functions/#label_join), eg: 306 | 307 | ```yaml 308 | - alert: KubePodCrashLooping 309 | expr: label_join(max_over_time(kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff", job="kube-state-metrics", namespace=~".*"}[5m]) >= 1, 'alertinstance', '/', 'namespace', 'pod') 310 | for: 15m 311 | labels: 312 | severity: warning 313 | annotations: 314 | description: 'Pod {{ $labels.namespace }}/{{ $labels.pod }} ({{ $labels.container }}) is in waiting state (reason: "CrashLoopBackOff").' 315 | summary: Pod is crash looping. 316 | ``` 317 | 318 | 2. (Preferred) Directly add `alertinstance` label, eg: 319 | 320 | ```yaml 321 | - alert: KubePodCrashLooping 322 | expr: max_over_time(kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff", job="kube-state-metrics", namespace=~".*"}[5m]) >= 1 323 | for: 15m 324 | labels: 325 | severity: warning 326 | alertinstance: '{{ $labels.namespace }}/{{ $labels.pod }}' 327 | annotations: 328 | description: 'Pod {{ $labels.namespace }}/{{ $labels.pod }} ({{ $labels.container }}) is in waiting state (reason: "CrashLoopBackOff").' 329 | summary: Pod is crash looping. 330 | ``` 331 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 17 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 18 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 19 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 20 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 21 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 22 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 23 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 24 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 25 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 26 | github.com/bougou/webhook-adapter v0.1.1 h1:MYQhQfpH5N66HLLPiayZ1DSRdZKtUPr4te6w+dF/gUs= 27 | github.com/bougou/webhook-adapter v0.1.1/go.mod h1:GYAmLIjZqdf6kBKea+odq3RtTTf3B/tji1DPuFFOT14= 28 | github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= 29 | github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 30 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 31 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 32 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 33 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 34 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 35 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 36 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 37 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 38 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 40 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 42 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 43 | github.com/emicklei/go-restful/v3 v3.4.0 h1:IIDhql3oyWZj1ay2xBZGb4sTOWMad0HVW8rwhVxN/Yk= 44 | github.com/emicklei/go-restful/v3 v3.4.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 45 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 46 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 47 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 48 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 49 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 50 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 51 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 52 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 53 | github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= 54 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 55 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 56 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 57 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 58 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 59 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 60 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 61 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 62 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 66 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 67 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 68 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 69 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 70 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 71 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 72 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 73 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 74 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 75 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 76 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 77 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 78 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 79 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 80 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 81 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 82 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 83 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 84 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 85 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 86 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 87 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 88 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 89 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 90 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 91 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 92 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 93 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 94 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 95 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 96 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 97 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 98 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 99 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 100 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 101 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 102 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 103 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 104 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 105 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 106 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 107 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 108 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 109 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 110 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 111 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 112 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 113 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 114 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 115 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 116 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 117 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 118 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 119 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 120 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 121 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 122 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 123 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 124 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 125 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 126 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 127 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 128 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 129 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 130 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 131 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 132 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 133 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 134 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 135 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 136 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 137 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 138 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 139 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 140 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 141 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 142 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 143 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 144 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 145 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 146 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 147 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 148 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 149 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 150 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 151 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 152 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 153 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 154 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 155 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 156 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 157 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 158 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 159 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 160 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 161 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 162 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 163 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 164 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 165 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 166 | github.com/slack-go/slack v0.9.4 h1:C+FC3zLxLxUTQjDy2RZeMHYon005zsCROiZNWVo+opQ= 167 | github.com/slack-go/slack v0.9.4/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= 168 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 169 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 170 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 171 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 172 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 173 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 174 | github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= 175 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 176 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 177 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 178 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 179 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 180 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 181 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 182 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 183 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 184 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 185 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 186 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 187 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 188 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 189 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 190 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 191 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 192 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 193 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 194 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 195 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 196 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 197 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 198 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 199 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 200 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 201 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 202 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 203 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 204 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 205 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 206 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 207 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 208 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 209 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 210 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 211 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 212 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 213 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 214 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 215 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 216 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 217 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 218 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 219 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 220 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 221 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 222 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 223 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 224 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 225 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 226 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 227 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 228 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 229 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 230 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 231 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 232 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 233 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 234 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 235 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 236 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 237 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 238 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 239 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 240 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 244 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 245 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 246 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 247 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 249 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 250 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 257 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 259 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 260 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 261 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 262 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 263 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 264 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 265 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 266 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 267 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 268 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 269 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 270 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 271 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 272 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 273 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 274 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 275 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 276 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 277 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 278 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 279 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 280 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 281 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 282 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 283 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 284 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 285 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 286 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 287 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 288 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 289 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 290 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 291 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 292 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 293 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 294 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 295 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 296 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 297 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 298 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 299 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 300 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 301 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 302 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 303 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 304 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 305 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 306 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 307 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 308 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 309 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 310 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 311 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 312 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 313 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 314 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 315 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 316 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 317 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 318 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 319 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 320 | --------------------------------------------------------------------------------