Files
iotlcm/bot/internal/render/alert.go
T

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
}
}