Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: 添加多模板能力,以适应不同告警使用不同模板场景。 #12

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,25 @@ docker run --name a2w -d -p 5001:5001 -e TZ=Asia/Tokyo rea1shane/a2w

### 消息模板

消息模板决定了企业微信机器人发出的消息格式,在启动 A2W 时通过 `--template` 指定模板。默认模板的使用说明见 [文档](https://github.com/rea1shane/a2w/blob/main/templates/base.md)。
消息模板决定了企业微信机器人发出的消息格式,在启动 A2W 时通过 `--template` 指定模板文件所在目录。默认模板的使用说明见 [文档](https://github.com/rea1shane/a2w/blob/main/templates/base.md)。

> [!NOTE]
>
> 因为企业微信机器人接口限制单条消息的最大长度为 4096,所以本软件会对大于此限制的长消息进行分段。如果你使用自定义模板,请在想要分段的地方留一个空行(在企业微信中,至少三个连续的 `\n` 才被认为是一个空行),以便本软件对消息进行正确的分段。

使用 tmpl URL Query 可以指定要使用的其他模板(以便适应想要让不同的告警使用不同的模板场景):

```yaml
receivers:
- name: "a2w"
webhook_configs:
- url: "http://{a2w_address}/send?tmpl=base_two&key={key}"
```

> [!Attention]
>
> 模板文件必须以 .tmpl 作为后缀,前缀要符合 URL 字符规范 [RFC 3986, Uniform Resource Identifier(URI): Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986.html)。不要使用中文作为文件名。

### 用户提醒

A2W 支持用户提醒功能,修改 Alertmanager 中的配置如下:
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.0
0.2.1
38 changes: 27 additions & 11 deletions a2w.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"path/filepath"
"strconv"
"strings"
"text/template"
Expand Down Expand Up @@ -53,15 +53,17 @@ const (
)

var (
tmplPath, tmplName string
logger *logrus.Logger
tmplDir, tmplName string
// key: 模板文件名称; value: 模板文件路径
tmplFiles map[string]string = make(map[string]string)
logger *logrus.Logger
)

func main() {
// 解析命令行参数
logLevel := flag.String("log-level", "info", "日志级别。可选值:debug, info, warn, error")
addr := flag.String("addr", ":5001", "监听地址。格式: [host]:port")
flag.StringVar(&tmplPath, "template", "./templates/base.tmpl", "模板文件路径。")
flag.StringVar(&tmplDir, "template", "./templates", "模板文件所在目录。")
flag.Parse()

// 解析日志级别
Expand All @@ -70,9 +72,16 @@ func main() {
logrus.Panicf("日志级别解析失败: %s", *logLevel)
}

// 解析模板文件名称
split := strings.Split(tmplPath, "/")
tmplName = split[len(split)-1]
// 解析模板文件名称,获取所有后缀为 .tmpl 的文件
files, err := filepath.Glob(filepath.Join(tmplDir, "*.tmpl"))
if err != nil || len(files) == 0 {
logrus.Fatalf("无法从 %s 目录获取到模板文件: %v", tmplDir, err)
}
for _, file := range files {
split := strings.Split(file, "/")
tmplName := split[len(split)-1]
tmplFiles[tmplName] = file
}

// 创建 logger
logger = logrus.New()
Expand Down Expand Up @@ -100,7 +109,14 @@ func health(c *gin.Context) {
func send(c *gin.Context) {
// 获取 bot key
key := c.Query("key")

// 获取模板名称
tmplNamePrefix := c.Query("tmpl")
if tmplNamePrefix == "" {
tmplName = "base.tmpl"
} else {
tmplName = fmt.Sprintf("%v.tmpl", tmplNamePrefix)
}
logrus.Debugf("将要使用的模板: %v", tmplFiles[tmplName])
// 获取提醒列表
mentions, exist := c.GetQueryArray("mention")
var mentionsBuilder strings.Builder
Expand Down Expand Up @@ -138,7 +154,7 @@ func send(c *gin.Context) {
tfm["timeFormat"] = timeFormat
tfm["timeDuration"] = timeDuration
tfm["timeFromNow"] = timeFromNow
tmpl := template.Must(template.New(tmplName).Funcs(tfm).ParseFiles(tmplPath))
tmpl := template.Must(template.New(tmplName).Funcs(tfm).ParseFiles(tmplFiles[tmplName]))
var content bytes.Buffer
if err := tmpl.Execute(&content, notification); err != nil {
e := c.Error(err)
Expand Down Expand Up @@ -169,7 +185,7 @@ func send(c *gin.Context) {
for _, fragment := range fragments {
// 切割后的单条消息都过长
if len(fragment)+len(emptyLine) > snippetMaxLen {
e := c.Error(errors.New(fmt.Sprintf("切割后的消息长度 %d 仍超出片段长度限制 %d", len(fragment), snippetMaxLen-len(emptyLine))))
e := c.Error(fmt.Errorf("切割后的消息长度 %d 仍超出片段长度限制 %d", len(fragment), snippetMaxLen-len(emptyLine)))
e.Meta = "分段消息失败"
c.Writer.WriteHeader(http.StatusBadRequest)
return
Expand Down Expand Up @@ -223,7 +239,7 @@ func send(c *gin.Context) {
wecomRespBody, _ := io.ReadAll(wecomResp.Body)
wecomResp.Body.Close()
if wecomResp.StatusCode != http.StatusOK || string(wecomRespBody) != okMsg {
e := c.Error(errors.New(string(wecomRespBody)))
e := c.Error(fmt.Errorf(string(wecomRespBody)))
e.Meta = "请求企业微信失败,HTTP Code: " + strconv.Itoa(wecomResp.StatusCode)
c.Writer.WriteHeader(http.StatusInternalServerError)
return
Expand Down
24 changes: 24 additions & 0 deletions templates/base_two.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
二号模板
{{ range $i, $alert := .Alerts }}

{{- if eq $alert.Status "firing" }}
<font color="warning">**[firing] {{ $alert.Labels.alertname }}**</font>
{{- if $alert.Labels.level }}
**警报等级**: {{ $alert.Labels.level }}
{{- end }}
**触发时间**: {{ timeFormat ($alert.StartsAt) }}
**持续时长**: {{ timeFromNow ($alert.StartsAt) }}
{{- if $alert.Annotations.current }}
**当前状态**: {{ $alert.Annotations.current }}
{{- end }}
{{- else if eq $alert.Status "resolved"}}
<font color="info">**[resolved] {{ $alert.Labels.alertname }}**</font>
**触发时间**: {{ timeFormat ($alert.StartsAt) }}
**恢复时间**: {{ timeFormat ($alert.EndsAt) }}
**持续时长**: {{ timeDuration ($alert.StartsAt) ($alert.EndsAt) }}
{{- end }}
{{- if $alert.Annotations.labels }}
**标签列表**: {{ $alert.Annotations.labels }}
{{- end }}

{{ end }}