监控告警这块,Prometheus 本身的能力大家都清楚,规则文件写起来不算复杂,但一旦告警规则多了、业务复杂了,直接在 YAML 里改规则这件事就会变得非常痛苦。我在实际运维和开发中经常被问到同一个问题:能不能给 Prometheus 告警规则加个 UI 管理能力?让开发、业务同学自己上去配阈值,不用每次都得找运维改文件、重新加载。
这篇文章就围绕这件事来写:怎么设计、怎么选型、怎么实现,以及踩过哪些坑。目标很明确,不是做一个复杂庞大的监控平台,而是轻量级地给 Prometheus 的告警规则补齐 UI 管理这层能力,让规则配置从"改文件 reload"变成"界面操作自动生效"。
1. 整体思路拆解:为什么选择"旁路管理"而不是"替换 Prometheus"
1.1 先理清楚原生告警规则的痛点
Prometheus 原生告警规则以 YAML 文件的形式存在,通过 rule_files 字段加载,配合promtool check rules做校验,修改后需要向 Prometheus 进程发送 SIGHUP 信号或调用/-/reload接口完成热加载。这套机制本身很成熟,也是官方推荐的标准做法,但放到真实团队协作场景里,问题就集中在几个地方:
第一,规则文件分散。业务多了以后,规则分散在多个 YAML 文件里,每个文件可能几百行,定位某一条规则要么靠 grep 要么靠人肉翻。第二,权限管理粗放。改规则基本等于给运维提工单,或者把 Prometheus 服务器权限开放给所有人——前者效率低,后者风险高。第三,缺少审计和版本概念。谁在什么时候改了什么,难以追踪。第四,阈值调整要反复 reload,如果多个规则文件里有语法错误,一个 reload 可能导致全部规则失效,这是很吓人的。
与其重新实现一个 Prometheus 的配置中心,不如换一个思路:保留 Prometheus 原生规则引擎和加载方式,在它的前面增加一个 UI 管理层。UI 管规则,规则生成 YAML 文件,Prometheus 继续用原生方式读取。也就是"旁路管理"架构。
1.2 旁路管理相比"直接改文件"的优势
这种方案本质上是把"人对文件的直接操作"替换成"人对 UI 的操作 + 系统对文件的原子化更新"。
它带来的直接好处是:规则可以按业务线、按团队分类管理,每个人只能操作自己权限范围内的规则;每次修改都会留下操作记录;规则在 UI 侧做完语法校验和 PromQL 表达式校验,降低把错误配置推到生产环境的概率。
这里有一个关键设计决策:不修改 Prometheus 进程本身,不侵入代码,也不引入一个新的时序存储。Prometheus 负责采集和告警计算,UI 管理平台只负责规则的 CRUD 和文件同步。这样做的风险最小,Prometheus 版本升级、配置变更都不会影响这套管理平台的存在价值。
我选择把这个管理平台做成一个独立服务,而不是塞进 Prometheus 里面,也是出于解耦的考虑。Prometheus 是状态无关的拉模型和规则引擎,它不需要关心规则的"来源"是什么,只需要在启动时或 reload 时能读到合法的规则文件就行。管理平台只是规则的"生产者"。
2. 核心细节解析与实操要点
2.1 规则管理的数据模型设计
既然要管理告警规则,第一件事是把规则结构化,而不是继续停留在"一段 YAML 文本"的粒度上。数据模型设计得对不对,直接决定 UI 的体验和后续扩展空间。
我设计的核心模型分三层:
- RuleGroup(规则组):对应 Prometheus 原生规则中的 group 概念,包含 group_name、interval、rules 列表。这是 UI 列表页的第一层展示单元。
- Rule(规则对象):包含 alert 名称、expr 表达式、for 持续时间、labels 标签、annotations 注解。
- Namespace(命名空间):这是我自己加的一层抽象,用于按团队或业务线隔离规则。比如"电商组"、"数据库组",每个命名空间下可以有多个 RuleGroup。
这里分享一个细节经验:labels和annotations在设计时要分开存储,不能整体当一个字符串处理。因为在实际场景中,排障和路由需要依赖 labels 里的关键字段(比如 severity、team、env),而 annotations 更多是展示信息。如果把它们合并存储,后面做搜索、过滤、权限匹配都会非常难受。
Prometheus 原生规则里的expr是 PromQL 表达式,这块也是大多数人写规则时最容易出错的地方。UI 层可以在前端引入一个轻量级的 PromQL 校验逻辑(比如在保存前调用后端接口执行promtool check rules或使用 promql-parser),而不是等规则写入后再让 Prometheus reload 才发现问题。
2.2 规则文件的读写与原子更新
这是整个方案中技术风险最高的部分。规则的最终载体是磁盘上的 YAML 文件,Prometheus 进程会读取并解析这些文件。如果写入过程中出现半截内容、非法的 YAML 结构,Prometheus reload 后最轻是报错拒载,重则影响所有告警计算。
我强烈建议采用"临时文件 + rename"的原子写方案,而不是直接打开原文件写入。具体流程是:
- 将所有规则内容在内存中组装成完整的 YAML 结构。
- 先写入一个临时文件,例如
.rules.tmp.xxxx,写入完成后 flush 到磁盘。 - 用
os.rename把临时文件替换为目标规则文件。 - 调用 Prometheus reload 接口。
- reload 后立即检查 Prometheus 的
/api/v1/rules接口或/-/ready状态,确认规则加载无误。
tmp 到目标的 rename 在同一个文件系统内是原子的,不会存在"写入一半被读取"的情况。这里有个容易忽略的坑:Prometheus 对规则文件路径的反斜杠、目录权限很敏感,管理平台运行用户必须有规则文件所在目录的写权限,但管理平台 Web 服务本身不应该以 root 运行。我会在后面的部署部分详细给出权限配置建议。
2.3 配置文件结构与管理平台职责切割
Prometheus 自身的prometheus.yml里通过rule_files指定规则文件路径。管理平台的职责是维护这些路径指向的内容,而不是修改rule_files本身。
我遇到过一些团队把规则文件路径直接指向一个"由管理平台全量生成的单一文件",这种做法在小规模下没问题,但规则多了以后每次全量生成都会造成无谓的 reload。更好的做法是:按命名空间拆分成多个规则文件,管理平台只更新变更涉及的规则文件,Prometheus 每次 reload 仍然会重新读取所有规则文件,但至少写入量变小、diff 更清晰。
这里还要注意:Prometheus reload 是全局的。即使你只改了一个规则文件,Prometheus 也会重新加载全部规则文件并重新计算所有规则的状态。所以不要把 reload 频率设计得太激进,比如每次用户敲一个字就自动保存 reload 这种交互就不可取。我一般建议 UI 采用"编辑 + 保存/发布"两步操作,保存写入草稿,发布才触发 reload。
3. 实操过程与核心环节实现
3.1 前后端技术选型
在技术选型上,我走了不少弯路。最早我的想法是直接用 Python + Flask 做个简易 API,前端用 Vue 的 Element Plus 快速搭一套表单。但实际用下来发现,规则管理场景中前端需要频繁地处理嵌套结构(group 下面有 rules,rules 下面有 labels 和 annotations),如果前端数据结构设计得不够稳,后面改起来会想哭。
前端我最终选择了Vue 3 + Element Plus + Monaco Editor。选 Vue 纯粹是团队熟悉度和生态成熟度,核心是 Monaco Editor——它支持 PromQL 高亮,配合一个 PromQL 自动补全插件,用户在编辑告警表达式时体验和写代码差不多,大幅降低表达式写错的概率。如果你团队更擅长 React,用 React + Ant Design + Monaco 也一样,架构上不会因此有任何约束。
后端我选择了Go。原因很简单:Prometheus 本身就是 Go 写的,Go 的gopkg.in/yaml.v3处理 YAML 结构体序列化非常顺手;而且我们需要调用promtool做规则校验,Go 的promtool是按包组织的,可以直接以库的方式嵌入校验逻辑,不需要额外启动子进程。如果你用 Python,用 subprocess 调 promtool 也能行,但每次校验都 fork 一个进程,性能上差点意思。
存储层没有引入 MySQL 或 PostgreSQL,初期直接用 SQLite 作为规则元数据的存储。每条规则的真实内容最终在 YAML 文件里,SQLite 存的是规则的"台账信息":命名空间、规则组名、报警名称、变更记录、操作人、更新时间。为什么不做成"数据库为主、文件为从"?因为 Prometheus 只认文件,管理平台必须保证文件是最终事实源。数据库只是索引和审计层。这套设计在规则数量几百上千时完全够用。
3.2 规则文件同步模块的关键代码思路
我直接贴核心的同步逻辑伪代码,并解释为什么这样写。代码不重要,思路才是核心。
type RuleFileContent struct { Groups []RuleGroup `json:"groups" yaml:"groups"` } func (s *Service) SyncRuleFile(namespace string) error { rules, err := s.LoadRulesByNamespace(namespace) if err != nil { return err } content := buildYAMLContent(rules) tmpPath := filepath.Join(s.ruleDir, ".tmp-"+namespace+"-"+randomString(6)) finalPath := filepath.Join(s.ruleDir, "rules-"+namespace+".yml") // 写入临时文件 if err := os.WriteFile(tmpPath, content, 0644); err != nil { return err } // 原子替换 if err := os.Rename(tmpPath, finalPath); err != nil { return err } // 触发 reload return s.TriggerReload() } func (s *Service) TriggerReload() error { resp, err := http.Post(s.prometheusAddr+"/-/reload", "application/json", nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("reload failed: %d", resp.StatusCode) } // 等待几秒后拉取规则状态确认加载成功 time.Sleep(3 * time.Second) return s.CheckRulesLoaded() }这里有一个细节值得展开:TriggerReload后面的 3 秒 sleep 和CheckRulesLoaded。我踩过的坑是,Prometheus 的/-/reload接口非常"心大",只要配置没有语法错误,它就返回 200,但规则计算是一个异步的过程。如果规则文件里存在语义错误(比如引用了不存在的 label),Prometheus 并不会在 reload 时立刻暴露,而是在规则评估周期里持续报错。
所以,管理平台不能只在写入和 reload 后就不管了。我实现了一个"发布后体检"逻辑:reload 后调用/api/v1/rules,检查目标规则是否出现在返回的 rules 数组中,并对比规则名称和 expected 数量;同时拉取/api/v1/alertmanager状态确认告警路由正常。这一步在初期可以手动做,但建议做成自动化,哪怕只是 30 秒跑一次对账任务。
3.3 UI 层的操作流程建模
UI 表面上看是给用户填表单、点按钮,但背后需要把用户的交互行为映射成规则的内部状态。我设计的操作流程是:
第一,命名空间选择。用户进入管理界面后先选择业务线,这个动作不是装饰,它是后续所有操作的权限上下文,也是文件隔离的依据。
第二,规则组列表。在命名空间内展示规则组,每条规则组显示名称、更新时间和规则数量。用户可以新建组、修改评估周期 interval。interval 我直接透传给了 Prometheus,不做限制,方便应对不同的业务敏感度需求。
第三,规则编辑器。编辑单条规则时,页面主要有四块:基础信息区(规则名称、for 时间)、PromQL 表达式区(Monaco Editor 编辑)、标签区(labels 的 key-value 动态表单)、注解区(annotations 的 key-value 动态表单)。动态表单这块是 UI 易用性的分水岭。如果用静态表单,用户只能填固定的几个字段,遇到需要加自定义 label 的场景就懵了。我采用的是"动态 key-value 行 + 每行一个删除按钮"的结构,用户可以随意增删。
第四,保存与发布。保存只是把 UI 表单状态写入 SQLite,发布才生成 YAML 并触发 reload。在"保存"和"发布"之间还插了一个校验步骤:调用 promtool 的规则校验接口,如果校验失败,发布按钮置灰并展示错误信息。
这个流程做出来以后,我明显感觉到开发和业务的反馈变得非常好:改阈值再也不用等运维走流程了,用户在 UI 上改完点一下发布,几十秒后告警规则就生效了。当然,前提是权限架构撑得住。
4. 常见问题与排查技巧实录
4.1 规则写入了但 Prometheus 没生效
这是我被问得最多的问题,没有之一。现象是:UI 上明明显示发布成功,管理平台的 SQLite 里也确实存了新规则,但到 Prometheus 的 Rules 页面看,还是旧规则。
排查路径非常固定。先确认rule_files路径是否指向了管理平台生成的文件目录。很多人配置时把prometheus.yml里的 rule_files 写成了相对路径,而管理平台写入的是绝对路径,两个路径不匹配,Prometheus 读的压根不是你写的那份文件。检查 Prometheus 的启动参数--config.file和rule_files的组合,确认路径解析后的结果是同一物理文件。
第二个容易被忽略的点是 reload 权限问题。Prometheus 从 2.x 某个版本开始,/-/reload接口不再是默认开放的,需要启动参数开启 web lifecycle API。如果你是直接给 Prometheus 发送 SIGHUP 信号,那管理平台所在容器必须和 Prometheus 进程共享 PID namespace,或者管理平台需要有权限执行 kill 命令。这个坑我在 K8s 环境里踩得很深:管理平台 Deployment 和 Prometheus 是两个独立容器,SIGHUP 根本发不进去,最后统一改走 HTTP reload 接口才解决。
第三个点是 reload 接口返回 200 但实际没生效。这种情况通常是 YAML 里存在多个同名规则组,Prometheus 在加载时只会保留其中一个,而管理平台校验时没有做全局查重。我在管理平台里加了"相同 namespace + group_name + alert 名称不能重复"的约束,并把规则内容的 hash 存下来,发布时先做一次内容 diff,如果 hash 完全一致就不触发 reload。这个设计一方面是避免无谓 reload,另一方面也能在"用户点了发布但什么都没变"时给出明确提示。
4.2 高并发编辑下的写冲突
团队规模一大,就可能出现两个用户同时编辑不同命名空间下的规则,或者一个用户编辑规则 A、另一个用户编辑同一命名空间下的规则 B。由于最终同步是"整个命名空间生成一个 YAML 文件",后发布的用户会把先发布的内容默默覆盖掉。
这不是技术难题,但设计上很容易遗漏。我给出的方案是基于命名空间的乐观锁。每次用户从 UI 加载命名空间时,后端返回一个revision标识(可以是最后更新时间的毫秒值或自增版本号);用户点击发布时带上这个revision,后端在写文件前检查当前数据库里的revision是否和提交的一致,不一致就返回 409 冲突,前端弹窗提示"当前规则已被他人修改,请刷新页面后重新编辑"。
还有一个并发相关的隐患:管理平台后端在生成 YAML 时,用的内存数据可能是过期的。比如用户 A 加载了规则列表,用户 B 删除了其中一条规则,A 随后发布整个命名空间,把 B 的删除覆盖成"恢复"。要彻底解决这个问题,不应该让前端提交整个规则组的全量数据,而是提交结构化操作指令:新增规则 X、修改规则 Y、删除规则 Z。后端逐条应用操作,最终 YAML 永远基于数据库最新状态生成。全量提交在技术上是省事,但在并发场景下会出大事。
补充一个经验:YAML 文件的生成要保证字段顺序稳定。Prometheus 对规则的字段顺序不敏感,但人肉 diff 时会非常敏感。我用 Go 结构体序列化的方式,结构体字段顺序固定,同一份数据生成出来的 YAML 字节级一致,方便自动化对比。
4.3 UI 优化:规则多了以后页面卡顿问题
热门搜索词里连续出现"ui界面卡顿"和"ui框架",说明大家都在实际使用中遇到了性能瓶颈。规则管理界面也不例外。
我遇到过的情况是:一个命名空间下有 200 多条规则,前端渲染的是一个巨大的动态表单表格,每条规则展开后有 5 个以上的输入框,Vue 的响应式系统在这种规模下明显吃力,输入字符时能感觉到延迟。
解决思路不是优化单个组件的渲染,而是改变交互模式。列表页只显示规则名、表达式摘要、更新时间和一个"编辑"按钮,点击编辑才加载完整表单。这样页面上同时存在的重型组件数量从 200 多个降到了个位数,卡顿自然消失。
另外我建议在前端引入防抖。特别是 PromQL 表达式输入框,不要每次击键都触发校验请求,而是 500ms 防抖后再调用后端校验接口。批量修改严重程度等级这类通用操作,直接做成"勾选多条规则 + 批量更新标签"的交互,避免用户逐条打开编辑。
4.4 Prometheus 升级后的兼容性
Prometheus 版本升级是一个容易炸雷的事件,尤其是告警规则格式的相关变更。比如for字段的类型、labels里保留关键字的行为、PromQL 函数名的变更,都可能导致老规则在新版本上出现异常。
我的管理平台在每次生成 YAML 前会把 Prometheus 版本号和服务端能力做个映射,不同的版本对应不同的规则格式模板。这个听起来复杂,但实际上只需要在配置里维护一个"最低支持版本"和"需要禁用的字段列表"即可。新版本如果引入新的规则字段(例如某些实验性参数),管理平台在 UI 上会标注"当前环境不支持",避免用户配置了无法生效的字段。
还有一个小技巧:每次 Prometheus 升级前,用管理平台把所有规则导出,在新版本环境上执行promtool check rules批量校验,把校验结果自动生成一个兼容性报告。这个报告能在升级前暴露潜在问题,而不是等生产环境告警瘫痪后才排查。
5. 从零部署这套规则 UI 管理平台
5.1 快速上手的目录结构和配置
下面给出一套可以快速复现的目录结构。这套结构我用了很长时间,改动不大,越简单越不容易出错。
alert-rules-ui/ ├── main.go # 后端入口 ├── build/ ├── configs/ │ ├── config.yaml # 管理平台自身配置 │ └── prometheus.yml # 通过模板管理 prometheus 配置 ├── internal/ │ ├── api/ # HTTP handler │ ├── model/ # 规则模型与 YAML 序列化 │ ├── service/ # 业务逻辑 │ ├── store/ # SQLite 存储 │ └── sync/ # 文件同步与 reload 逻辑 ├── ui/ │ ├── src/ │ ├── views/ # Vue 页面 │ └── package.json └── rules/ ├── rules-common.yml └── rules-payment.yml管理平台配置文件config.yaml至少要包含以下几项:
server: listen: ":8080" prometheus: addr: "http://127.0.0.1:9090" rules_dir: "/data/prometheus/rules" reload_method: "http" # 可选 http 或 signal config_file: "/etc/prometheus/prometheus.yml" storage: db_path: "/data/alert-rules-ui/data.db" auth: mode: "simple" token: "xxxx"这其中的reload_method: http很关键。默认走 HTTP 调用/-/reload,如果 Prometheus 没开启 web lifecycle API,就切换到signal方式。我见过有的部署里把 reload 权限直接交给了管理平台的 service 账号,这样 agent 端就不需要额外的权限。
5.2 Prometheus 侧的配置配合
Prometheus 侧需要做的最小改动是把 rule_files 指向管理平台管理的目录。示例:
rule_files: - "/data/prometheus/rules/*.yml"记得在启动 Prometheus 时加上--web.enable-lifecycle参数,否则管理平台调/-/reload会收到 403 响应。如果你用的是 systemd 管理 Prometheus 进程,也可以考虑让管理平台通过 systemd-run 或者 DBus 发送 reload 信号,但这条路径相对复杂,非必要不推荐。
权限配合的基本思路是:Prometheus 进程运行用户是prometheus,管理平台进程运行用户是prometheus-ui。把rules目录的属主设为prometheus-ui,并把目录组设为prometheus且权限为 750。这样管理平台可以写文件,Prometheus 可以读文件,但普通用户不能直接摸到规则文件。有人说"那 Prometheus 用户没有写权限,挂载时会不会有问题",不会,Prometheus 只会读取规则文件,不会写回。
5.3 用 Docker Compose 一步启动演示环境
对于想先跑起来看看效果的同学,我给一个演示环境的 Docker Compose,这套可以放在本地验证,但别直接照搬到生产。
version: "3.8" services: prometheus: image: prom/prometheus:latest container_name: prometheus volumes: - "./prometheus.yml:/etc/prometheus/prometheus.yml" - "./rules:/data/prometheus/rules" command: - "--config.file=/etc/prometheus/prometheus.yml" - "--web.enable-lifecycle" ports: - "9090:9090" rule-ui: build: . container_name: rule-ui depends_on: - prometheus volumes: - "./rules:/data/prometheus/rules" environment: - PROMETHEUS_ADDR=http://prometheus:9090 - RULES_DIR=/data/prometheus/rules ports: - "8080:8080"有一点必须强调:容器内的/data/prometheus/rules和宿主机./rules用的是同一个 volume 映射,这是为了保证"同一份规则文件,两个容器都能访问"。如果不共享这个目录,管理平台写入的规则 Prometheus 根本看不见,所有的 UI 操作都是自娱自乐。
5.4 上线前要做的检查清单
我把上线前检查清单整理成一个表,安全起见,每一条都别跳过。
| 检查项 | 检查方法 | 期望结果 |
|---|---|---|
| rule_files 路径一致性 | 在容器和宿主机分别查看路径 | 指向同一物理文件(硬链接或同 volume) |
| reload 接口鉴权 | curl 调用 /-/reload | 返回 200,且规则生效 |
| 规则目录写权限 | 用 UI 创建一条测试规则 | 文件生成成功,Prometheus 识别 |
| 并行编辑冲突 | 模拟双用户同时编辑 | 后提交者收到 409 提示 |
| 规则校验失败拦截 | UI 输入一个非法 PromQL | 发布被拦截,错误信息可读 |
| 审计日志 | 查看 SQLite 变更记录表 | 操作人、时间、变更内容完整 |
这个清单是血的教训总结。我早期上线时跳过过一次"reload 接口鉴权"的检查,结果管理平台调接口一直返回 403,排查了半天才发现 Prometheus 启动参数里忘了加--web.enable-lifecycle。这类问题如果上线前检查一遍,十几分钟就能兜底。
6. 写在最后的实践心得
这套规则 UI 管理平台从最初的一个小工具,慢慢变成团队监控告警体系的标配。给我的最大感受是:管理告警规则这件事,技术难度不大,真正的复杂度在于规则状态的管理和团队协作的约束。
如果你只在单机环境用 Prometheus,其实直接在文件里维护规则就够了,没必要专门做一个 UI。但当规则数量超过几十条、业务线超过两三个、需要多人协作时,UI 管理带来的效率提升和错误拦截会非常明显。
另外我强烈建议把 PromQL 表达式的测试功能也集成到管理平台里。用户写了一个表达式,可以在 UI 里输入一个测试查询的时间范围,后端调用 Prometheus 的/api/v1/query_range拉出数据曲线预览,直观判断阈值是否合理。这算是一个 low code 式的增强,但用户体验的提升非常明显——很多用户看到曲线图后会意识到"这个阈值写得太低了,随便一个波动就会告警",这在纯文本编辑的场景下是发现不了的。
最后再分享一个小经验:管理平台的 UI 不要追求大而全。刚开始我也想过把 Dashboard、告警静默、通知路由全部塞进去,后来发现这会让项目的边界失控。定位就是"规则文件的可视化编辑器和审计层",守好这一亩三分地,比什么都强。后续如果真有需求,单独做 Alertmanager 的静默管理页面,也是另一个独立项目了。