教你如何在 AlertManager 报警通知中展示监控图表

本文介绍了如何使用Promoter直接绘制报警图表并上传至对象存储,然后在钉钉消息中展示,以提供更友好的报警通知体验。Promoter通过Prometheus API获取数据,使用gonum库绘制图表,并支持自定义模板和样式。部署和配置过程也在文中详细说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

导读今天换了另外一种方式来实现,直接去绘制渲染报警图表,然后上传到对象存储中保存起来,在钉钉中就可以直接展示了。Promoter 就是这个方案的一个实现。

之前用 Python 实现了一个非常简陋的 AlertManager 的钉钉接收器,一直想在钉钉的消息通知中将当前报警图表也展示出来,这样显然对用户来说更加友好。之前想的思路是通过爬虫的方式去 Prometheus 页面将 Graph 图形截图保存下来,该方式理论上确实是可行的,但是这种方式不稳定因素较多,而且会占用大量的资源。

今天换了另外一种方式来实现,直接去绘制渲染报警图表,然后上传到对象存储中保存起来,在钉钉中就可以直接展示了,Promoter 就是这个方案的一个实现,支持在消息通知中展示实时报警图表,效果图如下所示:

目前是将报警数据渲染成图片后上传到 S3 对象存储,所以需要配置一个对象存储(阿里云 OSS 也可以),此外消息通知展示样式支持模板定制.

模板

默认模板位于 template/default.tmpl,可以根据自己需求定制:

{{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }}
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver }}{{ end }}
{{ define "default.__text_alert_list" }}{{ range . }}
### {{ .Annotations.summary }}
**详情:** {{ .Annotations.description }}
{{ range .Images }}
**条件:** `{{ .Title }}`
![📈]({{ .Url }})
{{- end }}
**标签:**
{{ range .Labels.SortedPairs }}{{ if and (ne (.Name) "severity") (ne (.Name) "summary") }}> - {{ .Name }}: {{ .Value | markdown | html }}
{{ end }}{{ end }}
{{ end }}{{ end }}
{{/* Default */}}
{{ define "default.title" }}{{ template "__subject" . }}{{ end }}
{{ define "default.content" }}
{{ if gt (len .Alerts.Firing) 0 -}}
#### **{{ .Alerts.Firing | len }} 条报警**
{{ template "default.__text_alert_list" .Alerts.Firing }}
{{ range .AtMobiles }}@{{ . }}{{ end }}
{{- end }}
{{ if gt (len .Alerts.Resolved) 0 -}}
#### **{{ .Alerts.Resolved | len }} 条报警恢复**
{{ template "default.__text_alert_list" .Alerts.Resolved }}
{{ range .AtMobiles }}@{{ . }}{{ end }}
{{- end }}
{{- end }}

部署

默认配置文件如下所示,放置在 /etc/promoter/config.yaml:

debug: true
http_port: 8080
timeout: 5s
prometheus_url:   # Prometheus 的地址
metric_resolution: 100
s3:
  access_key:   
  secret_key: 
  endpoint: oss-cn-beijing.aliyuncs.com
  region: cn-beijing
  bucket: 
dingtalk:
  url: https://oapi.dingtalk.com/robot/send?access_token=
  secret:   # secret for signature

可以直接使用 Docker 镜像 cnych/promoter:v0.1.1 部署,在 Kubernetes 中部署可以直接参考 deploy/kubernetes/promoter.yaml。

启动完成后在 AlertManager 配置中指定 Webhook 地址即可:

route:
  group_by: ['alertname', 'cluster']
  group_wait: 30s
  group_interval: 2m
  repeat_interval: 1h
  receiver: webhook
receivers:
- name: 'webhook'
  webhook_configs:
  - url: 'http://promoter.kube-mon.svc.cluster.local:8080/webhook'  # 配置 promoter 的 webhook 接口
    send_resolved: true

核心原理

该项目采用 golang 实现,Webhook 的实现很简单,www.linuxprobe.com这里的核心部分是如何渲染监控图表,核心方式是通过 Prometheus 的 API 接口来获取查询的指标数据:

func Metrics(server, query string, queryTime time.Time, duration, step time.Duration) (promModel.Matrix, error) {
 client, err := prometheus.NewClient(prometheus.Config{Address: server})
 if err != nil {
  return nil, fmt.Errorf("failed to create Prometheus client: %v", err)
 }
 api := prometheusApi.NewAPI(client)
 value, _, err := api.QueryRange(context.Background(), query, prometheusApi.Range{
  Start: queryTime.Add(-duration),
  End:   queryTime,
  Step:  duration / step,
 })
 if err != nil {
  return nil, fmt.Errorf("failed to query Prometheus: %v", err)
 }
 metrics, ok := value.(promModel.Matrix)
 if !ok {
  return nil, fmt.Errorf("unsupported result format: %s", value.Type().String())
 }
 return metrics, nil
}

然后将获取的指标绘制出来,图形绘制使用的 gonum.org/v1/plot 这个包来实现的:

