Operations 10 min read

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.

Ops Development Stories
Ops Development Stories
Ops Development Stories
Render Real‑Time Alert Charts in DingTalk with Promoter – A Go Solution

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.tmpl

and 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 }}`
![📈]({{ .Url }})
{{- 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.1

or 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/plot

package.

<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.

monitoringGoPrometheusAlertmanagerWebhookDingTalkChart Rendering
Ops Development Stories
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.