Render Real‑Time Alert Charts in DingTalk with Promoter – A Go Solution
This article explains how to programmatically render Prometheus alert charts, upload them to object storage, and embed the images in DingTalk notifications using the Go‑based Promoter tool, including template customization, deployment steps, and core rendering logic.
Previously I built a simple AlertManager DingTalk receiver in Python, but wanted to embed the alert chart directly in DingTalk notifications. The initial approach used a web scraper to capture Prometheus graph screenshots, which was unstable and resource‑intensive.
Now I switched to rendering the chart programmatically, uploading it to object storage, and displaying it in DingTalk. The implementation, called Promoter, supports real‑time alert charts in notifications, as shown below.
Promoter renders alert data into an image and stores it in an S3‑compatible object store (e.g., Alibaba Cloud OSS). The notification style is template‑customizable, based on the project https://github.dev/timonwong/prometheus-webhook-dingtalk.
Template
The default template resides at
template/default.tmpland can be customized as needed.
<code>{{ 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 }}`

{{- end }}
**标签:**
{{ range .Labels.SortedPairs }}{{ if and (ne (.Name) "severity") (ne (.Name) "summary") }}> - {{ .Name }}: {{ .Value | markdown | html }}
{{ end }}{{ end }}
{{ end }}{{ end }}
{{ 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 }}</code>Deployment
The default configuration file (
/etc/promoter/config.yaml) looks like:
<code>debug: true
http_port: 8080
timeout: 5s
prometheus_url: <prometheus_url> # Prometheus address
metric_resolution: 100
s3:
access_key: <ak>
secret_key: <sk>
endpoint: oss-cn-beijing.aliyuncs.com
region: cn-beijing
bucket: <bucket>
dingtalk:
url: https://oapi.dingtalk.com/robot/send?access_token=<token>
secret: <SEC> # secret for signature
</code>You can run the Docker image
cnych/promoter:v0.1.1or deploy with the Kubernetes manifest
deploy/kubernetes/promoter.yaml. After starting, configure AlertManager to point to the Promoter webhook URL.
<code>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
</code>Core Principle
Promoter is written in Go. The webhook itself is simple; the key part is rendering the monitoring chart by querying Prometheus via its API.
<code>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
}
</code>The retrieved metrics are plotted using the
gonum.org/v1/plotpackage.
<code>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)
}
// Font setup, axis configuration, color palette, drawing lines, polygon overlay, canvas creation, drawing latest value …
return c, nil
}
</code>For more implementation details, see the project repository at https://github.com/cnych/promoter.
Ops Development Stories
Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.