n9e-generate-message-template

star 13.1k

生成或修改夜莺(n9e)告警通知消息模板。当用户要求写通知模板、改消息格式、加主机名/恢复值/级别、钉钉/飞书/Lark/邮件/短信/电话模板时使用。

ccfos By ccfos schedule Updated 6/3/2026

name: n9e-generate-message-template description: 生成或修改夜莺(n9e)告警通知消息模板。当用户要求写通知模板、改消息格式、加主机名/恢复值/级别、钉钉/飞书/Lark/邮件/短信/电话模板时使用。 tags: - internal

夜莺(n9e) 通知消息模板生成

夜莺的消息模板是 Go text/template / html/template 语法(邮件走 text/template,其他走 html/template 再做转义)。用户在「通知管理 → 消息模板」页面编辑,保存后被通知规则引用,触发告警时按渲染数据替换变量后发送到各通道。

本技能专注于写/改模板片段本身,不涉及创建通知规则或通道配置。


渲染上下文

后端执行时传入的 renderData

key 类型 说明
.events []*AlertCurEvent 当前批次的告警事件列表,通常只有 1 条
.domain string n9e 站点 URL,用于拼跳转链接

自动注入的简写(不用手写):

{{ $events := .events }}
{{ $event := index $events 0 }}
{{ $labels := $event.TagsMap }}
{{ $value := $event.TriggerValue }}

所以模板里可以直接使用 $event.xxx$labels.agent_hostname$value


