91 lines
2.9 KiB
Go
91 lines
2.9 KiB
Go
package render
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Alert 描述一条运维告警。
|
|
type Alert struct {
|
|
Environment string
|
|
// Title 是卡片标题,如「归档目录不可用」。
|
|
Title string
|
|
// Summary 是一句人话结论,如「正在重启以重新挂载」。
|
|
Summary string
|
|
// Reason 是技术细节(英文错误原样保留,便于排查)。
|
|
Reason string
|
|
// ExitCount 为累计退出次数,>1 时提示可能反复出现。
|
|
ExitCount int
|
|
// Path 是涉及的路径。
|
|
Path string
|
|
}
|
|
|
|
// AlertCard 把告警渲染成飞书交互卡片。
|
|
//
|
|
// 为什么不用纯文本:text 消息在飞书里渲染成灰底等宽块,长错误串会把气泡撑得很宽,
|
|
// 而且和同步结果卡片(interactive)风格不一致。卡片可以带红色 header、分区字段,
|
|
// 一眼能看出是「出事了」而不是一条普通消息。
|
|
func AlertCard(a Alert) ([]byte, error) {
|
|
title := fmt.Sprintf("%s · %s", displayEnvironment(a.Environment), a.Title)
|
|
|
|
fields := []any{}
|
|
if a.Summary != "" {
|
|
fields = append(fields, map[string]any{
|
|
"tag": "div",
|
|
"text": map[string]any{"tag": "lark_md", "content": "**" + a.Summary + "**"},
|
|
})
|
|
}
|
|
if a.Path != "" {
|
|
fields = append(fields, map[string]any{
|
|
"tag": "div",
|
|
"text": map[string]any{"tag": "lark_md", "content": "**路径**\n`" + a.Path + "`"},
|
|
})
|
|
}
|
|
if a.Reason != "" {
|
|
fields = append(fields, map[string]any{
|
|
"tag": "div",
|
|
"text": map[string]any{"tag": "lark_md", "content": "**原因**\n" + a.Reason},
|
|
})
|
|
}
|
|
if a.ExitCount > 1 {
|
|
fields = append(fields, map[string]any{
|
|
"tag": "div",
|
|
"text": map[string]any{"tag": "lark_md", "content": fmt.Sprintf("**已重复 %d 次**,若持续出现请检查宿主机上该目录是否被反复删除。", a.ExitCount)},
|
|
})
|
|
}
|
|
fields = append(fields, map[string]any{"tag": "hr"})
|
|
fields = append(fields, map[string]any{
|
|
"tag": "note",
|
|
"elements": []any{
|
|
map[string]any{"tag": "plain_text", "content": "iotlcm-bot 自动检测 · 恢复后不再重复提醒"},
|
|
},
|
|
})
|
|
|
|
card := map[string]any{
|
|
"msg_type": "interactive",
|
|
"card": map[string]any{
|
|
// red 让告警在消息流里一眼可辨,与结果卡片的 blue 区分开。
|
|
"header": map[string]any{"template": "red", "title": map[string]any{"tag": "plain_text", "content": title}},
|
|
"elements": fields,
|
|
},
|
|
}
|
|
return json.Marshal(card)
|
|
}
|
|
|
|
// FriendlyReason 把常见的技术错误翻成人话,保留原文以便排查。
|
|
func FriendlyReason(reason string) string {
|
|
switch {
|
|
case strings.Contains(reason, "deleted on the host"):
|
|
return "宿主机上的归档目录已被删除,容器仍指向旧目录(挂载失效)。"
|
|
case strings.Contains(reason, "no such file or directory"):
|
|
return "目录不存在。"
|
|
case strings.Contains(reason, "permission denied"):
|
|
return "没有读取权限。"
|
|
case strings.Contains(reason, "not a directory"):
|
|
return "该路径不是目录。"
|
|
default:
|
|
return reason
|
|
}
|
|
}
|