一、代码
#初始化目录
cd ~
mkdir feishu-forwarder
cd feishu-forwarder
go mod init feishu-forwarder
#Dockerfile
FROM golang:1.24.2-alpine AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/feishu-forwarder .
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/feishu-forwarder /feishu-forwarder
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/feishu-forwarder"]
#main.go
package main
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"log"
"math/rand"
"net/http"
"os"
"sort"
"strings"
"sync"
"time"
)
type Alert struct {
Status string `json:"status"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
GeneratorURL string `json:"generatorURL"`
Fingerprint string `json:"fingerprint"` // Alertmanager webhook 通常会带;没有也没关系
}
type AMPayload struct {
Status string `json:"status"`
Receiver string `json:"receiver"`
ExternalURL string `json:"externalURL"`
GroupKey string `json:"groupKey"`
CommonLabels map[string]string `json:"commonLabels"`
Alerts []Alert `json:"alerts"`
}
// webhook body:text 用 content;card 用 card
type FeishuBody struct {
Timestamp string `json:"timestamp,omitempty"`
Sign string `json:"sign,omitempty"`
MsgType string `json:"msg_type"`
Content map[string]interface{} `json:"content,omitempty"`
Card map[string]interface{} `json:"card,omitempty"`
}
type Target struct {
Webhook string
Secret string
Name string // 用于日志
}
type Config struct {
// 路由:按 severity 选择目标群
WebhooksCritical string
WebhooksWarning string
WebhooksDefault string
SecretCritical string
SecretWarning string
SecretDefault string
// 消息类型:card / text
MsgType string
// 去重窗口
DedupTTL time.Duration
// 重试
RetryMax int
RetryBase time.Duration
// 限流(全局)
RateQPS float64
RateBurst int
// HTTP
SendTimeout time.Duration
}
func loadConfig() Config {
cfg := Config{
WebhooksCritical: strings.TrimSpace(os.Getenv("FEISHU_WEBHOOKS_CRITICAL")),
WebhooksWarning: strings.TrimSpace(os.Getenv("FEISHU_WEBHOOKS_WARNING")),
WebhooksDefault: strings.TrimSpace(os.Getenv("FEISHU_WEBHOOKS_DEFAULT")),
SecretCritical: strings.TrimSpace(os.Getenv("FEISHU_SECRET_CRITICAL")),
SecretWarning: strings.TrimSpace(os.Getenv("FEISHU_SECRET_WARNING")),
SecretDefault: strings.TrimSpace(os.Getenv("FEISHU_SECRET_DEFAULT")),
MsgType: strings.ToLower(strings.TrimSpace(os.Getenv("FEISHU_MSG_TYPE"))),
DedupTTL: mustParseDuration(getEnvDefault("DEDUP_TTL", "10m")),
RetryMax: mustParseInt(getEnvDefault("RETRY_MAX", "3")),
RetryBase: mustParseDuration(getEnvDefault("RETRY_BASE", "300ms")),
RateQPS: mustParseFloat(getEnvDefault("RATE_QPS", "2")), // 每秒2条
RateBurst: mustParseInt(getEnvDefault("RATE_BURST", "5")), // 突发5条
SendTimeout: mustParseDuration(getEnvDefault("SEND_TIMEOUT", "6s")),
}
if cfg.MsgType != "text" && cfg.MsgType != "card" {
cfg.MsgType = "card"
}
// 兼容:如果你只设置了旧的 FEISHU_WEBHOOKS/FEISHU_SECRET
if cfg.WebhooksDefault == "" {
cfg.WebhooksDefault = strings.TrimSpace(os.Getenv("FEISHU_WEBHOOKS"))
}
if cfg.SecretDefault == "" {
cfg.SecretDefault = strings.TrimSpace(os.Getenv("FEISHU_SECRET"))
}
return cfg
}
/* ============ 去重(内存 TTL) ============ */
type Deduper struct {
mu sync.Mutex
ttl time.Duration
data map[string]time.Time
}
func NewDeduper(ttl time.Duration) *Deduper {
d := &Deduper{
ttl: ttl,
data: make(map[string]time.Time),
}
go d.gcLoop()
return d
}
func (d *Deduper) Allow(key string) bool {
if d.ttl <= 0 {
return true
}
now := time.Now()
d.mu.Lock()
defer d.mu.Unlock()
if t, ok := d.data[key]; ok {
if now.Sub(t) < d.ttl {
return false
}
}
d.data[key] = now
return true
}
func (d *Deduper) gcLoop() {
t := time.NewTicker(1 * time.Minute)
defer t.Stop()
for range t.C {
now := time.Now()
d.mu.Lock()
for k, v := range d.data {
if now.Sub(v) > d.ttl*2 {
delete(d.data, k)
}
}
d.mu.Unlock()
}
}
/* ============ 简单全局限流(token bucket) ============ */
type RateLimiter struct {
ch chan struct{}
stop chan struct{}
}
func NewRateLimiter(qps float64, burst int) *RateLimiter {
if qps <= 0 {
return nil
}
if burst < 1 {
burst = 1
}
rl := &RateLimiter{
ch: make(chan struct{}, burst),
stop: make(chan struct{}),
}
// 初始塞满 burst
for i := 0; i < burst; i++ {
rl.ch <- struct{}{}
}
interval := time.Duration(float64(time.Second) / qps)
if interval < 10*time.Millisecond {
interval = 10 * time.Millisecond
}
go func() {
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-t.C:
select {
case rl.ch <- struct{}{}:
default:
}
case <-rl.stop:
return
}
}
}()
return rl
}
func (rl *RateLimiter) Acquire(ctx context.Context) error {
if rl == nil {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-rl.ch:
return nil
}
}
/* ============ 飞书签名 ============ */
func genFeishuSign(secret, timestamp string) string {
stringToSign := timestamp + "\n" + secret
mac := hmac.New(sha256.New, []byte(stringToSign))
sum := mac.Sum(nil)
return base64.StdEncoding.EncodeToString(sum)
}
/* ============ severity 路由 ============ */
func getSeverity(p AMPayload) string {
s := strings.ToLower(strings.TrimSpace(p.CommonLabels["severity"]))
if s == "" && len(p.Alerts) > 0 {
s = strings.ToLower(strings.TrimSpace(p.Alerts[0].Labels["severity"]))
}
switch s {
case "critical", "fatal", "sev0", "sev1":
return "critical"
case "warning", "warn", "sev2":
return "warning"
default:
return "default"
}
}
func splitWebhooks(s string) []string {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
parts := strings.Split(s, ",")
var out []string
for _, x := range parts {
x = strings.TrimSpace(x)
if x != "" {
out = append(out, x)
}
}
return out
}
func selectTargets(cfg Config, sev string) []Target {
var whs []string
var secret string
var name string
switch sev {
case "critical":
whs = splitWebhooks(cfg.WebhooksCritical)
secret = cfg.SecretCritical
name = "critical"
case "warning":
whs = splitWebhooks(cfg.WebhooksWarning)
secret = cfg.SecretWarning
name = "warning"
default:
whs = splitWebhooks(cfg.WebhooksDefault)
secret = cfg.SecretDefault
name = "default"
}
// fallback:如果 critical/warning 没配,则退回 default
if len(whs) == 0 {
whs = splitWebhooks(cfg.WebhooksDefault)
secret = cfg.SecretDefault
name = "default(fallback)"
}
var out []Target
for _, w := range whs {
out = append(out, Target{Webhook: w, Secret: secret, Name: name})
}
return out
}
/* ============ 去重 key ============ */
func alertKey(a Alert) string {
if a.Fingerprint != "" {
return a.Fingerprint
}
// 没 fingerprint 就用 labels 自己算一个稳定 hash
keys := make([]string, 0, len(a.Labels))
for k := range a.Labels {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
b.WriteString(k)
b.WriteString("=")
b.WriteString(a.Labels[k])
b.WriteString(";")
}
sum := sha256.Sum256([]byte(b.String()))
return hex.EncodeToString(sum[:])
}
/* ============ 消息构建 ============ */
func formatText(p AMPayload, alerts []Alert) string {
var b strings.Builder
b.WriteString("【Alertmanager】" + strings.ToUpper(p.Status) + "\n")
if v := p.CommonLabels["alertname"]; v != "" {
b.WriteString("alertname: " + v + "\n")
}
if v := p.CommonLabels["severity"]; v != "" {
b.WriteString("severity: " + v + "\n")
}
b.WriteString("alerts: ")
b.WriteString(intToString(len(alerts)))
b.WriteString("\n\n")
for i, a := range alerts {
b.WriteString("#")
b.WriteString(intToString(i + 1))
b.WriteString(" ")
if an := a.Labels["alertname"]; an != "" {
b.WriteString(an)
}
if inst := a.Labels["instance"]; inst != "" {
b.WriteString(" @ " + inst)
}
b.WriteString("\n")
if s := a.Annotations["summary"]; s != "" {
b.WriteString("summary: " + s + "\n")
}
if d := a.Annotations["description"]; d != "" {
b.WriteString("desc: " + d + "\n")
}
if a.GeneratorURL != "" {
b.WriteString("url: " + a.GeneratorURL + "\n")
}
b.WriteString("\n")
}
return b.String()
}
func buildCard(p AMPayload, sev string, alerts []Alert) map[string]interface{} {
alertname := p.CommonLabels["alertname"]
if alertname == "" && len(alerts) > 0 {
alertname = alerts[0].Labels["alertname"]
}
// 颜色:FIRING + critical 用 red;warning 用 orange;resolved 用 green
template := "blue"
if strings.ToLower(p.Status) == "firing" {
if sev == "critical" {
template = "red"
} else if sev == "warning" {
template = "orange"
} else {
template = "blue"
}
} else {
template = "green"
}
title := "[" + strings.ToUpper(p.Status) + "][" + sev + "] " + alertname + " (" + intToString(len(alerts)) + ")"
// 内容用 markdown,最多展示前 5 条(避免卡片过长)
maxShow := 5
show := alerts
more := 0
if len(alerts) > maxShow {
show = alerts[:maxShow]
more = len(alerts) - maxShow
}
var md strings.Builder
md.WriteString("**Receiver:** " + p.Receiver + "\n")
if p.ExternalURL != "" {
md.WriteString("**Alertmanager:** " + p.ExternalURL + "\n")
}
md.WriteString("\n")
for i, a := range show {
an := a.Labels["alertname"]
inst := a.Labels["instance"]
md.WriteString("**#" + intToString(i+1) + "** " + an)
if inst != "" {
md.WriteString(" @ `" + inst + "`")
}
md.WriteString("\n")
if s := a.Annotations["summary"]; s != "" {
md.WriteString("- **summary:** " + s + "\n")
}
if d := a.Annotations["description"]; d != "" {
md.WriteString("- **desc:** " + d + "\n")
}
if a.StartsAt != "" {
md.WriteString("- **startsAt:** " + a.StartsAt + "\n")
}
if a.GeneratorURL != "" {
md.WriteString("- **url:** " + a.GeneratorURL + "\n")
}
md.WriteString("\n")
}
if more > 0 {
md.WriteString("…还有 **" + intToString(more) + "** 条未展开\n")
}
// 如果有 generatorURL,就加一个按钮(取第一条有 url 的)
btnURL := ""
for _, a := range alerts {
if a.GeneratorURL != "" {
btnURL = a.GeneratorURL
break
}
}
elements := []interface{}{
map[string]interface{}{"tag": "markdown", "content": md.String()},
}
if btnURL != "" {
elements = append(elements,
map[string]interface{}{"tag": "hr"},
map[string]interface{}{
"tag": "action",
"actions": []interface{}{
map[string]interface{}{
"tag": "button",
"text": map[string]interface{}{"tag": "plain_text", "content": "打开规则/图表"},
"type": "primary",
"url": btnURL,
},
},
},
)
}
card := map[string]interface{}{
"config": map[string]interface{}{
"wide_screen_mode": true,
},
"header": map[string]interface{}{
"title": map[string]interface{}{"tag": "plain_text", "content": title},
"template": template,
},
"elements": elements,
}
return card
}
/* ============ 发送:重试 + 限流 ============ */
type httpError struct {
code int
body string
}
func (e *httpError) Error() string {
if e.body != "" {
return "http status " + intToString(e.code) + " body=" + e.body
}
return "http status " + intToString(e.code)
}
func isRetryableStatus(code int) bool {
return code == 429 || code >= 500
}
func doPost(ctx context.Context, client *http.Client, url string, payload []byte) error {
req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 == 2 {
return nil
}
// 读一点返回体,方便排查(最多 2KB)
b, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return &httpError{code: resp.StatusCode, body: strings.TrimSpace(string(b))}
}
func sendWithRetry(ctx context.Context, rl *RateLimiter, cfg Config, t Target, body FeishuBody) error {
client := &http.Client{Timeout: cfg.SendTimeout}
payload, _ := json.Marshal(body)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
var lastErr error
for i := 0; i < cfg.RetryMax; i++ {
// 限流:每次尝试都要拿 token(避免重试时把飞书打爆)
if err := rl.Acquire(ctx); err != nil {
return err
}
err := doPost(ctx, client, t.Webhook, payload)
if err == nil {
return nil
}
lastErr = err
// 只有可重试错误才重试
retry := false
if he, ok := err.(*httpError); ok {
retry = isRetryableStatus(he.code)
} else {
retry = true // 网络错误等
}
if !retry || i == cfg.RetryMax-1 {
break
}
// 退避:base * 2^i + jitter(0~100ms)
sleep := cfg.RetryBase * time.Duration(1<<i)
sleep += time.Duration(r.Intn(100)) * time.Millisecond
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(sleep):
}
}
return lastErr
}
func buildFeishuBody(cfg Config, sev string, p AMPayload, alerts []Alert, webhookSecret string) FeishuBody {
ts := ""
sign := ""
if webhookSecret != "" {
ts = int64ToString(time.Now().Unix())
sign = genFeishuSign(webhookSecret, ts)
}
if cfg.MsgType == "text" {
return FeishuBody{
Timestamp: ts,
Sign: sign,
MsgType: "text",
Content: map[string]interface{}{
"text": formatText(p, alerts),
},
}
}
// card
return FeishuBody{
Timestamp: ts,
Sign: sign,
MsgType: "interactive",
Card: buildCard(p, sev, alerts),
}
}
/* ============ helpers ============ */
func getEnvDefault(k, def string) string {
v := strings.TrimSpace(os.Getenv(k))
if v == "" {
return def
}
return v
}
func mustParseDuration(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
return 0
}
return d
}
func mustParseInt(s string) int {
n := 0
for _, ch := range s {
if ch >= '0' && ch <= '9' {
n = n*10 + int(ch-'0')
}
}
if n == 0 {
return 0
}
return n
}
func mustParseFloat(s string) float64 {
// 简易 parse:支持 "2" "2.5"
var n, frac, div float64
div = 1
seenDot := false
for _, ch := range s {
if ch == '.' {
seenDot = true
continue
}
if ch < '0' || ch > '9' {
continue
}
if !seenDot {
n = n*10 + float64(ch-'0')
} else {
frac = frac*10 + float64(ch-'0')
div *= 10
}
}
return n + frac/div
}
func intToString(x int) string { return int64ToString(int64(x)) }
func int64ToString(x int64) string {
if x == 0 {
return "0"
}
neg := x < 0
if neg {
x = -x
}
var buf [32]byte
i := len(buf)
for x > 0 {
i--
buf[i] = byte('0' + x%10)
x /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}
/* ============ main ============ */
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
cfg := loadConfig()
deduper := NewDeduper(cfg.DedupTTL)
rl := NewRateLimiter(cfg.RateQPS, cfg.RateBurst)
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte("ok"))
})
http.HandleFunc("/alertmanager", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(405)
return
}
var p AMPayload
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
log.Printf("decode failed: remote=%s err=%v", r.RemoteAddr, err)
w.WriteHeader(400)
w.Write([]byte(err.Error()))
return
}
sev := getSeverity(p)
log.Printf("recv webhook: remote=%s status=%s receiver=%s alerts=%d severity=%s alertname=%s",
r.RemoteAddr, p.Status, p.Receiver, len(p.Alerts), sev, p.CommonLabels["alertname"])
// 去重(只对 firing 做,resolved 不去重,避免“恢复消息被吞”)
alertsToSend := make([]Alert, 0, len(p.Alerts))
suppressed := 0
for _, a := range p.Alerts {
if strings.ToLower(p.Status) == "firing" && cfg.DedupTTL > 0 {
key := "firing:" + alertKey(a)
if !deduper.Allow(key) {
suppressed++
continue
}
}
alertsToSend = append(alertsToSend, a)
}
if len(alertsToSend) == 0 {
log.Printf("dedup suppressed all alerts: suppressed=%d total=%d", suppressed, len(p.Alerts))
w.WriteHeader(200)
w.Write([]byte("ok"))
return
}
targets := selectTargets(cfg, sev)
if len(targets) == 0 {
log.Printf("no targets configured for severity=%s", sev)
w.WriteHeader(500)
w.Write([]byte("no targets configured"))
return
}
okCount := 0
failCount := 0
// 给本次请求一个总体 deadline(避免 handler 卡太久)
ctx, cancel := context.WithTimeout(r.Context(), cfg.SendTimeout*time.Duration(cfg.RetryMax+1))
defer cancel()
for _, t := range targets {
body := buildFeishuBody(cfg, sev, p, alertsToSend, t.Secret)
err := sendWithRetry(ctx, rl, cfg, t, body)
if err != nil {
log.Printf("send failed: group=%s target=%s err=%v", t.Name, t.Webhook, err)
failCount++
continue
}
log.Printf("send ok: group=%s target=%s", t.Name, t.Webhook)
okCount++
}
log.Printf("send summary: ok=%d fail=%d targets=%d suppressed=%d severity=%s",
okCount, failCount, len(targets), suppressed, sev)
if okCount == 0 {
w.WriteHeader(502)
w.Write([]byte("failed"))
return
}
w.WriteHeader(200)
w.Write([]byte("ok"))
})
addr := ":8080"
log.Printf("listening on %s msgType=%s dedup=%s retryMax=%d rateQps=%.2f burst=%d",
addr, cfg.MsgType, cfg.DedupTTL, cfg.RetryMax, cfg.RateQPS, cfg.RateBurst)
log.Fatal(http.ListenAndServe(addr, nil))
}
二、构建部署
docker build -t harbor.axzys.cn/monitoring/feishu-forwarder:0.1.0 .
docker push harbor.axzys.cn/monitoring/feishu-forwarder:0.1.0 .cat feishu.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: feishu-forwarder
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels: { app: feishu-forwarder }
template:
metadata:
labels: { app: feishu-forwarder }
spec:
containers:
- name: app
image: harbor.axzys.cn/monitoring/feishu-forwarder:0.1.2
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: FEISHU_WEBHOOKS_CRITICAL
valueFrom: { secretKeyRef: { name: feishu-forwarder-secret, key: FEISHU_WEBHOOKS_CRITICAL } }
- name: FEISHU_WEBHOOKS_WARNING
valueFrom: { secretKeyRef: { name: feishu-forwarder-secret, key: FEISHU_WEBHOOKS_WARNING } }
- name: FEISHU_WEBHOOKS_DEFAULT
valueFrom: { secretKeyRef: { name: feishu-forwarder-secret, key: FEISHU_WEBHOOKS_DEFAULT } }
- name: FEISHU_SECRET_CRITICAL
valueFrom: { secretKeyRef: { name: feishu-forwarder-secret, key: FEISHU_SECRET_CRITICAL } }
- name: FEISHU_SECRET_WARNING
valueFrom: { secretKeyRef: { name: feishu-forwarder-secret, key: FEISHU_SECRET_WARNING } }
- name: FEISHU_SECRET_DEFAULT
valueFrom: { secretKeyRef: { name: feishu-forwarder-secret, key: FEISHU_SECRET_DEFAULT } }
- name: FEISHU_MSG_TYPE
value: "card" # card / text
- name: DEDUP_TTL
value: "10m"
- name: RETRY_MAX
value: "3"
- name: RETRY_BASE
value: "300ms"
- name: RATE_QPS
value: "2"
- name: RATE_BURST
value: "5"
- name: SEND_TIMEOUT
value: "6s"
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 2
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: feishu-forwarder
namespace: monitoring
spec:
selector: { app: feishu-forwarder }
ports:
- name: http
port: 8080
targetPort: 8080
type: ClusterIP
cat feishu-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: feishu-forwarder-secret
namespace: monitoring
type: Opaque
stringData:
FEISHU_WEBHOOKS_CRITICAL: "https://open.feishu.cn/open-apis/bot/v2/hook/飞书token"
FEISHU_WEBHOOKS_WARNING: "https://open.feishu.cn/open-apis/bot/v2/hook/飞书token"
FEISHU_WEBHOOKS_DEFAULT: "https://open.feishu.cn/open-apis/bot/v2/hook/xxxx"
# 如果你开了“签名校验”,不同群机器人 secret 往往不同
FEISHU_SECRET_CRITICAL: "SEC_xxx"
FEISHU_SECRET_WARNING: "SEC_yyy"
FEISHU_SECRET_DEFAULT: "SEC_zzz"
cat alertmanagerConfig.yaml
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: feishu-forwarder
namespace: monitoring
labels:
alertmanagerConfig: main # 这行是否需要,取决于你的 Alertmanager 选择器
spec:
route:
receiver: feishu-forwarder
groupWait: 30s
groupInterval: 5m
repeatInterval: 30m
receivers:
- name: feishu-forwarder
webhookConfigs:
- url: http://feishu-forwarder.monitoring.svc:8080/alertmanager
sendResolved: true
root@k8s-01:/woke/prometheus/feishu# kubectl get pod -n monitoring
NAME READY STATUS RESTARTS AGE
alertmanager-main-0 2/2 Running 0 7d2h
alertmanager-main-1 2/2 Running 0 7d2h
alertmanager-main-2 2/2 Running 0 7d2h
blackbox-exporter-7fcbd888d-zv6z6 3/3 Running 0 15d
feishu-forwarder-8559bf6b68-njzvq 1/1 Running 0 68m
grafana-7ff454c477-l9x2k 1/1 Running 0 15d
kube-state-metrics-78f95f79bb-wpcln 3/3 Running 0 15d
node-exporter-2vq26 2/2 Running 2 (35d ago) 39d
node-exporter-622pm 2/2 Running 24 (35d ago) 39d
node-exporter-rl67z 2/2 Running 22 (35d ago) 39d
prometheus-adapter-585d9c5dd5-bfsxw 1/1 Running 0 8d
prometheus-adapter-585d9c5dd5-pcrnd 1/1 Running 0 8d
prometheus-k8s-0 2/2 Running 0 7d2h
prometheus-k8s-1 2/2 Running 0 7d2h
prometheus-operator-78967669c9-5pk25 2/2 Running 0 7d2h
三、调试 

kubectl -n monitoring port-forward svc/alertmanager-main 19093:9093
kubectl -n monitoring logs -f deploy/feishu-forwarder
curl -i -X POST http://127.0.0.1:19093/api/v2/alerts \
-H 'Content-Type: application/json' \
-d '[{
"labels":{"alertname":"axingWarning","severity":"warning","instance":"demo:9100","namespace":"monitoring"},
"annotations":{"summary":"injected warning","description":"from alertmanager api"},
"startsAt":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"
}]'
操作过程中出现了 prometheus 已经告警 并且已经到Alertmanager了 但是 feishu-forwarder 没有收到 解决方法 是加上 不然他只会通知monitoring这个名称空间的告警信息
matchers:
- name: prometheus
value: monitoring/k8s
还有就是InfoInhibitor 一直刷屏通知告警 有两种解决方法 一、就是改上面的feishu-secret把FEISHU_WEBHOOKS_DEFAULT注释掉 还有一种就是下面这种只通知warning/critical
root@k8s-01:/woke/prometheus/feishu# cat alertmanagerConfig.yaml
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
name: feishu-forwarder
namespace: monitoring
labels:
alertmanagerConfig: main
spec:
route:
receiver: feishu-forwarder
groupWait: 30s
groupInterval: 5m
repeatInterval: 30m
matchers:
- name: prometheus
value: monitoring/k8s
matchType: "="
# 建议:只推 warning/critical,InfoInhibitor(severity=none) 就不会再刷飞书
- name: severity
value: warning|critical
matchType: "=~"
receivers:
- name: feishu-forwarder
webhookConfigs:
- url: http://feishu-forwarder.monitoring.svc:8080/alertmanager
sendResolved: true
评论 (0)