func PlotMetric(metrics promModel.Matrix, level float64, direction string) (io.WriterTo, error) {
 p, err := plot.New()
 if err != nil {
  return nil, fmt.Errorf("failed to create new plot: %v", err)
 }
 textFont, err := vg.MakeFont("Helvetica", 3*vg.Millimeter)
 if err != nil {
  return nil, fmt.Errorf("failed to load font: %v", err)
 }
 evalTextFont, err := vg.MakeFont("Helvetica", 5*vg.Millimeter)
 if err != nil {
  return nil, fmt.Errorf("failed to load font: %v", err)
 }
 evalTextStyle := draw.TextStyle{
  Color:  color.NRGBA{A: 150},
  Font:   evalTextFont,
  XAlign: draw.XRight,
  YAlign: draw.YBottom,
 }
 p.X.Tick.Marker = plot.TimeTicks{Format: "15:04:05"}
 p.X.Tick.Label.Font = textFont
 p.Y.Tick.Label.Font = textFont
 p.Legend.Font = textFont
 p.Legend.Top = true
 p.Legend.YOffs = 15 * vg.Millimeter
 // Color palette for drawing lines
 paletteSize := 8
 palette, err := brewer.GetPalette(brewer.TypeAny, "Dark2", paletteSize)
 if err != nil {
  return nil, fmt.Errorf("failed to get color palette: %v", err)
 }
 colors := palette.Colors()
 var lastEvalValue float64
 for s, sample := range metrics {
  data := make(plotter.XYs, 0)
  for _, v := range sample.Values {
   fs := v.Value.String()
   if fs == "NaN" {
    _, err := drawLine(data, colors, s, paletteSize, p, metrics, sample)
    if err != nil {
     return nil, err
    }
    data = make(plotter.XYs, 0)
    continue
   }
   f, err := strconv.ParseFloat(fs, 64)
   if err != nil {
    return nil, fmt.Errorf("sample value not float: %s", v.Value.String())
   }
   data = append(data, plotter.XY{X: float64(v.Timestamp.Unix()), Y: f})
   lastEvalValue = f
  }
  _, err := drawLine(data, colors, s, paletteSize, p, metrics, sample)
  if err != nil {
   return nil, err
  }
 }
 var polygonPoints plotter.XYs
 if direction == "<" {
  polygonPoints = plotter.XYs{{X: p.X.Min, Y: level}, {X: p.X.Max, Y: level}, {X: p.X.Max, Y: p.Y.Min}, {X: p.X.Min, Y: p.Y.Min}}
 } else {
  polygonPoints = plotter.XYs{{X: p.X.Min, Y: level}, {X: p.X.Max, Y: level}, {X: p.X.Max, Y: p.Y.Max}, {X: p.X.Min, Y: p.Y.Max}}
 }
 poly, err := plotter.NewPolygon(polygonPoints)
 if err != nil {
  return nil, err
 }
 poly.Color = color.NRGBA{R: 255, A: 40}
 poly.LineStyle.Color = color.NRGBA{R: 0, A: 0}
 p.Add(poly)
 p.Add(plotter.NewGrid())
 // Draw plot in canvas with margin
 margin := 6 * vg.Millimeter
 width := 20 * vg.Centimeter
 height := 10 * vg.Centimeter
 c, err := draw.NewFormattedCanvas(width, height, "png")
 if err != nil {
  return nil, fmt.Errorf("failed to create canvas: %v", err)
 }
 cropedCanvas := draw.Crop(draw.New(c), margin, -margin, margin, -margin)
 p.Draw(cropedCanvas)
 // Draw last evaluated value
 evalText := fmt.Sprintf("latest evaluation: %.2f", lastEvalValue)
 plotterCanvas := p.DataCanvas(cropedCanvas)
 trX, trY := p.Transforms(&plotterCanvas)
 evalRectangle := evalTextStyle.Rectangle(evalText)
 points := []vg.Point{
  {X: trX(p.X.Max) + evalRectangle.Min.X - 8*vg.Millimeter, Y: trY(lastEvalValue) + evalRectangle.Min.Y - vg.Millimeter},
  {X: trX(p.X.Max) + evalRectangle.Min.X - 8*vg.Millimeter, Y: trY(lastEvalValue) + evalRectangle.Max.Y + vg.Millimeter},
  {X: trX(p.X.Max) + evalRectangle.Max.X - 6*vg.Millimeter, Y: trY(lastEvalValue) + evalRectangle.Max.Y + vg.Millimeter},
  {X: trX(p.X.Max) + evalRectangle.Max.X - 6*vg.Millimeter, Y: trY(lastEvalValue) + evalRectangle.Min.Y - vg.Millimeter},
 }
 plotterCanvas.FillPolygon(color.NRGBA{R: 255, G: 255, B: 255, A: 90}, points)
 plotterCanvas.FillText(evalTextStyle, vg.Point{X: trX(p.X.Max) - 6*vg.Millimeter, Y: trY(lastEvalValue)}, evalText)
 return c, nil
}
func drawLine(data plotter.XYs, colors []color.Color, s int, paletteSize int, p *plot.Plot, metrics promModel.Matrix, sample *promModel.SampleStream) (*plotter.Line, error) {
 var l *plotter.Line
 var err error
 if len(data) > 0 {
  l, err = plotter.NewLine(data)
  if err != nil {
   return &plotter.Line{}, fmt.Errorf("failed to create line: %v", err)
  }
  l.LineStyle.Width = vg.Points(1)
  l.LineStyle.Color = colors[s%paletteSize]
  p.Add(l)
  if len(metrics) > 1 {
   m := labelText.FindStringSubmatch(sample.Metric.String())
   if m != nil {
    p.Legend.Add(m[1], l)
   }
  }
 }
 return l, nil
}