$event 可用字段(*AlertCurEvent

常用字段

字段 类型 说明
.Id int64 告警事件 ID(拼跳转链用)
.RuleId int64 告警规则 ID
.RuleName string 规则名称
.RuleNote string 规则备注
.Severity int 告警级别:1=Critical, 2=Warning, 3=Info
.PromQl string 告警触发表达式
.RuleAlgo string 规则算法类型
.TriggerTime int64 触发时间 unix 秒
.TriggerValue string 触发时的指标值(已为字符串)
.FirstTriggerTime int64 首次异常时间(连续告警)
.LastEvalTime int64 最近一次评估时间,恢复时作为恢复时间使用
.IsRecovered bool 是否已恢复
.NotifyCurNumber int 本次通知是第几次发送
.TargetIdent string 监控对象(通常是 agent_hostname)
.TargetNote string 对象备注
.GroupId / .GroupName int64 / string 业务组
.Cluster string 数据源集群名
.Cate string 数据源类型(prometheus / host / mysql / ...)
.RunbookUrl string 运行手册链接

标签 / 注解 / 触发值对象

字段 类型 说明
.TagsMap map[string]string 事件标签,优先用:{{$labels.agent_hostname}}
.TagsJSON []string 形如 ["k=v", ...] 的字符串数组(早期模板常用)
.AnnotationsJSON map[string]string 注解,常用 .AnnotationsJSON.recovery_value
.TriggerValuesJson.ValuesWithUnit map 带单位的触发值(多值场景)

通知对象

字段 类型 说明
.NotifyUsersObj []*User 本次要通知的用户对象(有 Username/Nickname/Phone/Email)
.NotifyGroupsObj []*UserGroup 关联用户组

用户对象在钉钉/飞书/Lark 模板里常用来拼 at,见下文 ats 系列 helper。


可用 helper 函数(tplx.TemplateFuncMap

时间

  • timeformat <unix> — 默认 "2006-01-02 15:04:05";传第二个参数覆盖:{{timeformat $event.TriggerTime "15:04:05"}}
  • timestamp — 当前时间字符串
  • now.Unix — 当前 unix 秒(now 是 Go template 内置)
  • humanizeDuration <sec>"3m15s"
  • humanizeDurationInterface <interface> — 同上但接受 interface
  • toTime <unix> — 返回 time.Time,可链式调用
  • parseDuration <"5m"> — 返回 time.Duration

数值 / 格式

  • formatDecimal <v> <n> — 保留 n 位小数({{formatDecimal $event.TriggerValue 2}}
  • humanize <v> — K/M/G 单位(1000 进制,SI)
  • humanize1024 <v> — Ki/Mi/Gi(1024 进制)
  • humanizePercentage <v> / humanizePercentageH <v> — 百分比
  • add / sub / mul / div <a> <b> — 四则
  • printf "%.2f" <v> — Go fmt.Sprintf

字符串

  • toUpper / toLower / title — 大小写
  • contains <s> <sub> — 子串
  • match <regex> <s> — 正则匹配(bool)
  • reReplaceAll <regex> <repl> <s> — 正则替换
  • split <s> <sep> / join <slice> <sep>
  • stripPort <host:port> / stripDomain <host.domain>
  • b64enc / b64dec — base64

链接 / 转义

  • escape <s> — URL 路径转义
  • unescaped <s> — 输出 raw HTML(不转义)
  • safeHtml <s> — 同上
  • urlconvert <s>

标签 / 触发值

  • label <key> <labelMap> / value <key> <m> / strvalue <v>
  • first <slice> — 取第一个
  • tagsMapToStr <map> — 标签拼成 k=v,k=v
  • sortByLabel <items> <key> — 按标签排序

@人(钉钉/飞书/Lark 专用)

  • ats <users> <platform> — 生成 at 片段
  • batchContactsAts <contacts> <platform> — 批量 at
  • batchContactsAtsInFeishuEmail <contacts> / batchContactsAtsInFeishuId <contacts> — 飞书专用
  • batchContactsJoinComma <contacts> / batchContactsJsonMarshal <contacts>
  • mappingAndJoin <map> <kvSep> <itemSep>

其它

  • jsonMarshal <v> — 序列化为 JSON 字符串
  • mapDifference <a> <b> — 集合差

各通道(notify_channel_ident)语法差异

消息模板绑在具体通道上,通道 ident 决定文本引擎和转义行为:

Ident 引擎 说明
email text/template 不再次转义,HTML 模板直接写标签
slackwebhook / slackbot html/template 渲染后把 " / \n 转义并包成 template.HTML;通常写 Markdown
其它(dingtalk / feishu / feishucard / larkcard / wecom / tx-sms / ali-voice …) html/template 渲染后对 " \n \r 做 JSON 字符串转义,适合直接塞入 webhook payload

重要:html/template 会对 < > & 自动转义。不想转义的地方要用 {{unescaped "…"}}{{safeHtml .X}},否则钉钉里渲染出来会看到字面 &lt;


写模板的工作流

  1. 确认通道 ident:用户说"钉钉"就是 dingtalk,"飞书卡片"是 feishucard,"邮件"是 email。不同通道风格差异大。
  2. 判断是否需要渲染恢复状态:默认要分 $event.IsRecovered 两个分支。只有短信/语音可以省略。
  3. 挑字段
    • 标签优先用 $labels.<key>,而不是 $event.TagsJSON
    • 触发值用 $event.TriggerValue;要两位小数就 {{formatDecimal $event.TriggerValue 2}}
    • 时间戳一律过 timeformat
    • 跳转链接拼 {{.domain}}/share/alert-his-events/{{$event.Id}}
  4. 级别显示
    • 数字:{{$event.Severity}}
    • 中文:{{if eq $event.Severity 1}}一级{{else if eq $event.Severity 2}}二级{{else}}三级{{end}}
    • 英文:Critical / Warning / Info
  5. @人(钉钉/飞书)
    • 钉钉 @全员:在模板末尾加一行 @all(钉钉按空格分词匹配)。精确 at 手机号:{{range $event.NotifyUsersObj}}@{{.Phone}} {{end}}
    • 飞书卡片:用 {{batchContactsAtsInFeishuEmail $event.NotifyUsersObj}} 拼出 <at email=...></at> 片段。
  6. 输出:用 markdown 代码块 gotemplate ``` 包裹模板主体,末尾给一段变量/函数说明,每个非平凡变量/函数单独一行。

输出格式

回复给用户时:

  1. 一到两句导语说明模板用途、适配哪个通道。
  2. <模板内容>
    
  3. **变量说明**:列出模板里用到的非平凡字段和函数($event.TargetIdentformatDecimaltimeformat 之类),每项一行。
  4. 如果用户的需求有歧义(比如"加主机名"——到底是 target_ident 还是某个 tag),在导语里直接点出做了什么假设,不要反问。

语言跟随用户输入(中文输入就用中文)。


内置参考模板(改造起点)

钉钉 markdown(完整版)

#### {{if $event.IsRecovered}}<font color="#008800">💚{{$event.RuleName}}</font>{{else}}<font color="#FF0000">💔{{$event.RuleName}}</font>{{end}}
---
{{$duration := sub now.Unix $event.FirstTriggerTime}}{{if $event.IsRecovered}}{{$duration = sub $event.LastEvalTime $event.FirstTriggerTime}}{{end}}
- **告警级别**: S{{$event.Severity}}
{{- if $event.RuleNote}}
- **规则备注**: {{$event.RuleNote}}
{{- end}}
{{- if $event.TargetIdent}}
- **监控对象**: {{$event.TargetIdent}}
{{- end}}
{{- if not $event.IsRecovered}}
- **触发时值**: {{$event.TriggerValue}}
- **触发时间**: {{timeformat $event.TriggerTime}}
- **持续时长**: {{humanizeDurationInterface $duration}}
{{- else}}
- **恢复时间**: {{timeformat $event.LastEvalTime}}
- **持续时长**: {{humanizeDurationInterface $duration}}
{{- end}}
- **事件标签**:
{{- range $k, $v := $labels}}
{{- if ne $k "rulename"}}
    - {{$k}}: {{$v}}
{{- end}}
{{- end}}
[事件详情]({{.domain}}/share/alert-his-events/{{$event.Id}}) | [屏蔽1小时]({{.domain}}/alert-mutes/add?__event_id={{$event.Id}})

飞书卡片(简洁版)

{{- if $event.IsRecovered}}
**级别状态:** S{{$event.Severity}} Recovered
**告警名称:** {{$event.RuleName}}
**事件标签:** {{$event.TagsJSON}}
**恢复时间:** {{timeformat $event.LastEvalTime}}
{{- else}}
**级别状态:** S{{$event.Severity}} Triggered
**告警名称:** {{$event.RuleName}}
**事件标签:** {{$event.TagsJSON}}
**触发时间:** {{timeformat $event.TriggerTime}}
**触发时值:** {{$event.TriggerValue}}
{{- if $event.RuleNote}}
**告警描述:** {{$event.RuleNote}}
{{- end}}
{{- end}}

短信 / 语音(极简)

级别状态: S{{$event.Severity}} {{if $event.IsRecovered}}Recovered{{else}}Triggered{{end}} 规则: {{$event.RuleName}} 对象: {{$event.TargetIdent}}

典型改造场景

1) "在钉钉模板里加主机名"

- **主机**: {{$event.TargetIdent}}
{{- if $labels.ip}}
- **IP**: {{$labels.ip}}
{{- end}}

说明:target_ident 一般就是主机名;如果需要 IP,优先看标签 ipinstancehost

2) "把 trigger_value 保留两位小数"

- **触发时值**: {{formatDecimal $event.TriggerValue 2}}

TriggerValue 本身是字符串,formatDecimal 会先转浮点再格式化,非数字会原样返回。

3) "钉钉模板末尾 @ 告警接收人"

...模板正文...

{{- range $event.NotifyUsersObj}}@{{.Phone}} {{end}}

钉钉按 "空格 + 手机号" 识别被 at 的用户。或者统一 @all

4) "恢复时显示恢复时的值"

{{- if $event.IsRecovered}}
{{- if $event.AnnotationsJSON.recovery_value}}
- **恢复时值**: {{formatDecimal $event.AnnotationsJSON.recovery_value 4}}
{{- end}}
- **恢复时间**: {{timeformat $event.LastEvalTime}}
{{- end}}

恢复值由告警引擎在恢复时写入 AnnotationsJSON.recovery_value仅在有恢复值的情况下存在,用 if 保护。

5) "告警级别用中文"

- **级别**: {{if eq $event.Severity 1}}一级(紧急){{else if eq $event.Severity 2}}二级(重要){{else}}三级(提示){{end}}

6) "只发生产环境告警相关机器"

模板本身不做过滤——过滤应放在通知规则attributes / label_keys。模板里只负责展示。如果用户问到这里要提示一下。

7) "按状态/级别切色"

钉钉/邮件用 <font color>,飞书卡片靠 template 字段填颜色 keyword:

{{/* 钉钉 markdown:标题前置 emoji + 颜色 */}}
#### {{if $event.IsRecovered}}<font color="#008800">✅ {{$event.RuleName}}</font>{{else}}<font color="#FF0000">🚨 {{$event.RuleName}}</font>{{end}}

{{/* 飞书 feishucard 的 template 字段(决定卡片头部色块) */}}
{{if $event.IsRecovered}}turquoise{{else}}{{if eq $event.Severity 1}}red{{else if eq $event.Severity 2}}orange{{else}}grey{{end}}{{end}}

飞书卡片只认枚举色:red / orange / yellow / green / turquoise / blue / indigo / purple / carmine / grey,hex 颜色码会被忽略。

8) "显示告警持续时间"

{{$duration := sub now.Unix $event.FirstTriggerTime}}
{{- if $event.IsRecovered}}{{$duration = sub $event.LastEvalTime $event.FirstTriggerTime}}{{end}}
- **持续**: {{humanizeDurationInterface $duration}}

触发态:当前时间 - FirstTriggerTime;恢复态:LastEvalTime - FirstTriggerTimehumanizeDurationInterface 输出 1h3m5s 这种人类可读形式。

9) "URL 中嵌入 tag 值,避免空格/特殊字符破坏链接"

[查看仪表盘]({{.domain}}/dashboards/123?ident={{urlquery $event.TargetIdent}}&host={{urlquery (index $labels "host")}})
[屏蔽1小时]({{.domain}}/alert-mutes/add?__event_id={{$event.Id}})

直接 {{$event.TargetIdent}} 拼进 URL,遇到空格、中文或 & 会被 IM 端二次转义(典型表现:&&amp;,链接打不开)。所有进入 URL query 的变量都用 urlquery

10) "标签 key 含中划线/点/中文 — 用 index 取"

- **应用**: {{index $labels "app-name"}}
- **K8s 域**: {{index $labels "k8s.io/cluster"}}
- **业务面板**: {{index $event.AnnotationsJSON "dashboard_url"}}

$labels.app-name 会被 Go template 解析成"appname"必然失败。Key 只要不是纯字母数字下划线,一律 index 取,Annotations 同理。

11) "数值异常(+Inf / NaN)兜底"

- **触发时值**: {{if or (eq $event.TriggerValue "+Inf") (eq $event.TriggerValue "NaN")}}N/A{{else}}{{formatDecimal $event.TriggerValue 2}}{{end}}

PromQL 的 /0 会返回 +Inf,缺数据的聚合会返回 NaN。直接渲染到飞书/钉钉里就是字面 +Inf,看着像 bug。

12) "Edge 模式下事件 Id=0 — 跳转链接降级"

{{if gt $event.Id 0}}
[事件详情]({{.domain}}/share/alert-his-events/{{$event.Id}})
{{else}}
(Edge 模式事件,请到中心端查看详情)
{{end}}

已知问题:Edge 模式下事件 Id 写入是异步的,渲染时拿到 0,跳转链接会落到 /alert-his-events/0(404)。模板层加 gt $event.Id 0 守护。

13) "多告警合并展示"

{{range $i, $e := .events}}
{{- if $i}}
---
{{end -}}
- 规则:{{$e.RuleName}}
- 对象:{{$e.TargetIdent}}
- 触发值:{{$e.TriggerValue}}
- 时间:{{timeformat $e.TriggerTime}}
{{end}}

默认场景下一条通知就一个事件,模板按 $event = index .events 0 渲染。当一条通知聚合了多个事件(订阅聚合、批量发送)时必须 range .events 才能展开。


关键注意事项

  1. html/template 会 HTML 转义。钉钉/飞书/Lark 里 <font><at> 等标签必须用 unescaped 包裹或放在内容里由 Go template 自己识别。邮件走 text/template 不受此限。
  2. TriggerValue 是字符串:直接比较大小要用 parseDuration/自定义,常规只做显示就好。
  3. 不要手动写 {{$events := .events}} 等头部——系统会自动注入。
  4. 恢复分支不能漏NotifyRecovered=1 的规则会复用同一模板发恢复消息。
  5. 标签 key 里的中划线/点$labels.agent_hostname 能用,但 $labels.app-name 不行,要用 index $labels "app-name"
  6. 时间字段全部是 unix 秒int64),不要直接 {{$event.TriggerTime}} 当文本,用 timeformattoTime
  7. 多告警合并.events 可能有多条,钉钉/飞书默认模板只渲染第 0 条($event);如果用户要批量展示,用 {{range .events}}
  8. 链接用 .domain:不要硬编码 http://localhost:17000,否则切环境就坏。

常见错误

  • {{$event.TriggerValue | printf "%.2f"}}printf 第一个参数才是 format,且 TriggerValue 是 string。

  • {{formatDecimal $event.TriggerValue 2}}

  • {{$event.TriggerTime}} 直接展示 — 会输出 unix 秒。

  • {{timeformat $event.TriggerTime}}

  • ❌ 钉钉里写 <font color="red"> 不转义 — html/template 会转义成 &lt;font&gt;

  • ✅ 放在条件分支里(Go template 的 {{if}} 分支字面量不会被 HTML 转义内部标签),或整体 {{unescaped "…"}}。夜莺官方钉钉模板直接写 <font> 能生效,是因为外层是纯文本区段(不在属性值里)——遵循官方样例即可。

  • ❌ 忘记分恢复分支 — 恢复通知里触发时间误导人。

  • ✅ 所有动态内容先用 {{if $event.IsRecovered}}…{{else}}…{{end}} 包好。

  • {{if .IsRecoverd}} — 拼写少一个 e。Go template 对不存在的字段静默走 else 分支(不报错),后果是恢复通知发的是触发态文案,且很难定位。

  • {{if $event.IsRecovered}}。同类拼写陷阱:Resoverd Recoverd recovered(小写)都不行。

  • {{if lt $value 10}}{{if gt $event.TriggerValue 80}}TriggerValue 是 string,lt/gt 跟数字比较会报 incompatible types,或被解释成字符串字典序得到错误结果。当前版本没有内置 toFloat helper。数值阈值判断请放回告警规则的条件表达式里,模板层只做展示。

  • ❌ 拼 URL 时直接 ?ident={{$event.TargetIdent}} — TargetIdent 含空格/中文/& 会破坏链接。

  • ?ident={{urlquery $event.TargetIdent}}所有进入 URL query 的变量都过 urlquery

  • {{$labels.app-name}}{{$labels.k8s.io/cluster}} — 含中划线/点/斜杠的 key 会被解析失败。

  • {{index $labels "app-name"}}{{index $labels "k8s.io/cluster"}}

Install via CLI
npx skills add https://github.com/ccfos/nightingale --skill n9e-generate-message-template
Repository Details
star Stars 13,092
call_split Forks 1,726
navigation Branch main
article Path SKILL.md
More from Creator