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.

{{ 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 }}

Deployment

The default configuration file ( /etc/promoter/config.yaml) looks like:

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

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.

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

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.

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
}

The retrieved metrics are plotted using the gonum.org/v1/plot package.

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
}

For more implementation details, see the project repository at https://github.com/cnych/promoter.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.