<think>嗯,用户问的是为什么Grafana已经有报警功能了,还需要用Alertmanager。我需要先理清楚两者各自的定位和功能差异。首先,Grafana的报警功能主要是针对监控数据的可视化,可以设置基于面板图表的阈值规则,触发警报后通过邮件、Slack等方式通知。而Alertmanager是Prometheus生态中的组件,专门处理报警的去重、分组、静默、路由等。 接下来,用户可能不太清楚两者在功能上的互补性。比如,Grafana虽然能触发报警,但在复杂的路由和去重处理上可能不如Alertmanager强大。此外,Alertmanager支持多数据源的聚合,而Grafana的报警可能局限于其自身的数据源。用户可能在环境中同时使用多种监控工具,这时候Alertmanager可以作为统一的报警处理中心。 还需要考虑用户的实际使用场景。如果他们只用Grafana做监控,可能Alertmanager不是必须的。但若他们使用Prometheus等组合,Alertmanager的作用就凸显出来了。用户可能没有意识到在大型分布式系统中,高效管理大量报警的需求,这时候分组、抑制、路由等功能就非常重要。 另外,需要解释两者的集成关系。比如,Grafana的报警可以配置成发送到Alertmanager,这样结合两者的优势。可能用户以为必须二选一,但实际上可以协同工作。需要举例说明,比如Grafana处理可视化报警触发,Alertmanager处理后续的派发和优化。 还要注意用户的技术水平,避免使用过于专业的术语,但又要准确。可能需要分点说明,清晰对比两者的职责和优势,帮助用户理解为什么在某些情况下需要同时使用两者。</think>Grafana的报警功能和Alertmanager的设计定位存在差异,两者在监控体系中是互补关系而非替代关系。以下是具体原因分析: 1. **职责定位不同** - **Grafana报警**:核心功能是**基于可视化数据的阈值触发** - 依赖Dashboard面板的查询结果 - 主要支持静态规则(如CPU>80%持续5分钟) - 报警触发后直接发送通知到指定渠道 - **Alertmanager**:核心功能是**报警事件管理** - 不直接产生报警,而是接收来自多个源的报警(如Prometheus、Grafana等) - 专注于报警的**路由、分组、去重、静默**等高级管理 2. **复杂场景处理能力差异** - **分组降噪**:当多个服务同时故障时 ```bash # Alertmanager配置示例:按集群分组报警 route: group_by: ['cluster'] group_wait: 30s # 等待30秒收集同集群报警 ``` Grafana原生不支持此类分组策略 - **抑制规则**:可实现层级化报警抑制 ```bash # 当数据库宕机时,抑制相关应用报警 inhibit_rules: - source_match: severity="critical" target_match: severity="warning" equal: ['db_instance'] ``` - **多路路由**:根据标签将报警分派给不同团队 ```bash routes: - match: team=frontend receiver: fe-slack - match: team=backend receiver: be-webhook ``` 3. **生态系统整合需求** - **多数据源聚合**:Alertmanager可统一处理来自Prometheus、Grafana、Thanos等不同系统的报警 - **告警生命周期管理**:与Prometheus Alert Rules形成完整工作流 - **标准化接口**:通过Webhook对接各类通知渠道(如PagerDuty、OpsGenie) **典型协作流程示例**: ``` Grafana检测阈值超标 → 发送报警AlertmanagerAlertmanager去重分组 → 通过Slack发送给值班团队 ``` **选择建议**: - 简单场景:仅需基础报警 → 单独使用Grafana - 复杂场景:多数据源/需要精细管理 → Grafana+Alertmanager组合 - 混合架构:Grafana负责可视化报警配置,Alertmanager负责事件处理 **最终决策矩阵**: $$ \begin{array}{|c|c|c|} \hline \text{需求特征} & \text{推荐方案} \\ \hline \text{单一数据源+简单通知} & \text{仅Grafana} \\ \hline \text{多系统聚合+复杂路由} & \text{Grafana+Alertmanager} \\ \hline \text{已有Prometheus生态} & \text{必须使用Alertmanager} \\ \hline \end{array} $$ 两者的组合使用可以同时发挥Grafana强大的可视化监控能力和Alertmanager专业的报警管理能力,形成完整的监控告警闭环。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值