
直接把场景挑明我在公司同时维护Zabbix和Prometheus两套监控。Zabbix管基础设施和业务状态Prometheus管应用指标、Kubernetes和MySQL集群。告警消息之前全靠手机推送钉钉、邮件、Telegram机器人全都接了一圈。你以为很稳但真实情况是凌晨两点没人看手机“群消息已读”根本代表不了“处理了”。有一回一台数据库盘占用涨到95%推送确实发出去了消息还在群里置顶可值班同事和我在各自家里都没注意到直到业务方打电话来问“数据库是不是挂了”我才赶紧爬起来处理。这次事故的教训很直接监控告警的最后一公里不该只有一块手机屏幕。后来我把目光放到“能出声、能发光”的告警终端上最终定下来的是博灵语音通知终端。这类设备不依赖手机网络接进局域网后就能通过HTTP API接收告警然后用声光提醒外加TTS语音播报把监控系统里的Trigger状态变成物理世界里能直观感知的动静。这篇文章就是我接Zabbix和Prometheus到博灵终端的完整记录包括API对接方式、两边监控平台的具体配置、声光联动和TTS文案怎么设计以及上线过程中踩到的各种坑。如果你也在考虑把现有监控告警打出手机屏幕这篇应该能省你不少时间。1. 为什么我最后还是买了台“会说话”的告警终端1.1 手机推送的失效瞬间比想象中频繁得多手机推送失效并不是一次两次而是系统性的问题。我总结下来有三个典型场景。第一个是告警疲劳。监控跑久了之后每天几十条甚至上百条恢复、抖动、误报挤在同一个群里真正需要人介入的那条会被当成“又一个噪音”。我见过不止一个团队把alertname堆在同一个钉钉机器人里故障告警和INFO级定时任务结果混在一起出事时群里刷得飞快关键信息早就被顶到上面去了。第二个是渠道不靠谱。钉钉机器人、Telegram Bot、邮件网关这些通道本身又依赖外部网络和认证状态。真出大事的时候最先挂掉的可能反而是告警通道。我就遇到过企业微信机器人限流同一分钟里连续30多条告警后面的直接被拒收也遇到过邮件服务器证书过期告警邮件全部落到垃圾箱。第三个是环境没人看。大半夜里手机屏幕群消息不断但人在洗漱、在开车、在睡觉屏幕通知根本进不了注意范围。特别是当告警发生频率并不高时人对“红色气泡”的敏感度会迅速下降。等到真的严重故障发生反而不容易第一时间反应。这几个问题叠加在一起让我意识到一件事监控系统的告警链路如果只到手机为止那么“电闸跳了、手机没电了、人静音了”这三件事中只要发生一件整个监控就白搭。1.2 终端设备想解决的三个现实问题博灵语音通知终端把这些问题拆开来看其实每个都对应一个很实际的使用场景。无人值守机房。旁边没有大屏也没有人盯着终端设备放在运维办公室或走廊尽头。告警一响灯一亮人从办公室另一端也听得到。之前在多个机柜隔离开的空间里手机外放音量根本不够而终端的蜂鸣和灯光是专门为“被注意到”设计的。混合值守环境。IT和产线团队共用值班室的时候产线噪音很大手机铃声完全被盖住。声光提示比任何群消息都有效因为它是直接对着空间发的广播而不是对着个人发的通知。信息分级需求。终端不再只是响一声而是用不同灯光颜色和不同TTS文案表达严重程度。绿色是恢复黄色是预警红色是严重故障。值班人员瞟一眼灯的颜色就知道处理优先级不需要解锁手机点进群消息去翻上下文。另外这类设备还有一层隐性价值它在物理空间里形成了多人共用的信息载体。手机上通知只属于一个人你不看别人也不一定看而终端是“公共屏幕”一个人听到故障播报周围的人也会下意识去确认。这正好弥补了单人通知模式下“传达链条断裂”的问题。2. 博灵终端的HTTP API把它当作一个会说话、会发光的通用接口2.1 我假设的接口模型与调用逻辑我在接入博灵终端之前先做了一件事把它的对外交互能力当做一个标准的、会说话会发光的告警出口来理解。不同厂商的设备API有差异但绝大多数局域网语音告警设备走的是同一种套路设备上跑一个HTTP Server调用方以POST方式发送JSON报文报文里包含文本内容、TTS开关、通知级别、灯光颜色、蜂鸣方式等终端收到后按照字段内容触发语音播放和声光动作。博灵终端也基本遵循这种模型。如果你的固件版本不同字段名可能有出入但适配思路是一模一样的。一个典型的请求体长这样{ event_id: monitor-20250621-001, level: critical, text: 生产MySQL磁盘使用率超过百分之九十请立即处理, tts: true, sound: high, light: red, repeat: 3 }字段含义大致如下event_id告警事件唯一ID终端用来做去重。同样的ID重复提交时终端不会反复播报。level告警级别常见值是critical、warning、resolved终端会根据级别选择默认的灯光和播报通道。text要播报的文本也是TTS引擎的输入内容。tts是否开启语音合成。如果你只想亮灯不想打扰人可以传false。sound提示音模式有的固件支持high、low、none等参数。light灯光颜色或闪烁模式red、yellow、green、blue是常见选项。repeat播报次数用于关键告警重复提醒。如果终端不支持某个可选项一般也不会报错而是忽略未知字段最终以终端固件实际支持的参数为准。这也就是为什么第一步永远是“先发一条curl确认终端能收到”。2.2 用curl做第一次握手不管Zabbix还是Prometheus最终到终端这条路上的“最后一跳”都是HTTP请求。所以把所有平台配置放到后面先用curl把终端API打通。这一步如果过不了后面配置再完美也是白搭。curl -X POST http://bingling-terminal.local/api/v1/notify \ -H Content-Type: application/json \ -H Authorization: Bearer your-token-here \ -d { event_id: manual-test-001, level: warning, text: 这是一条来自curl的语音测试, tts: true, light: yellow, sound: low }执行之后你要观察两件事。第一终端是不是真的响了、亮了TTS是不是把中文播报出来了。第二HTTP返回体里的业务状态码是不是0。有些终端即使返回HTTP 200业务层也可能返回失败信息比如“token无效”或“文本为空”一定要看返回体内容。如果你的设备API文档里不是/api/v1/notify这个路径改成设备实际路径就行。不同固件版本之间路径差异很大有的设备用/openapi/v1/device/action有的用/api/notify但请求方式基本都是POST JSON。调通之后把这条curl保存成shell脚本后面用于验证Zabbix和Prometheus的接入链路时会反复用到。2.3 token、幂等与返回码的约定局域网设备最容易被忽略的就是认证。很多设备出厂时API默认是关闭的需要在设备管理页或配套App里打开“外部API调用”开关然后生成一个访问Token。这里有两个建议不要用同一个Token绑定多台设备每台终端单独生成一个Token方便后期单独踢掉某台设备。局域网内如果不太担心被探测可以只用Token如果设备和办公网存在交叉建议用独立VLAN或ACL限制8000等端口只能被监控服务器访问。幂等性问题也要提前设计好。HTTP调用应用层经常有重试Zabbix或Alertmanager在请求超时后会按自己的重试策略再发一次。如果你没有event_id同一个故障就会被播报两遍甚至三遍值班同事半夜会被“重复轰炸”搞得很烦躁。我常用的做法是Zabbix侧用event.idPrometheus侧用Alertmanager的fingerprint把它们塞进event_id字段。这样终端自然就具备了对同一告警事件的去重能力。下面是常见返回码的对照方便排查HTTP状态码终端返回体中的code含义处理方式2000成功无需处理200非0业务拒绝查看具体错误信息400-参数格式错误检查JSON字段名和类型401-Token无效重新生成或在API设置里检查Token404-路径不存在核对API路径和固件版本500-服务端异常保留日志联系售后或检查固件需要注意有些终端网关本身会返回500但这不是终端业务不可用而是API网关层面没找到合适路由。后面第六节我会重点讲这个坑因为Zabbix接进去时最容易遇到。3. Zabbix侧接入把Trigger状态变成TTS播报的秘密全在Webhook脚本里3.1 为什么要用Webhook脚本而不是简单URLZabbix的告警通道抽象叫“媒体类型”Media Type支持邮件、脚本、Webhook等很多种。很多人第一次接第三方系统会直接在Action里配一个“发送到URL”的远程命令或者用最简单的脚本去curl。这样也能跑通但后面维护会很痛苦因为URL、Token、JSON模板这些硬编码散落在各个Action里换一台终端要改好几处。更规范的方式是创建一个Webhook类型的Media Type把终端地址、Token、JSON拼接逻辑、返回解析逻辑全部封装在一个JavaScript脚本里。Zabbix 5.4之后这个机制很成熟脚本执行在Zabbix Server内部不需要额外在主机上装curl也天然支持Zabbix的告警事件变量。改一处Media Type所有引用它的Action自动生效。Zabbix Webhook脚本的另一个优势是能看到返回值。你可以通过return语句把终端返回的原始信息写回Zabbix的告警日志里。如果终端返回了“token错误”或“request returned 500”你在Zabbix界面直接能看到不用再去终端后台翻日志。这里也顺便说一个很多人踩过的坑在Zabbix里新建脚本类型Media Type时如果Server主机的Timeout值设置得过短而终端TTS播报处理耗时较长Zabbix可能发送请求后等不到响应就直接标记为失败。实际经验是把Zabbix Server配置里的Timeout从默认的3秒调到10秒左右会更稳。3.2 一个可以直接用的Zabbix Webhook脚本下面是我在Zabbix 7.0环境里验证过的脚本参数用Media Type里的URL和TOKEN传入这样不同环境只需要改配置不用改脚本。var params JSON.parse(value); var url params.URL; var token params.TOKEN; var levelMap { 0: info, 1: info, 2: warning, 3: warning, 4: critical, 5: critical }; var level levelMap[event.severity] || warning; var text Zabbix告警 event.host.name event.name; var payload { event_id: event.id, level: level, text: text, tts: true, light: level critical ? red : yellow, sound: level critical ? high : low, repeat: level critical ? 3 : 1 }; var request new HttpRequest(); request.addHeader(Content-Type: application/json); request.addHeader(Authorization, Bearer token); try { var response request.post(url, JSON.stringify(payload)); if (response) { var result JSON.parse(response); if (result.code 0) { return OK: response; } return ERROR: response; } return ERROR: empty response; } catch (error) { throw Failed to send to terminal: error; }脚本里用到了Zabbix Webhook默认注入的event对象。event.severity对应触发器严重程度数字0到5event.id是事件唯一IDevent.host.name是主机名event.name是触发器名称。不同Zabbix大版本里事件对象字段名可能略有差异如果你用的是5.4以前的版本可以先在脚本里加一行return JSON.stringify(event)看看实际结构再调整字段名。这段脚本把严重程度映射成终端的灯光和提示音模式critical亮红灯、高音、重复播报3遍warning亮黄灯、低音、播报1遍。恢复类通知怎么做我建议不要把恢复当成单独的警报而是在Action里也配置recovery operations脚本里根据event.source或操作类型判断把level置成resolved亮绿灯TTS播报“恢复”。这样一个故障从发生到恢复终端会有一条完整的状态闭环。3.3 在Zabbix里配置Media Type和Action的完整步骤页面上的实际操作路径是Administration → Media types → Create media type。Type选择Webhook。Name填BingLing Terminal。Script里粘贴上面的代码。Parameters区域添加两个参数URLhttp://你的终端地址/api/v1/notifyTOKEN你的Token。勾选Enabled保存。保存之后先别急着建Action点一下Media Type列表里的Send test message。这个功能会弹出一个包含模拟event对象的窗口你可以手动填id、severity、host.name、name然后点发送。如果脚本没问题终端应该马上播报测试语音Zabbix界面上会显示OK: {code:0,...}。这一步是验证脚本和网络通断最快的方式。接下来是Action侧。Configuration → Actions → Triggers actions → Create action。在Conditions里设定你要触发的条件比如Severity Warning。在Operations里选择“发送消息”收件人选一个或多个用户/用户组媒体类型勾选BingLing Terminal。建议同时配置Recovery operations这样触发恢复时也会发送一条“恢复”通知给终端。这里有一个特别隐蔽的坑Webhook媒体类型也需要“用户地址”。也就是说你必须在相应用户的Media配置里添加一条地址记录媒体类型选BingLing Terminal地址可以随便填一个固定的字符串比如terminal因为Webhook脚本最终不是靠这个地址发邮件。不少人在配置完Action后收不到告警Public用户或操作发送用户下根本没有添加地址Zabbix就不会调用Webhook。这个坑我踩过两次后来每次教同事都把这个写在第一步。4. Prometheus侧接入Alertmanager把告警“投递”到声光终端4.1 从Alertmanager到终端的整体路径Prometheus监控和Zabbix不一样它本身不做告警告警路由都靠Alertmanager。大体流程是Prometheus规则命中后生成AlertAlertmanager根据路由规则分组和收敛再通过webhook_configs把告警POST到某个HTTP端点。这里遇到的第一问题是“这个HTTP端点是谁”。如果博灵终端本身提供的就是标准Webhook接收格式那Alertmanager可以直接指向终端IP但绝大多数终端API字段和Alertmanager的webhook JSON格式不完全吻合直接裸指向终端终端可能无法解析。我的做法是在中间加一层轻量适配器让适配器把Alertmanager的JSON翻译成终端认识的字段。上面说Zabbix这层时用Zabbix自己的Webhook脚本完成翻译而Prometheus这边用一个小服务完成职责更清晰。整体链路Prometheus Server → Alertmanager → adapter翻译层 → 博灵终端HTTP API → 声光TTS按官方文档习惯Alertmanager的webhook请求体里会有status、alerts、groupLabels、commonLabels等字段其中alerts是一个数组。适应器最核心的工作就是遍历alerts数组提取labels.alertname、labels.severity、labels.instance和status然后组合成终端需要的text和level。4.2 用routes和receivers按严重级别分发Alertmanager的配置看起来复杂但如果你只关注“把告警送到终端”这一件事只需要动两个块route和receivers。下面是一个我后来一直在用的精简配置route: group_by: [alertname, instance] group_wait: 10s group_interval: 2m repeat_interval: 12h routes: - matchers: - severity ~ critical|warning receiver: bingling-terminal receivers: - name: bingling-terminal webhook_configs: - url: http://adapter:8080/hook send_resolved: true关键点有三个matchers决定了哪些告警会送到终端。我只让critical和warning进终端info级别的留给手机或看板避免夜间所有细微抖动都触发语音播报。repeat_interval: 12h很关键。Alertmanager默认对同一个告警组会反复发送通知如果没有这个限制一个持续故障每2分钟就会响一次终端值班同事会疯。12小时意味着同一个故障在未恢复时最多每天重复提醒两次左右。send_resolved: true能让恢复通知也发到终端对应终端levelresolved时亮绿光。没有这个终端恢复后灯光还是一直红着很容易让人误判。需要说明的是Alertmanager按severity匹配依赖Prometheus rule里包含这个标签。建议在Prometheus rule里统一打上labels: severity: critical这样规则配置和Alertmanager路由就能稳定协作。4.3 写一个轻量适配器把Alertmanager JSON转成终端能认的格式适配器没必要写得特别重我用Python FastAPI写了一个不到60行的服务放在内网同一个VLAN里跑。下面是核心部分from flask import Flask, request, json import urllib.request app Flask(__name__) TERMINAL_URL http://bingling-terminal.local/api/v1/notify TOKEN your-token def send_terminal(level, text, event_id, repeat): payload json.dumps({ event_id: event_id, level: level, text: text, tts: True, light: red if level critical else yellow if level warning else green, sound: high if level critical else low, repeat: repeat }).encode(utf-8) req urllib.request.Request(TERMINAL_URL, datapayload, headers{ Content-Type: application/json, Authorization: Bearer TOKEN, }) with urllib.request.urlopen(req, timeout5) as resp: return resp.status app.route(/hook, methods[POST]) def hook(): alert request.json for item in alert.get(alerts, []): labels item.get(labels, {}) status item.get(status, firing) severity labels.get(severity, warning) if severity not in (critical, warning): continue level resolved if status resolved else severity text f{labels.get(alertname)} 来自 {labels.get(instance)} event_id item.get(fingerprint, ) try: send_terminal(level, text, event_id, repeat3 if severity critical else 1) except Exception as exc: app.logger.error(terminal send failed: %s, exc) return ok这段代码在生产环境里足够用一个中等规模集群。如果你想更讲究一点可以换成线程池发送避免一条网络慢的告警阻塞后续告警也可以把终端不可用时的错误写入本地磁盘或Redis后面补发。但是对我自己的场景来说是够用的。适配器建议用gunicorn或uvicorn跑监听只绑定内网IP端口不要暴露到公网。测试时用一条模拟Alertmanager请求验证curl -X POST http://adapter:8080/hook \ -H Content-Type: application/json \ -d { status: firing, alerts: [ { labels: { alertname: HighCPULoad, instance: node-01, severity: critical }, fingerprint: test-fp-001 } ] }如果终端开始播报“HighCPULoad 来自 node-01”说明Prometheus那边只差一个配置文件重载了。5. 声光联动与TTS文案设计报警不是“响一声”就完事5.1 告警等级 → 灯光、声音、播报文本的映射表博灵终端这类设备的体验好坏很大程度上取决于声光联动策略。不是说“有告警就响”这么简单而是要让值班人员在半睡半醒状态下一听声音、一看灯色就知道要不要马上爬起来。我最后落地的映射关系是这样的级别灯光提示音TTS播报重复次数典型场景resolved绿色常亮无或柔和短音“XX告警恢复”1故障已恢复info蓝色无不播报只看灯0低优先级事件warning黄色慢闪低频短音“警告XX”1磁盘超过80%负载偏高critical红色快闪高频蜂鸣“严重告警XX请立即处理”3服务宕机、数据库不可用这里的思路是向信息密度低的方向收敛。resolved一定要给一个明显的绿灯否则故障恢复了值班人员还要跑到终端前确认状态非常浪费精力。critical则必须给人不可忽视的感觉红色快闪外加高频蜂鸣TTS播报文本里带上“严重告警”“请立即处理”这样的强指令人在半睡状态下也能快速建立优先级。5.2 TTS文案怎么写才不遭人嫌弃TTS播报和你在屏幕上看到的消息完全是两码事。屏幕信息可以很长写满所有标签和注释但语音播报只能听一遍太长的内容根本记不住反而让人烦躁。我的经验是每一条TTS文本控制在30到60个字只保留四个要素级别、对象、问题、动作。对比一下糟糕的写法“alertnameHighCPULoad, instancenode-01, jobnode-exporter, severitycritical, 当前CPU使用率95%, 超过阈值85%, 触发时间2025-06-21 02:15:04”可执行的写法“严重告警node零一CPU使用率百分之九十五请立即处理”中文TTS有一个细节很多引擎读英文和数字会很别扭。比如“HighCPULoad”会一个字母一个字母拼出来折磨人。所以我在构建TTS文本时做了两件事。第一把英文告警名映射成中文别名。在适配器里维护一个小的别名表比如HighCPULoad - CPU高负载InstanceDown - 主机离线。没有匹配到就拼原字段但尽量给核心告警配上中文别名。第二数字和特殊符号用中文表达。比如95%改写成百分之九十五node-01改写成node零一。你可以在拼接文本时用正则替换。别小看这个细节实际播放出来以后清晰度完全是两个量级。5.3 避免被同一问题轰炸的收敛策略声光终端只要在工作就是一个高存在感设备。如果收敛策略没做好它会造成新的“告警疲劳”只不过这次不是被忽略而是被“物理拔电”。所以我给终端配了三个收敛措施强烈建议你也做。第一Alertmanager侧的repeat_interval这点在前面配置里已经体现。Zabbix侧则要利用触发器里的recovery expression和problem tags尽量让同一个根因只产生一条告警而不是每个指标都触发一条。第二设置夜间静默时段。博灵终端如果支持定时静默可以直接在设备里配置如果不支持就在适配器层判断当前时间。比如每天23点到次日7点warning级别的告警只亮灯不播报critical级别才允许语音。这个策略在只有一台终端共享多个团队场景下尤其重要。第三利用event_id做幂等。前面提到过Zabbix侧用event.idPrometheus侧用fingerprint。有了这个ID终端端可以对短时间内重复的告警做合并不会同一故障播报三遍。这三个措施同时生效后我观察到的结果是值班同事不会因为夜间被过度打扰而关掉终端声音同时真正严重的故障响起时他们反而比之前更紧张因为知道“能响起来的一定不简单”。这种“稀缺感”才是声光告警设备最好的状态。6. 上线后遇到的真实报错和排查方法6.1 Zabbix Webhook报“request returned 500 internal server error”的教训我在第一次把Zabbix Media Type配置好、点下“Send test message”之后返回的内容并不是OK而是一段让我查了好几个小时的话request returned 500 internal server error for api route and version http://这句话看起来像是Zabbix在说“终端API返回500”。但奇怪的是我直接用curl调终端完全正常。同一个URLcurl可以Zabbix却500这让我一度怀疑Zabbix的Webhook脚本没有正确发送JSON。后来把Zabbix Server的日志打开再用curl -v模拟对比发现真正问题出在Zabbix的HTTP请求对象上。Zabbix Webhook的HttpRequest默认会把URL解析后重新拼一次如果URL里包含了空格、换行或者Token字段意外带了一个\nZabbix发出的请求就会被终端侧的API网关判定为“路由不存在或版本不匹配”从而返回500。我的解决办法有三个在Media Type参数里把URL和TOKEN都清理干净确认没有多余空格和换行。特别是从终端后台复制Token时不要选中后面的隐藏字符。在脚本里对url做一次trim()对Token也做同样处理。用request.post时不拼接带版本号的完整API路径而是把版本号写在URL参数里让Zabbix尽量不去改它。这个问题本质上不是Zabbix问题也不是博灵终端的问题而是中间那一层HTTP语义理解不一致造成的。遇到这类报错第一反应不要怀疑设备坏了而是先回到curl逐步对比请求头、请求体和URL定位是哪一层在“翻译”上出了问题。6.2 从curl到端到端的验证拆解排查这类集成问题我有固定的验证步骤按顺序执行能省很多时间。第一步回到curl直接打终端的API确认设备本身是正常的。如果curl已经失败先解决终端网络、Token和API路径问题。第二步用Zabbix自带的“Send test message”单独测Media Type。这步会绕过Trigger和Action只测试脚本和网络。如果测试失败问题基本确定在脚本或URL参数。第三步如果Media Type测试通过但实际触发器触发后没有播报检查Action里的Operations是否选择了正确的用户、用户组和Media Type以及那个用户是否配置了“地址”。第四步在Alertmanager这边可以先手动POST一条模拟webhook请求到适配器确认适配器能正常调用终端。然后curl一次Alertmanager的/-/reload端点或者直接kill -SIGHUP alertmanager-pid重载配置再触发一个真实告警。第五步看日志。Zabbix日志默认在/var/log/zabbix/zabbix_server.logAlertmanager的日志会打印每条webhook是否发送成功适配器日志能看到每一步的异常。我的习惯是出现问题时把这三个地方同时打开对着时间线查通常五分钟内能定位。6.3 正式上线前的一次完整演练上线前一定要做一次“人为故障注入”演练不要跳过。我自己是这么操作的先在Zabbix里手动把一台测试机的CPU负载打到阈值以上触发一个critical告警然后观察终端是否亮红灯、播报“严重告警”。接着手动恢复负载确认终端切回绿灯播报“恢复”。再模拟一次网络故障把终端网线拔掉确认Zabbix侧能显示发送失败日志且不会无限重试刷屏。最后把终端重新插上网线确认恢复后不会再重复播报之前那条告警。Prometheus侧的演练同样手动停止一个测试exporter让InstanceDown告警进入Alertmanager确认适配器把fingerprint作为event_id传给终端然后再启动exporter确认resolved消息把终端灯光切回绿色。这类演练最好安排在白天非业务高峰并且准备一个单独的测试告警规则避免把生产告警一起带出来。演练完成后把测试规则删掉或禁用观察一周实网运行数据再做下一步优化。我个人的体会是声光TTS的告警终端不是用来取代手机推送的它更像一个实体哨兵——手机通知负责“你知道有告警”终端负责“你不得不注意到告警”。两者配合告警链路才算完整。接入过程中最麻烦的不是写脚本而是各个平台之间字段、语义、重试策略的差异。但只要先用curl把最底层打通一层层往上叠最终就能得到一个半夜也能把你从座位上拽起来的告警系统。