熟悉PRTG的人应该都有过这种经历:监控配了一大堆,邮件告警发出去,结果没人看,值班手机一响又只是一行干巴巴的“CPU Load 98%”,你还得再打开电脑登录PRTG,看趋势图判断到底是突发还是持续恶化。我这次做的,是彻底改变这个流程——让PRTG的告警直接推到企业微信,而且推送的不再是纯文本,而是图文混排消息:上面是设备、传感器、状态、时间这些关键文字,下面直接带一张监控趋势图,手机屏幕上一条消息就能完成“看情况、定级别、决定要不要起床处理”的全部动作。
这套方案我在真实运维环境里已经跑了一段时间,踩了不少坑,也找到了不少可以优化的小技巧。这篇就把整个实现过程完整拆开讲清楚:为什么选企业微信,通道怎么选,图表图片怎么从PRTG里捞出来,最后怎么组装成图文消息推出去。适合正在用PRTG做网络或服务器监控、又想把告警收敛到手机上的IT运维同学参考,也适合还没想好怎么给值班人员发告警的团队抄作业。
1. 告警形态设计:先想清楚要发什么,再谈怎么发
1.1 纯文本告警为什么不够用
做运维久了你会发现,告警推送最容易陷入一个误区:觉得“发出去了”就完事了。PRTG默认支持邮件推送、App推送、HTTP请求通知,但这些通知方式都偏向“事件通知”,而不是“问题说明”。邮件里带的那点信息,通常就是一个传感器名称和一行状态描述,像“Device XYZ (CPU Load) - Down (Warning)”,然后呢?你还是要打开PRTG看这是哪台设备、这个负载是从什么时候开始高的、是不是持续了半小时的突发流量。
我问过不少值班同事,他们最在意的其实不是“收到消息”,而是“收到消息后能不能少打开一次监控系统”。纯文本消息最大的问题就在这里:它只是告诉你有事,但没告诉你这事有多严重、趋势是什么。图文混排消息恰好解决这个问题——文字负责描述“发生了什么”,图表负责展示“这件事是怎么演变的”,两样东西组合在一起,值班人员不需要再去PRTG里翻窗口,效率提升不是一点点。
1.2 图文告警的目标形态:一条消息,三秒决策
我在设计这套东西的时候,给自己定了一个标准:一条告警推送到手机上,正常人三秒钟之内要能回答出三个问题——哪个设备出事、什么指标异常、这个异常是突发的还是持续的。对应到消息形态上,就是下面这个结构:
- 标题部分:告警级别 + 设备名 + 传感器名,一眼定位目标
- 信息部分:当前值、阈值、持续时间、告警时间,判断严重程度
- 图片部分:该传感器最近的趋势图,确认异常曲线和走向
- 链接部分:点击跳转到PRTG控制台,需要深挖时直接进去看
手机上的视觉效果,就是企业微信对话框里先出现一条带格式的markdown消息,紧接着出现一张清晰的监控图表,两条消息连在一起看,就是完整的图文告警卡片。这里说的“图文混排”,不需要依赖企业微信的某个特殊消息类型,而是用“连续消息组”的方式实现,简单、可靠、适用于所有企业微信客户端版本。
1.3 通道选型与图文组装方式的取舍
企业微信对接PRTG,主流有三条通道:群机器人Webhook、企业微信自建应用、邮件网关转发。邮件转发这种方案等于绕了一圈,时效和格式都不好控制,我直接排除。真正需要考虑的是前两条,我把它们放在一张表里对比:
| 对比项 | 群机器人Webhook | 企业微信自建应用 |
|---|---|---|
| 接入门槛 | 极低,群里添加机器人即可 | 需后台创建应用,拿到CorpID、AgentId、Secret |
| 发送目标 | 只能发到所在群 | 可发送给成员、部门、标签或指定群聊 |
| 图片消息 | 支持,先上传素材再发 | 支持,先上传素材再发 |
| Markdown消息 | 支持 | 支持 |
| 是否支持@人 | 支持,按手机号或用户ID | 支持,按用户ID |
| 消息频率限制 | 每机器人每分钟约20条 | 更高但仍有限频 |
| 适合场景 | 快速验证、小团队共享群 | 生产环境、需要定向告警 |
结论很明确:如果只是图个方便,群机器人三分钟就能通;如果是正经生产环境,我建议直接用自建应用。原因不光是限频,更关键的是自建应用可以按AgentId隔离权限,收发消息都有完整日志,后期接值班表@指定负责人也更方便。
图文组装方式上,第一次做的人容易纠结“怎么把图片嵌进文字消息里”。其实企业微信消息里要想真正显示另一张内网图片,只有两个办法:
- 办法A:先把图片上传到企业微信临时素材库拿到media_id,然后连续发送一条markdown消息和一条image消息。图片不用公网地址,内网环境也能用。
- 办法B:在markdown消息里直接引用图片的HTTP URL。前提是这个URL必须能从企业微信服务器访问到,意味着你的PRTG要暴露到公网或做反向代理,安全性大打折扣。
我选办法A,也是本文后续代码采用的方式。PRTG一般部署在内网,临时素材上传机制正好绕开公网暴露问题,收到的图片还会被企业微信做CDN缓存,手机端加载速度也快。
2. 前置配置:PRTG与企业微信要准备哪些东西
2.1 PRTG侧:只读账号、传感器ID与图表地址
脚本要从PRTG拉取图表图片,前提是它能带着身份信息去访问PRTG的HTTP接口。我强烈建议在PRTG里为告警脚本单独建一个只读账号,不要用你的管理员账号到处传。原因很简单:脚本里要明文存放账号密码,权限越小,被泄露后影响面越小。账号创建路径是:PRTG Web界面右上角用户管理(账户图标)→ 添加用户 → 设置用户名密码 → 把权限级别设为“只读”。
接下来要记录两个关键值。
一个是传感器ID。进入任何一个传感器的详情页,浏览器地址栏里会有一个类似sensorid=123456的参数,这个数字就是传感器ID。通知脚本需要靠它定位要抓哪张图、消息里写哪个传感器。
另一个是图表图片地址。你可以在传感器“图表”页面里,右键图表图片复制图片地址,一般长这样:
http://10.0.0.8:8080/chartimage.png?type=graph&graphid=xxx&sensorid=xxx&width=850&height=300把这个URL记下来,加在脚本配置里。需要注意,直接用这个URL访问时PRTG会要求认证,所以脚本下载图片时需要一起带上只读账号的密码,下一章代码里会写清楚。
2.2 企业微信侧:自建应用的三件套与接口清单
企业微信自建应用是生产环境推荐方案。操作路径:企业微信管理后台 → 应用管理 → 应用 → 自建 → 创建应用。创建完成后,你需要把三个值记下来:
- 企业ID(CorpID):管理后台“我的企业”页面里能看到
- 应用ID(AgentId):创建后的应用详情页里能看到
- 应用密钥(Secret):在应用详情页里点击“查看”获取,注意只能完整显示一次
这三个值凑齐,脚本才能完成企业微信API的认证,缺一个都跑不通。
有了三件套,对接企业微信其实只需要记住三个接口动作:
- 获取access_token:拿CorpID和Secret换token,token有效期约2小时,脚本每次运行都去取一次也完全够用
- 上传图片素材:把本地PNG文件post到临时素材接口,返回media_id
- 发送消息:用media_id发image消息,用content字段发text或markdown消息
记得在自建应用的“可见范围”里配上告警应达成的成员或部门,这一步经常有人忘,最后明明代码没问题,token也正常,消息却发不出去。
2.3 备选通道:群机器人Webhook的快速接入
如果今天只是想让告警先进到一个测试群里跑通流程,群机器人是效率最高的方式。在你目标群里右键 → 添加群机器人 → 复制Webhook地址,拿到一个类似下面格式的地址:
https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的key用这个Webhook,发送消息的接口地址就是它本身,上传图片的接口略有不同:
POST https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=你的key&type=image群机器人不需要token,省去了CORP三件套。但它只能发到当前群,而且每个机器人每分钟约20条的频率限制,真出大故障时如果脚本不做任何合并,很容易触发限频。所以我的建议是:先用群机器人验证脚本逻辑,验证通过后换成自建应用,或者在生产环境两条通道并存,日常低级别告警走群,严重告警走应用定向发送。
3. 图文告警的完整落地:从PRTG触发到企微推送
3.1 整体链路与脚本结构设计
先看这条链路是怎么串起来的:
- PRTG检测到传感器状态从正常变为异常,比如CPU负载超过阈值
- PRTG触发我们配置好的通知动作,执行外部脚本
- PowerShell脚本接收传感器ID、设备名、告警消息等参数
- 脚本用只读账号从PRTG下载对应传感器的趋势图PNG文件
- 脚本调用企业微信API,把PNG上传为临时素材,拿到media_id
- 脚本连续发送一条markdown消息和一条image消息,手机端形成图文告警
这个流程里每个环节都可以单独调试,我建议按“手动→半自动→全自动”的顺序推进。第一次别急着接到PRTG里,先在命令行里手动带参数跑一次脚本,确认企业微信消息发出去了、图片加载正常,再挂到PRTG通知上。这样能把脚本逻辑和PRTG配置两个问题域分开排查。
脚本设计上,我坚持三个原则:幂等(同一告警重复执行不会产生副作用)、快速(从执行到消息发出尽量控制在10秒内)、可靠失败(出错时写日志,不影响PRTG主流程)。告警推送只是监控体系的辅助环节,脚本如果卡死反而拖累PRTG,那就得不偿失了。
3.2 从PRTG拉取图表图片的两种姿势
第一种姿势,手动配置URL,适合快速实现。上一章已经提到,从PRTG图表页面复制图片地址,把只读账号密码拼在URL后面,脚本就能直接下载:
http://10.0.0.8:8080/chartimage.png?type=graph&graphid=xxx&sensorid=xxx&width=850&height=300&username=prtg_ro&password=你的密码我把用户名密码放在URL参数里,是为了让脚本少写一层认证逻辑。PRTG支持这种基础认证方式,实测有效。但明文密码会留在PRTG的通知日志和脚本配置里,只读账号权限低,风险可接受。
第二种姿势,通过API自动获取,适合传感器特别多、不想一个一个人工复制URL的场景。PRTG有一个表格接口专门用来拿图表信息:
/api/table.json?content=graphs&id=传感器ID&username=xx&password=xx返回的JSON里能找到图表渲染URL字段,脚本对这个URL发起请求就能拿到PNG。不同PRTG版本的字段命名略有差异,有的版本叫url,有的版本叫graph,甚至有嵌套结构。建议先用浏览器打开上面的API地址,观察实际返回的JSON结构,再用代码去解析对应字段。我生产环境用的就是这个方式,新增传感器时无需维护图表URL,脚本全自动处理。
3.3 PowerShell脚本核心代码:上传图片并推送图文消息
先说明一下脚本环境:PRTG跑在Windows Server上,PowerShell就是天然选择,系统自带,不用装Python。脚本我会用PowerShell 5.1兼容写法,上传图片时调用Windows自带的curl.exe,而不是PowerShell的Invoke-RestMethod -Form,因为后者在Windows PowerShell 5.1里并不存在。下面是一份可以直接参考的核心脚本:
param( [string]$SensorId, [string]$ServerName, [string]$SensorName, [string]$MessageText, [string]$LastValue ) # 配置区域:按实际环境修改 $PrtgServer = "http://10.0.0.8:8080" $PrtgUser = "prtg_ro" $PrtgPassword = "ChangeMe" $PrtgChartUrl = "http://10.0.0.8:8080/chartimage.png?type=graph&graphid=xxx&sensorid=$SensorId&width=850&height=300&username=$PrtgUser&password=$PrtgPassword" # 企业微信自建应用参数 $WecomCorpId = "ww1234567890abcdef" $WecomAgentId = 1000010 $WecomSecret = "ChangeMe" # 发送目标:可以填具体成员UserID,也可以@all(前提是应用可见范围包含所有人) $ToUser = "@all" $TempDir = "$env:TEMP\prtg_wecom" New-Item -ItemType Directory -Force -Path $TempDir | Out-Null $ChartFile = "$TempDir\chart_$SensorId.png" # ---------- 1. 下载PRTG趋势图 ---------- try { Invoke-WebRequest -Uri $PrtgChartUrl -OutFile $ChartFile -UseBasicParsing -TimeoutSec 10 Write-Host "[OK] 图表已下载: $ChartFile" } catch { Add-Content -Path "$TempDir\error.log" -Value "$(Get-Date) 图表下载失败: $($_.Exception.Message)" Write-Host "[ERR] 图表下载失败: $($_.Exception.Message)" } # ---------- 2. 获取企业微信access_token ---------- $tokenUri = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$WecomCorpId&corpsecret=$WecomSecret" $tokenResp = Invoke-RestMethod -Uri $tokenUri -Method Get -TimeoutSec 10 if ($tokenResp.errcode -ne 0) { Add-Content -Path "$TempDir\error.log" -Value "$(Get-Date) Token获取失败: $($tokenResp.errmsg)" exit 1 } $accessToken = $tokenResp.access_token # ---------- 3. 上传图表图片,获取media_id ---------- # 注意必须写 curl.exe,因为PowerShell里 curl 默认是 Invoke-WebRequest 别名 $uploadUri = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=$accessToken&type=image" $uploadResult = curl.exe -s -F "file=@$ChartFile" $uploadUri $uploadObj = $uploadResult | ConvertFrom-Json if ($uploadObj.errcode -ne 0) { Add-Content -Path "$TempDir\error.log" -Value "$(Get-Date) 图片上传失败: $($uploadObj.errmsg)" Write-Host "[ERR] 图片上传失败: $($uploadObj.errmsg)" exit 1 } $mediaId = $uploadObj.media_id # ---------- 4. 拼接markdown告警文本 ---------- $markdownContent = @" ## [运维告警] $ServerName - $SensorName **状态:异常** - 当前值:**$LastValue** - 触发消息:$MessageText - 时间:$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - [打开PRTG监控控制台]($PrtgServer) <font color="warning">请值班人员关注,必要时登录控制台进一步确认。</font> "@ $mdBody = @{ touser = $ToUser msgtype = "markdown" agentid = $WecomAgentId markdown = @{ content = $markdownContent } } | ConvertTo-Json -Depth 5 $sendUri = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=$accessToken" $mdResp = Invoke-RestMethod -Uri $sendUri -Method Post -Body $mdBody -ContentType "application/json; charset=utf-8" -TimeoutSec 10 if ($mdResp.errcode -ne 0) { Add-Content -Path "$TempDir\error.log" -Value "$(Get-Date) Markdown消息发送失败: $($mdResp.errmsg)" exit 1 } # ---------- 5. 发送图片消息 ---------- $picBody = @{ touser = $ToUser msgtype = "image" agentid = $WecomAgentId image = @{ media_id = $mediaId } } | ConvertTo-Json -Depth 5 $picResp = Invoke-RestMethod -Uri $sendUri -Method Post -Body $picBody -ContentType "application/json; charset=utf-8" -TimeoutSec 10 if ($picResp.errcode -ne 0) { Add-Content -Path "$TempDir\error.log" -Value "$(Get-Date) 图片消息发送失败: $($picResp.errmsg)" exit 1 } Write-Host "[OK] 告警图文消息已推送: $ServerName / $SensorName"脚本的执行顺序是先发markdown文字,再发图片,这样手机上的阅读顺序是“看到文字说明→紧接着看到图片”,符合阅读直觉。反过来先图后文也能用,但文字会被挤到图片下面,重点不突出。
换成群机器人通道的话,改动很小:把token逻辑去掉,$sendUri换成Webhook地址,上传素材接口换成webhook/upload_media?key=你的key&type=image,发送内容的JSON结构略有差异,但思路完全一样。
3.4 PRTG通知触发器的参数配置要点
脚本就绪后,最后一步是把PRTG的告警事件和脚本绑定。PRTG Web界面里进入“通知”管理 → 添加新通知 → 选择“执行程序”,然后在配置里填入执行命令和参数。
PRTG需要在Windows上执行外部命令时,建议直接调用PowerShell,并把脚本的绝对路径放在参数里。同时,PRTG允许你在参数里使用占位符,把告警上下文传给脚本,我常用的参数串是这个:
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -File "C:\PRTG\Scripts\Send-WecomAlarm.ps1" %sensorid "%host" "%name" "%message" "%lastvalue"一个个解释占位符:
%sensorid:传感器ID,传给脚本,用来拼图表URL%host:设备名,显示在告警标题里%name:传感器名,比如CPU Load、Memory Used%message:PRTG自动生成的告警文本%lastvalue:传感器最后一次读数,比如“98 %”
这里有一个容易被坑的细节:PRTG在执行外部程序时,对工作目录和执行文件位置有比较严格的安全策略,不同版本要求不同。有些版本只允许执行位于PRTG安装目录特定子目录下的程序。如果你配完发现脚本根本没被执行,先别怀疑脚本,检查一下是不是被PRTG的安全机制拦了。一个通用解法是写一个.cmd或.bat包装脚本,放到PRTG安装目录的notifications子目录下,再由它调用你实际的PowerShell脚本。
通知配好之后,强烈建议先用PRTG的“测试通知”功能完整跑一遍。测试按钮会模拟一次告警触发,你可以直接看到企业微信里是否收到了图文消息。这一步能确定成功,再考虑接到真实传感器上。
4. 踩坑实录:那些文档里不会写的问题
4.1 高频报错对照与解决办法
这套链路我调试的时候几乎把企业微信的报错都见了一遍,挑几个最有典型性的列成速查表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 返回 invalid media_id | 图片上传后隔了太久才发送,素材过期;或media_id和当前应用不匹配 | 上传后立即使用;确认media_id是本应用上传得到的 |
| 上传图片返回类型不支持 | 图片超过2MB;格式不是jpg/png | 把PNG压到2MB以内;用System.Drawing转成jpg或缩小尺寸 |
| 发送消息报60011 | 收件人不在应用的可见范围内 | 去自建应用配置里补上对应的成员或部门 |
| 中文乱码 | PowerShell 5.1控制台编码问题;curl.exe里传中文JSON被转码 | 用UTF-8编码保存脚本;优先用ConvertTo-Json生成的body文件,再配合curl.exe -d @file发送 |
| PRTG图表下载返回401 | 只读账号被锁定或密码错误;URL参数里没有带全username和password | 浏览器直接打开图表URL验证认证;确认账号状态正常 |
编码问题值得多说一句。Windows PowerShell 5.1里写中文脚本,文件必须用UTF-8编码或UTF-8带BOM保存,否则PowerShell会把脚本里的中文按GBK解析,企业微信那边收到的就是乱码。另外,不要在命令行参数里直接拼一大段中文告警内容,让curl.exe去转发,很容易在子进程启动时被系统编码吃掉字符。我最后改成先写临时JSON文件,再用curl.exe -d @json文件方式发送,问题彻底消失。
4.2 告警风暴下的限频与消息合并
故障来的时候往往是雪崩式的,核心交换机一断,下面几十上百个传感器同时变Down,一个传感器一条告警的话,企业微信的限制根本扛不住。群机器人20条/分钟的限频是明文规定的,自建应用虽然宽松些,同样有接口频控风险。
面对这个现实问题,我建议脚本加一个“同设备合并”的设计:同一台设备的多个传感器在短时间窗口内反复告警时,只向企业微信发一条经过聚合的告警,把全部故障传感器列在markdown里,统计总数和最早触发时间。再加一层“去重缓存”,用文件或内存记录每个传感器最近一次成功推送的时间戳,分钟级窗口内相同告警不重复发送。
# 去重示例:用文件记录时间戳 $CacheFile = "$TempDir\cache_$SensorId.txt" if (Test-Path $CacheFile) { $LastSendTime = [datetime]::Parse((Get-Content $CacheFile)) if (((Get-Date) - $LastSendTime).TotalMinutes -lt 5) { Write-Host "[SKIP] 短时间内已推送过,跳过:$SensorId" exit 0 } } Get-Date | Set-Content $CacheFile这类“避免同一告警五分钟内重复推送”的保护逻辑,在真实故障处理中价值极大。否则网线刚被施工方碰掉的那几分钟里,你的手机会被同一个告警刷屏到白屏。
4.3 脚本执行身份、超时与网络代理排查
PRTG在Windows上以服务方式运行,触发外部脚本时用的通常是PRTG服务账户,而不是你的登录账户。这会导致几种怪问题:脚本里访问内网资源出现401或权限不足,访问外网企业微信接口超时但浏览器里测试正常,甚至是脚本调用的某个模块找不到环境变量路径。
我的排查顺序是这样的:
- 先手动用同一个PowerShell脚本、同一套参数在命令行执行,确认脚本本身没问题
- 再确认PRTG服务登录账户是否具备脚本运行目录的读写权限
- 确认服务器访问外网是否经过代理,如果走代理,脚本里的
Invoke-RestMethod和curl.exe要分别配置代理信息
有一个实用技巧:在脚本开头强制写日志文件,记录执行开始时间、参数和异常信息。PRTG的告警通知配置里也能看到“执行程序”的输出内容,但远没有日志文件直观。脚本只有不断留痕,后续排查才有据可循。
5. 一点扩展:让告警变成一个完整的工作流
5.1 每日巡检图文摘要的自动推送
常告警推送稳定之后,我开始把同一套能力用在“每日巡检”上:每天早上固定时间,PRTG抽一批核心设备的流量、CPU、内存传感器,把各自的趋势图拼接成一张“总体健康图”,连同昨日告警统计推送到企业微信运维群里。值班人员早上看完这条消息,就能对前一夜的运行情况心里有数。
实现上和告警推送共用一套脚本函数,只是数据源变了。可以用Windows任务计划程序定时执行PowerShell,也可以用PRTG的“计划任务”加通知触发。拼接图片的部分,用.NET的System.Drawing画布,把多张PNG依次粘贴到一张长图上,再上传企业微信发图片消息,效果很直观。
5.2 告警分级与@责任人
每个团队都有值班表,但多数告警系统并没有真正把人串起来。企业微信应用消息支持markdown里通过@userid直接提醒负责人,也支持touser指定发送给某一个成员。我把告警分成了两级:普通告警发到运维大群,严重告警直接定向推送给当天值班人员,并在文本后面附上@值班人UserID。
这段markdown示例:
## [严重告警] 核心路由器 - 出口带宽利用率 - 当前值:**97%**,阈值为80% - 已持续:**15分钟** - <@zhangsan> 请立即确认配合企业微信App的强提醒,值班人员即使当时在看别的群,也会在顶置通知里看到这条告警。对我来说,这已经不是“做一个推送”的问题,而是把监控告警真正变成值班工作流的入口。
整套做下来,我最想跟你说的是:图文混排告警看起来只是形式优化,但它带来的实际改变是值班人员打开消息后的判断速度。从“收到一条不知道多严重的文本”到“一眼看到异常趋势图”,这种体验差距值得花时间打磨。最后再分享一个原则:所有告警脚本都要有日志、有去重、有失败处理,否则重要的告警一定会淹没在重复推送里。先把这三件事做好,再谈什么花哨的排版和自动化。