凌晨两点,手机开始连着振,我睡眼惺忪地摸过来一看,群消息已经刷了几十条。不是恶意骚扰,是PRTG在报警。可点开消息一看,清一色纯文本:一行设备名、一行传感器名、一行“警报:CPU负载率超过阈值”,数据是多少、持续了多久、影响范围有多大,全靠自己往上翻聊天记录去拼。
这不是我一个人遇到的尴尬。PRTG接企业微信做告警,很多团队第一步都是找群机器人Webhook直发纯文本,图省事。但真等告警风暴砸下来,纯文本几乎等于没有告警——信息都在,却什么都找不到。关键状态、当前值、恢复情况全部混成一团,值班的人光分辨这些消息就要花大把时间。
所以我今天把这套完整方案聊透:怎么把PRTG的告警消息,通过企业微信群机器人或自建应用,做成带层级、带视觉重点、带跳转链接的图文混排告警卡片。这篇东西适合正在用PRTG、被夜里纯文本告警折磨的监控运维,也适合刚准备搭监控通知体系、想把可读性一步做到位的人。
1. 纯文本告警为什么救不了夜班:一个真实的告警风暴现场
1.1 先复盘那个凌晨
某次在客户机房现场,一台精密空调故障,温度缓慢上升。按说PRTG一个传感器就能发现问题,但当时那个环境里300多个传感器同时在线,空调异常后,温度、湿度、设备负载、网络延迟这些传感器先后越阈值,告警像瀑布一样往下刷。
我盯着聊天窗口,看到的是一堆结构几乎一样的文本消息:
设备名 + 传感器名 + 状态 + 一行说明。没有颜色、没有标题层次,更没有“哪条影响最大”的提示。等值班同事从消息流里翻出最原始的那条空调告警,已经过去了十几分钟。冷却系统过温,这个告警优先级显然比某个内网交换机延迟超标高得多,但在纯文本列表里,它们长得一模一样。
1.2 纯文本告警的三个硬伤
- 第一个硬伤,没有视觉层级。所有消息一个模子印出来,高优低优全凭肉眼猜。值班人收到一屏告警,首先得做“分诊”,而纯文本把分诊成本全部推给了人。
- 第二个硬伤,缺少上下文。纯文本一般只给“当前状态”和一句固定描述。持续多久?当前实测值是多少?这已经是第几次触发?这些字段散落在不同消息里,想回答“严重不严重”,得自己拼图。
- 第三个硬伤,告警和恢复难区分,甚至自动恢复消息和告警消息挤在一起,如果群消息顺序乱一下,很容易让处理人以为问题已经解决,导致误判。
用一段大白话总结:纯文本告警适合喝杯咖啡慢慢对着屏幕分析,但不适合凌晨被消息震醒之后那三十秒的快速决策。
1.3 什么叫图文混排:不是插图,是信息分层
其实“图文混排”不是非得往消息里塞张图片。在企业微信的消息体系里,真正的图文混排指的是把一段富文本信息按标题、正文、字段、跳转按钮的方式重新编排,让人类视觉系统能快速抓重点。常见形态有三种:
| 消息类型 | 形态 | 适合场景 |
|---|---|---|
| text | 纯文本 | 应急测试、内部调试 |
| markdown | 富文本,支持加粗、引用、链接、分级标题 | 日常告警推送主力,信息密度高 |
| image_text | 图文卡片,上方可带图,下面配标题和正文 | 关键告警、需要吸引注意力、需要跳转详情 |
| news | 图文列表,多条可折叠 | 批量告警聚合、通报类消息 |
一图胜千言,但更重要的是“分层”:标题点明发生了什么,正文用加粗强调当前值,链接负责把人引导到PRTG后台。值班人扫一眼标题,就能决定“要不要起来处理”。
1.4 为什么选企业微信当监控告警通道
原因很现实:同事手机上已经装了企业微信,不需要额外装任何客户端。相比短信的低成本、高触达,相比邮箱的看完需要登录,企业微信消息出现在聊天列表里,接收率天然更高。
而且企业微信提供了Webhook和自建应用两套接口,前者适合快速接入,后者适合按组织架构定向通知。再加上群聊的历史消息天然可搜索,复盘告警只需要搜设备名就行。这套路线的性价比,在中小规模监控体系里几乎没有对手。
2. 企业微信通道二选一:群机器人Webhook还是自建应用推送
2.1 群机器人:三分钟就能跑通的方案
先给只想验证效果的读者一条最快路径。建一个企业微信群,点群设置,添加群机器人,复制Webhook地址。Webhook地址长这样:
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx拿到地址之后,直接用命令行测试连通性,不需要写任何代码:
curl -X POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_KEY' \ -H 'Content-Type: application/json' \ -d '{ "msgtype": "text", "text": { "content": "PRTG联动测试" } }'返回内容里如果出现"errcode":0,说明通道已经通了。群机器人的特点是每个群独立管理、互不影响,把不同业务系统的告警推到不同群,天然隔离。
2.2 自建应用:面向成员的定向推送
如果你的告警不想发到群里,而是希望按组织架构定向发给某个人或某个部门,就得用自建应用。操作步骤也不复杂:
登录企业微信管理后台,在应用管理里创建自建应用,拿到三个关键参数:企业ID(CorpID)、应用ID(AgentId)、应用密钥(Secret)。有了这三个参数,先换access_token,再发应用消息。
# 拿 access_token curl -G 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' \ --data-urlencode 'corpid=YOUR_CORP_ID' \ --data-urlencode 'corpsecret=YOUR_SECRET'# 发应用消息 curl -X POST 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=REPLACE_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "touser": "user1|user2", "msgtype": "text", "agentid": 1000002, "text": { "content": "PRTG联动测试" } }'自建应用的“可见范围”一定要认真设置,给开发人员和运维人员最小授权。因为应用消息是可以发给企业内所有成员的,配置不当容易变成全员告警广播。
2.3 两类通道怎么取舍
| 对比项 | 群机器人Webhook | 自建应用 |
|---|---|---|
| 接收对象 | 群成员 | 按组织架构指定的成员 |
| 配置门槛 | 低,建群即可 | 中,需要管理后台权限 |
| 消息类型 | text、markdown、image_text等 | text、markdown、image_text等 |
| 定向通知 | 不能指定某个人 | 可以 |
| 适用场景 | 运维值班群、自动化发布群 | 需要通知到具体负责人的场景 |
实际项目里,我的默认选择是先上群机器人,因为告警处理本来就是一个团队协作动作,消息发到值班群里,谁看了谁接手,信息是共享的。等到告警链路成熟、需要把“某台设备的专属告警发给专属负责人”时,再叠加自建应用不迟。
2.4 两类接口的限流与素材有效期
这个坑必须提前知道:企业微信的Webhook发送频率是有限制的,短时间大量消息会直接返回错误码。告警风暴场景下,几百条消息并发,很容易触发峰值限制。
另外,如果你要发送图片类素材,通过接口上传临时素材拿到的media_id,有效期只有三天。PRTG这种长期运行的监控系统,如果脚本里硬编码了一个media_id,三天后图片必然失效。正确做法是每次发送前实时上传图片,或者把图片放在内网HTTP服务上,用URL代替media_id。这些细节到第5章排错部分会再展开。
3. PRTG侧改造:用Execute Program把%变量变成结构化卡片
3.1 为什么放弃PRTG内置通知通道,改用脚本
PRTG的通知机制分三层:传感器触发通知模板,模板里可以选邮件、HTTP请求、执行程序等不同通道。能把消息推到企业微信的方式不止一种,但我的实践结论是,用“Execute Program”执行脚本是最可控的。
邮件通知要配SMTP,绕一圈不说,消息形态还是纯文本。HTTP通知虽然可以不写脚本,直接发POST请求,但PRTG的%变量在URL编码和模板转义上限制很大,想拼出规范的JSON请求体,会被特殊字符、引号、百分号折磨到怀疑人生。脚本则不同:PRTG把变量作为参数传给脚本,脚本内部随便处理、随便拼JSON,所有逻辑都在PowerShell里,调试起来透明得多。
3.2 PRTG变量到卡片字段的映射
在配置脚本之前,先搞清楚PRTG能给你什么。常用变量如下:
| PRTG变量 | 含义 | 落到卡片上的位置 |
|---|---|---|
| %device | 设备名称 | 卡片标题主体 |
| %sensor | 传感器名称 | 卡片标题主体 |
| %status | 传感器状态(如Down、Warning) | 状态前缀 |
| %message | 传感器当前消息文案 | 正文摘要 |
| %lastvalue | 传感器最新读取到当前值 | 当前值字段 |
| %up | 传感器正常运行时长 | 持续时长字段 |
| %time | 通知触发时间 | 时间字段 |
PRTG在通知配置里,可以把这些变量当作参数传给脚本。配置方式是在传感器的通知设置里选择“Execute Program”,然后在参数栏位上写类似这样的内容:
powershell.exe -ExecutionPolicy Bypass -File "C:\prtg-scripts\wechat_push.ps1" "%device" "%sensor" "%status" "%message" "%lastvalue" "%time"这里有一个关键操作:所有%变量必须用双引号包起来。因为%message和%lastvalue里可能包含空格,不加引号,PRTG在传参时会把一句话拆成好几个参数,脚本收到的就是一堆碎片。
3.3 PowerShell脚本完整示例:markdown版告警卡片
下面的脚本对应企业微信群机器人的markdown消息类型。PRTG把这些变量传进来,脚本在内部拼装markdown格式,再POST到Webhook:
param( [string]$DeviceName, [string]$SensorName, [string]$Status, [string]$Message, [string]$LastValue, [string]$Time ) $WebhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_KEY" $Content = @" ## 告警通知 **设备**:$DeviceName **传感器**:$SensorName **状态**:$Status **当前值**:$LastValue **发生时间**:$Time > $Message [打开PRTG后台](https://prtg.example.com) "@ $Body = @{ msgtype = "markdown" markdown = @{ content = $Content } } | ConvertTo-Json -Depth 5 $Utf8Bytes = [System.Text.Encoding]::UTF8.GetBytes($Body) Invoke-RestMethod -Uri $WebhookUrl -Method Post -Body $Utf8Bytes -ContentType "application/json; charset=utf-8"注意脚本里最后一行:把JSON字符串转成UTF8字节数组再发送,而不是直接传字符串。这是中文不乱码的关键之一。
3.4 升级为image_text图文卡片:先传图再发卡片
markdown在文字排版上很舒服,但如果希望消息看起来像一张“卡片”,带封面图、标题栏和描述区,就要用image_text消息类型。两条请求:先上传图片拿到media_id,再发送image_text。
上传图片用这种方式(在Windows PowerShell 5.1里推荐直接用curl.exe,因为老版本PowerShell的Invoke-RestMethod没有封装multipart表单):
curl.exe -F "media=@C:\prtg-scripts\banner_warning.png" "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=REPLACE_KEY&type=file"返回的JSON里有media_id。发送image_text消息的结构如下:
{ "msgtype": "image_text", "image_text": { "media_id": "MEDIA_ID_FROM_UPLOAD", "title": "告警 - 机房A - 精密空调温度", "text": "当前值: 27.3度, 阈值: 25度\n持续时长: 8分钟\n点击下方链接查看详情", "url": "https://prtg.example.com" } }这里最需要注意的一点是:企业微信的image_text支持不全的版本差异,偶尔会遇到发送报错。稳妥做法是脚本里同时写好markdown兜底逻辑,一旦image_text发送失败,自动退回markdown普通告警。监控通道可以晚10秒到,但不能不到。
3.5 PRTG运行脚本的三个隐藏前提
- 第一,PRTG核心服务运行在Windows服务账号下,这个账号必须有权限访问powershell.exe,并且能主动访问外网。经常有人在测试环境中用管理员账号手工跑通了脚本,但PRTG服务一执行就失败,排查半天才发现是服务账号出网权限不够。
- 第二,PowerShell执行策略。建议在命令行里显式加
-ExecutionPolicy Bypass,别指望目标机器的默认策略放行。 - 第三,脚本路径和脚本文件名不要带空格,不要放在OneDrive同步目录里,否则PRTG调用时会因为路径解析问题静默失败。
4. 图文卡片的信息设计:一张告警卡片上应该放什么
4.1 标题规范:一秒判断要不要起来处理
值班人看告警的顺序,永远是先看标题,再看正文。所以标题一定要在最短字数内回答四个问题:什么状态、哪台设备、哪个传感器、是否紧急。
我的标题模板长这样:
告警|机房A|精密空调温度|超过阈值恢复消息则改成:
恢复|机房A|精密空调温度|已回到正常范围多花几个字符写清楚“恢复”,比只改状态值强太多。因为凌晨值班心理是“告警处理完,看到恢复消息才真正松口气”。如果恢复消息只写“UP”,简直是在折磨人。
4.2 正文七要素,缺一不可
参考下面的markdown模板:
**状态**:警告 **设备**:机房A-精密空调 **传感器**:温度传感器 **当前值**:27.3 度 **阈值**:25.0 度 **持续时长**:8 分钟 **发生时间**:02:33 **链接**:[打开PRTG查看详情](https://prtg.example.com)七要素里,最容易被忽略的是持续时长。持续时长决定了这个告警是新发还是重复抖动。一个设备持续告警8分钟和一个刚触发10秒的告警,紧急程度完全不同。把持续时间放进去,值班人就能判断要不要立刻叫醒同事。
4.3 图片怎么用:不是越多越好
如果没条件把PRTG的实时图表导出成图片,就不要硬塞图。我见过很多告警卡片塞一张模糊截图,不仅没提供信息,还拉低了加载速度。
推荐的做法是准备一套按严重级别区分的banner图:警告用黄色,错误用红色,恢复用绿色,每条告警选择对应banner上传获取media_id。这样图片起到的是“视觉报警灯”作用,真正高密度的数据靠文字和表格承载。
如果确实需要展示传感器历史曲线,可以配置PRTG的Graph API定期把图表导出成PNG,放到内网HTTP服务下。卡片里用picurl指向这张图,而不是每次都上传到企业微信。这样既能展示走势图,又没有media_id过期问题。
4.4 恢复通知也要图文混排,但内容结构要变
恢复消息别简单把“告警”改成“恢复”,最佳实践是把这次事件的复盘要素放进去:持续时间、峰值、最终恢复值、处理建议。
**状态**:已恢复 **设备**:机房A-精密空调 **传感器**:温度传感器 **峰值**:27.3 度(14:02) **持续时间**:23 分钟 **当前温度**:24.1 度 **链接**:[打开PRTG查看历史](https://prtg.example.com)这种恢复卡片在事后复盘时特别好用,直接把聊天记录导出,就是一个简版事件报告。值班人员不用再翻监控后台拉数据。
5. 联调排错实录:让卡片稳定到达的五道关卡
5.1 关卡一:PRTG到底有没有把脚本执行起来
排错第一步永远是确认执行链路的上一环通不通。PRTG里可以查看传感器的通知历史,也可以直接在PRTG的“系统日志”里看脚本执行记录。但最直接的办法是让脚本自己写日志:
$LogDir = "C:\prtg-scripts\logs" if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force } $LogFile = Join-Path $LogDir ("alert_" + (Get-Date -Format "yyyyMMdd_HHmmss") + ".log") "Device=$DeviceName Sensor=$SensorName Status=$Status Time=$Time" | Out-File -FilePath $LogFile -Encoding utf8先在脚本开头写日志,再跑一遍告警,看日志文件有没有生成。如果日志都生成了,企业微信里没消息,那就是网络或参数问题,方向就很明确。我在现场排错时,几乎每次都用这个手段先划定问题边界。
5.2 关卡二:中文全是问号或者乱码
PowerShell 5.1会把无BOM的UTF-8脚本文件读取为ANSI编码,中文直接乱掉。所以脚本文件必须保存为UTF-8 with BOM。另一点,发送请求时如果Body直接传字符串,某些企业微信版本解析时会把JSON里的中文按默认字符集处理,结果就是星星点点。正确做法是像前面脚本里写的,用[System.Text.Encoding]::UTF8.GetBytes()转成字节数组。
如果乱码依然存在,先手工curl验证:
curl -X POST 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_KEY' \ -H 'Content-Type: application/json; charset=utf-8' \ -d '{"msgtype":"text","text":{"content":"中文测试"}}'curl不乱码而脚本乱码,问题必然在PowerShell的文本编码处理上。
5.3 关卡三:消息被截断或者markdown渲染失效
企业微信的markdown消息对内容大小有限制,大体在4096字节以内。PRTG的%message一旦包含长文本,很容易超限。脚本里要主动截断:
if ($Message.Length -gt 200) { $Message = $Message.Substring(0, 200) + "...(已截断,请点击链接查看原文)" }另一个细节是markdown里出现特殊符号会被解析成格式符。比如告警内容里如果包含尖括号,markdown渲染会出问题。保险做法是把会话消息里的换行统一为\n,把容易冲突的字符做简单清洗。
注意企业微信的markdown消息格式和常用markdown引擎不完全一致,加粗符号**和引用>通常可用,但表格、行内代码不一定可靠。所以卡片正文宁可用列表和加粗,不要依赖复杂markdown语法。
5.4 关卡四:图片卡片发不出来
image_text图片发不出来的场景,我大致归纳成三类:
- media_id失效。临时素材有效期只有3天,脚本里如果上传一次后缓存media_id,超过3天必失败。每次发送前重新上传,或者使用URL模式。
- picurl不可达。如果企业微信服务器拉取不到picurl对应图片,卡片会显示空白。注意不能只在PRTG服务器本机能访问图片,企业微信服务器也要能访问。内网环境酌情考虑。
- 图片超限。企业微信临时素材接口对图片大小有上限,现场踩过2M以上PNG被拒的情况。控制banner图片体积。
| 现象 | 排查方向 |
|---|---|
| media_id无效 | 是否超过3天有效期 |
| 卡片空白只有标题 | picurl是否外网/企业微信可达 |
| 上传接口报错 | 图片大小、字段名是否为media |
| 发送报错不识别 | 消息类型或参数名拼写错误 |
5.5 关卡五:告警风暴打爆webhook限流
大批设备同时掉线,几十上百条告警在一个时间点冲过来,很容易触发企业微信的频率限制。这时消息发送返回错误码,直接丢弃。我的处理方案是双重保险:
先是在脚本里做轻量重试,发送失败后间隔几秒再试,最多重试三次。这能解决瞬时抖动。真正管用的是在PRTG侧和脚本侧做聚合:同一个设备下的传感器告警不要逐条发,而是汇总成一条image_text,用列表把多个传感器状态展示出来。聚合后的卡片既避开了限流,分工也更清晰。
5.6 额外提醒:Webhook key泄露等于群聊被接管
最后说一句安全常识:群机器人的Webhook地址是一个免鉴权的推送入口,任何拿到URL的人都能往群里发消息。不要把它提交到Git仓库,不要把带key的脚本随手拷到共享目录。如果发现泄露,直接到群机器人设置里删除重建,别留隐患。
我在实际项目中踩过的最深的坑,是跳过了curl自测直接上脚本,结果PRTG通知触发后一片寂静,排查了半小时才发现是服务账号没有出网权限。现在我的习惯是:第一步用curl敲通道连通性,第二步用固定参数跑脚本,第三步再让PRTG真实触一发告警。把链路一层层割开验证,问题永远出现在你最没想到的那一环,但一定逃不出这几个关卡。