OneUptime 监控告警模板引擎:用{{variable}}占位符动态生成 Incident 与 Alert
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
当监控条件(Criteria)命中并自动创建 Incident(事件)或 Alert(告警)时,如果标题、描述与修复备注(Remediation Notes)是固定文案,排查效率会大打折扣——你需要在几千条告警里逐条打开才能知道哪台机器、哪个接口、什么状态码出了问题。OneUptime 提供了一套与监控 Criteria 中 JavaScript 表达式同源的{{variable}}占位符模板语法,在事件/告警自动创建时动态填充真实的监控数据(响应码、响应时间、CPU 使用率、SSL 证书信息、OID 值等)。读完本文,你将掌握每种监控类型可用的模板变量、基础与进阶模板写法(含{{#each}}数组循环)、以及模板底层的存储映射与渲染实现原理。
模板语法概览
OneUptime 的模板语法复用了监控 Criteria 中 JavaScript 表达式所使用的{{variable}}占位符标记。在监控条件实例(Criteria Instance)内的Incident/Alerta 表单中,你可以把标题(Title)、描述(Description)和修复备注(Remediation Notes)写成模板字符串,当条件触发并自动创建事件/告警时,系统会以该次监控的真实结果填充占位符。
从源码结构看,这一能力由 Common/Server/Utils/Monitor/MonitorTemplateUtil.ts 的processTemplateString承载,其内部调用 Common/Server/Utils/VM/VMAPI.ts 的replaceValueInPlace完成替换:
buildTemplateStorageMap根据监控类型,把监控响应数据组装成一个扁平/嵌套的存储映射(Storage Map,JSON 对象),例如 API 监控得到responseBody、responseStatusCode、responseTimeInMs等键;processTemplateString把模板字符串与存储映射交给VMUtil.replaceValueInPlace,用正则/{{(.*?)}}/g逐个匹配占位符,通过deepFind按点分路径取值并回填。
Incident 侧的实际调用位于 Common/Server/Utils/Monitor/MonitorIncident.ts,它先buildTemplateStorageMap生成存储映射,再对criteriaIncident.title、criteriaIncident.description执行processTemplateString,最后由SeriesContextEnricher补充序列标签信息后落库。Alert 侧的渲染逻辑与 Incident 完全对齐(源码注释明确指出二者必须一致,否则同一告警会因触发对象不同而呈现出不同描述)。
支持模板的监控类型与变量全集
以下监控类型支持动态模板,各自暴露不同的变量集合:
- 网站与 API 监控:响应数据、响应头、状态码、响应时间
- 请求入站(Incoming Request)监控:请求数据、请求头、请求方法、时间
- Ping 监控:连通性状态、响应时间、失败原因
- 端口监控:端口连通性、响应时间、超时状态
- IP 监控:IP 可达性、Ping 时间、失败信息
- SSL 证书监控:证书详情、校验状态、过期信息
- 服务器/VM 监控:系统指标(CPU、内存、磁盘)、进程、主机名
- 合成(Synthetic)监控:脚本执行结果、截图、浏览器详情
- 自定义 JavaScript 代码监控:执行结果、耗时、错误消息
- SNMP 监控:设备状态、响应时间、OID 值
注意:日志(Logs)、链路(Traces)与指标(Metrics)监控目前不支持Incident/Alert 模板,因为它们使用不同的触发机制。
网站与 API 监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
responseBody | 响应体对象;HTML/XML 时为字符串,JSON 时为 JSON 对象 | string或JSON |
responseHeaders | 响应头对象(键为小写) | Dictionary<string> |
responseStatusCode | 响应的 HTTP 状态码 | number |
responseTimeInMs | 响应时间(毫秒) | number |
isOnline | 监控是否被认为在线 | boolean |
对应的存储映射构建逻辑见 MonitorTemplateUtil.ts:其中responseBody会优先尝试JSON.parse解析为对象,解析失败则保留原始字符串;responseStatusCode取自responseCode字段。
请求入站(Incoming Request)监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
requestBody | 请求体对象 | string或JSON |
requestHeaders | 请求头对象(键为小写) | Dictionary<string> |
requestMethod | 入站请求的 HTTP 方法(GET、POST 等) | string |
incomingRequestReceivedAt | 收到入站请求的日期时间 | Date |
按负载字段分组(Group incidents and alerts by a payload field):当 Criteria 启用了按负载字段分组的选项后,提取出的分组键也会作为模板变量可用,变量名取自分组路径的最后一个段。例如按requestBody.alerts[*].labels.alertname分组,你会得到{{alertname}};按requestBody.alerts[*].fingerprint分组,会得到{{fingerprint}}。完整requestBody仍然可用。
注意:
[*]只在分组路径字段本身中可理解——这里不会解析它,因此该标记会被原样打印(含花括号)。在标题或描述中,{{requestBody.alerts[0].annotations.summary}}始终读取负载中的第一条告警,而非为它创建 Incident 的那一条。请改用分组变量以及负载的共享字段(commonLabels、commonAnnotations)。可参考 入站请求监控文档。
Ping 监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
isOnline | Ping 目标是否被认为在线 | boolean |
responseTimeInMs | Ping 响应时间(毫秒) | number |
failureCause | Ping 失败的原因 | string |
isTimeout | Ping 请求是否超时 | boolean |
端口监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
isOnline | 端口是否在线/可访问 | boolean |
responseTimeInMs | 连接响应时间(毫秒) | number |
failureCause | 端口检查失败的原因 | string |
isTimeout | 端口连接是否超时 | boolean |
IP 监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
isOnline | IP 地址是否被认为在线 | boolean |
responseTimeInMs | Ping 响应时间(毫秒) | number |
failureCause | IP 检查失败的原因 | string |
isTimeout | IP Ping 请求是否超时 | boolean |
SSL 证书监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
isOnline | SSL 证书校验是否成功 | boolean |
isSelfSigned | 证书是否自签名 | boolean |
createdAt | 证书创建日期 | Date |
expiresAt | 证书过期日期 | Date |
commonName | 证书通用名(CN) | string |
organizationalUnit | 组织单位(OU) | string |
organization | 组织(O) | string |
locality | 地点(L) | string |
state | 州/省(ST) | string |
country | 国家(C) | string |
serialNumber | 证书序列号 | string |
fingerprint | 证书 SHA-1 指纹 | string |
fingerprint256 | 证书 SHA-256 指纹 | string |
failureCause | SSL 校验失败的原因 | string |
这些字段直接取自探针返回的sslResponse对象(见 MonitorTemplateUtil.ts)。
服务器/VM 监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
hostname | 被监控服务器的主机名 | string |
requestReceivedAt | 收到服务器监控请求的日期时间 | Date |
cpuUsagePercent | CPU 使用率(百分比) | number |
cpuCores | CPU 核心数 | number |
memoryUsagePercent | 内存使用率(百分比) | number |
memoryFreePercent | 空闲内存百分比 | number |
memoryTotalBytes | 总内存(字节) | number |
diskMetrics | 所有挂载磁盘的磁盘指标数组 | Array<Object> |
diskMetrics[].diskPath | 磁盘挂载点路径 | string |
diskMetrics[].usagePercent | 该挂载点磁盘使用率 | number |
diskMetrics[].freePercent | 该挂载点磁盘空闲率 | number |
diskMetrics[].totalBytes | 该挂载点磁盘总空间(字节) | number |
processes | 服务器上运行的进程数组 | Array<Object> |
processes[].pid | 进程 ID | number |
processes[].name | 进程名称 | string |
processes[].command | 启动进程的命令 | string |
failureCause | 服务器检查失败的原因 | string |
CPU 与内存指标在basicInfrastructureMetrics.cpuMetrics、basicInfrastructureMetrics.memoryMetrics可用时才会被注入存储映射(见 MonitorTemplateUtil.ts),磁盘与进程数组同理。
合成(Synthetic)监控变量
合成监控会在多个浏览器(Chromium、Firefox、Webkit)与多种屏幕尺寸(移动端、平板、桌面)上运行同一个脚本,每种组合产生一条响应。每次执行通过syntheticResponses数组暴露;你可以用索引访问某次特定执行({{syntheticResponses[0].browserType}}),或用{{#each syntheticResponses}}迭代。
| 变量 | 说明 | 类型 |
|---|---|---|
failureCause | 合成检查失败的原因 | string |
syntheticResponses | 每条浏览器/屏幕尺寸组合一条执行记录的数组 | Array<Object> |
syntheticResponses[].executionTimeInMs | 本次执行耗时(毫秒) | number |
syntheticResponses[].result | 本次执行返回的结果 | string、number、boolean或JSON |
syntheticResponses[].scriptError | 本次执行中发生的错误 | string |
syntheticResponses[].logMessages | 本次执行产生的日志消息 | Array<string> |
syntheticResponses[].screenshots | 本次执行中截取的截图 | Object |
syntheticResponses[].browserType | 本次执行使用的浏览器 | string |
syntheticResponses[].screenSizeType | 本次执行使用的屏幕尺寸 | string |
自定义 JavaScript 代码监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
executionTimeInMs | 自定义代码执行耗时(毫秒) | number |
result | 自定义代码返回的结果 | string、number、boolean或JSON |
scriptError | 代码执行中发生的错误 | string |
logMessages | 执行过程中产生的日志消息数组 | Array<string> |
SNMP 监控变量
| 变量 | 说明 | 类型 |
|---|---|---|
isOnline | SNMP 设备是否在线并响应 | boolean |
responseTimeInMs | SNMP 查询响应时间(毫秒) | number |
failureCause | SNMP 查询失败的原因 | string |
isTimeout | SNMP 查询是否超时 | boolean |
oidResponses | OID 响应对象数组(含 oid、name、value、type) | Array<Object> |
oidResponses[].oid | 被查询的 OID | string |
oidResponses[].name | OID 的描述性名称(若提供) | string |
oidResponses[].value | OID 返回的值 | string或number |
oidResponses[].type | 值的 SNMP 数据类型 | string |
{{OID_NAME}} | 按名称直接访问 OID 值(如{{sysUpTime}}) | string或number |
基础用法
在监控条件实例的Incident/Alert 表单中,你可以直接书写模板字符串。例如:
API devolvió {{responseStatusCode}} en {{responseTimeInMs}}ms如果监控响应状态码为502、耗时为842,则存储的标题会变成:
API devolvió 502 en 842ms嵌套 JSON 访问与 JavaScript 表达式一致:
ID del problema: {{responseBody.error.id}} Mensaje: {{responseBody.error.message}}数组索引同样受支持:
Primer usuario: {{responseBody.users[0].name}}未解析占位符的兜底行为:如果某条路径不存在,占位符会原样保留在输出中——{{responseBody.error.id}}会带着花括号逐字出现在 Incident 标题里。唯一例外是{{#each}}块:若其目标路径不存在,整个块会被删除。这与底层实现一致:VMUtil.replaceValueInPlace在deepFind找不到变量时直接continue跳过替换(见 VMAPI.ts),这是刻意的“静默失败”设计,避免因一个字段缺失而丢掉整条告警。
进阶用法
数组元素访问
Uso del primer disco: {{diskMetrics[0].usagePercent}}% Último proceso: {{processes[-1].name}}注意索引-1并不是传统“从尾部数”的语义——底层deepFind在解析[、]之间的内容时,只有字面量last会映射到数组最后一个元素(见 VMAPI.ts)。数组越界或目标不是数组时返回undefined,占位符保持原样。
嵌套对象访问
Mensaje de error: {{responseBody.error.details.message}} Ubicación del servidor: {{sslCertificate.locality}} {{sslCertificate.country}}deepFind会把路径按.切分逐级下钻,每个段先剥离[索引]后缀再取键;空段(如路径中出现连续两个点)会直接返回undefined。
使用{{#each}}迭代数组
你可以用块语法{{#each path}}...{{/each}}遍历数组,适合把列表中的每一项都写进 Incident/Alert 描述。
语法:
{{#each arrayPath}} ...cuerpo usando {{property}} de cada elemento... {{/each}}在循环体内:
{{propertyName}}相对于当前数组元素解析{{nested.property}}点分访问作用于当前元素{{@index}}解析为当前迭代的 0 基索引{{this}}解析为当前元素的值(对字符串/数字等原始类型数组很有用)- 在当前元素中找不到的变量,会回退到父级存储映射查找
这些行为与 VMAPI.ts 中expandEachLoops的实现一一对应:循环体以当前元素属性合并父级存储映射构造作用域后再递归展开,{{@index}}被替换为数字下标,原始类型数组则替换{{this}};若解析路径不是数组,整个块被替换为空字符串;每个循环最多迭代 100 次作为防死循环的安全上限。
示例:带告警数组的入站请求(如 Grafana webhook)
假设入站请求体如下:
{ "status": "firing", "alerts": [ { "status": "firing", "labels": { "label": "Coralpay" } }, { "status": "firing", "labels": { "label": "capitecpay" } }, { "status": "resolved", "labels": { "label": "capricorn" } } ] }可以写这样的模板:
Etiquetas de alerta: {{#each requestBody.alerts}} - {{labels.label}} ({{status}}) {{/each}}渲染结果:
Etiquetas de alerta: - Coralpay (firing) - capitecpay (firing) - capricorn (resolved)示例:服务器磁盘指标
Uso del disco: {{#each diskMetrics}} - {{diskPath}}: {{usagePercent}}% usado {{/each}}示例:使用{{@index}}
Procesos: {{#each processes}} {{@index}}. {{name}} (PID: {{pid}}) {{/each}}示例:原始类型数组配合{{this}}
Mensajes de registro: {{#each logMessages}} - {{this}} {{/each}}示例:嵌套循环
多级数组可以嵌套{{#each}}块:
{{#each requestBody.groups}} Grupo: {{name}} {{#each members}} - {{id}}: {{role}} {{/each}} {{/each}}注意:如果路径未解析为数组,整个
{{#each}}...{{/each}}块会从输出中删除;空数组不会为块产生任何输出。模板表达式的分类解析(#each开标签、/each闭标签、@index、this与其他引用)在 Common/Types/Workflow/TemplateSyntax.ts 中有完整的枚举与解析实现,可作为理解该语法的权威参考。
各监控类型实战示例
网站/API 监控 Incident 标题
Alta latencia: {{responseTimeInMs}}ms (> umbral)网站/API 监控 Incident 描述
### Error de API Estado: **{{responseStatusCode}}** Latencia: **{{responseTimeInMs}}ms** Fragmento del cuerpo: `{{responseBody.error.message}}`入站请求 Alert 标题
Solicitud entrante defectuosa: method={{requestMethod}} auth={{requestHeaders.authorization}}SSL 证书 Alert 标题
Certificado SSL a punto de expirar: {{commonName}} caduca {{expiresAt}}服务器监控 Alert 描述
### Alerta del servidor: {{hostname}} Uso de CPU: **{{cpuUsagePercent}}%** Uso de memoria: **{{memoryUsagePercent}}%** Uso del primer disco: **{{diskMetrics[0].usagePercent}}%** Última verificación: {{requestReceivedAt}}Ping 监控 Alert 标题
Ping fallido para el destino: {{failureCause}} ({{responseTimeInMs}}ms)端口监控 Alert 描述
Problema de conectividad del puerto Estado del puerto de destino: {{isOnline}} Tiempo de respuesta: {{responseTimeInMs}}ms Causa del fallo: {{failureCause}}合成监控 Alert
按索引访问特定浏览器/屏幕尺寸组合的执行:
Primera ejecución: {{syntheticResponses[0].browserType}} / {{syntheticResponses[0].screenSizeType}} Resultado: {{syntheticResponses[0].result}} en {{syntheticResponses[0].executionTimeInMs}}ms用{{#each}}迭代每种浏览器/屏幕尺寸组合:
### Resultados del monitor sintético {{#each syntheticResponses}} - **{{browserType}} / {{screenSizeType}}**: {{result}} en {{executionTimeInMs}}ms - Error del script: {{scriptError}} - Primer registro: {{logMessages[0]}} {{/each}}自定义代码监控 Alert
Ejecución del código personalizado: {{executionTimeInMs}}ms Salida del registro: {{logMessages[0]}}SNMP 监控 Alert 标题
Dispositivo SNMP fuera de línea: {{failureCause}} ({{responseTimeInMs}}ms)SNMP 监控 Alert 描述
### Alerta del dispositivo SNMP Estado: **{{isOnline}}** Tiempo de respuesta: **{{responseTimeInMs}}ms** Tiempo de actividad del sistema: {{sysUpTime}} Nombre del sistema: {{sysName}} Valor del primer OID: {{oidResponses[0].value}}入站请求 + 数组循环(Grafana webhook)
标题:
[{{requestBody.status}}] {{requestBody.receiver}}描述:
### Alertas de {{requestBody.receiver}} {{#each requestBody.alerts}} **Alerta {{@index}}**: {{labels.alertname}} - Etiqueta: {{labels.label}} - Estado: {{status}} - Valores: {{valueString}} - Fuente: {{generatorURL}} {{/each}}服务器监控 + 磁盘循环
描述:
### Alerta del servidor: {{hostname}} Uso de CPU: **{{cpuUsagePercent}}%** Uso de memoria: **{{memoryUsagePercent}}%** **Uso del disco:** {{#each diskMetrics}} - {{diskPath}}: {{usagePercent}}% usado ({{freePercent}}% libre) {{/each}} **Procesos en ejecución:** {{#each processes}} - [{{pid}}] {{name}}: {{command}} {{/each}}SNMP 监控 + OID 循环
描述:
### Estado del dispositivo SNMP En línea: {{isOnline}} Respuesta: {{responseTimeInMs}}ms **Valores OID:** {{#each oidResponses}} - {{name}} ({{oid}}): {{value}} {{/each}}模板引擎的底层原理
理解模板引擎的实现细节,有助于写出稳定、可预期的模板:
- 存储映射构建(数据侧):
MonitorTemplateUtil.buildTemplateStorageMap按监控类型(MonitorType.API、MonitorType.Website、MonitorType.IncomingRequest、MonitorType.Ping、MonitorType.IP、MonitorType.Port、MonitorType.SSLCertificate、MonitorType.Server、MonitorType.Synthetic、MonitorType.CustomCode、MonitorType.Snmp)分别从ProbeMonitorResponse、IncomingMonitorRequest、ServerMonitorResponse、SslMonitorResponse、SyntheticMonitorResponse、CustomCodeMonitorResponse、SnmpMonitorResponse等类型中抽取字段。这也是文档中“不同监控类型暴露不同变量”的根本原因。 - 模板渲染(字符串侧):
processTemplateString调用VMUtil.replaceValueInPlace(storageMap, value, false)。渲染分两阶段:先调用expandEachLoops展开所有{{#each}}循环(嵌套循环按作用域递归),再对剩余的{{variable}}用deepFind在存储映射中逐条查找替换。未找到的变量被跳过、保留原文。 - JSON 值序列化:当解析出的值是对象时,会被
JSON.stringify(缩进 2 空格)后插入;替换使用函数形式而非字符串形式,避免值中$&、$1等字符被误当作正则替换模式。 - Incident/Alert 创建时的调用链:MonitorIncident.ts 在创建 Incident 时构建存储映射并对标题/描述做模板渲染,Alert 侧逻辑与之镜像对齐,保证同一阈值触发事件与告警时文案口径一致。
常见陷阱与最佳实践
- 路径不存在时占位符原样保留:
{{responseBody.error.id}}会在标题中带花括号出现。若希望整段文案消失,应使用{{#each}}块包裹(仅当路径解析为数组时生效)。 [*]不会被解析:它只在“按负载字段分组”的路径字段中有意义,模板正文里请用[0]、[1]这类具体索引。- 入站请求数组请优先用分组变量:
{{requestBody.alerts[0].annotations.summary}}永远读第一条告警,而非触发 Incident 的那一条;正确做法是使用分组键变量与commonLabels、commonAnnotations等共享字段。 - 循环体中优先相对路径:
{{labels.label}}先相对于当前数组元素解析,找不到再回退父级存储映射——这既是便利也是隐患,注意字段名冲突。 - 明确不支持的监控类型:日志、链路、指标监控目前不参与模板渲染,请勿在其 Criteria 中依赖本机制。
- 充分利用代码内核对变量名:遇到不确定的字段,可对照 MonitorTemplateUtil.ts 中
buildTemplateStorageMap的实际注入键名(如服务器监控只在basicInfrastructureMetrics存在时注入 CPU/内存/磁盘键),避免引用永远不会存在的变量。
掌握以上语法与实现细节后,你可以为每种监控类型定制信息密度恰到好处的 Incident/Alert 文案,让值班人员在通知到达的瞬间即可定位故障源,而无需逐条打开详情页。
【免费下载链接】oneuptimeComplete open-source monitoring and observability platform.项目地址: https://gitcode.com/GitHub_Trending/on/oneuptime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考