Files
LokiStack/backend/services/notifier.go
T
logan 2d73661dd1 feat: complete Loki query, insights, settings and deployment workflows
Add the Loki query UI, access policies, ingress insights, alert and registry settings, and their backend APIs. Include the related Kubernetes deployment and collector updates required by the integrated application.

Validated with go test ./..., npm run build, and npm run lint.
2026-10-25 21:28:32 +08:00

147 lines
4.1 KiB
Go

package services
// 飞书/Lark 群机器人通知 — 卡片消息(参照 opsx-sec notifier 与运维告警脚本的卡片结构),
// webhook 存 SystemSetting(AES 加密),对外只回 hasWebhook。
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
)
const (
notifySettingKey = "notify_settings"
notifyHTTPTimeout = 10 * time.Second
)
// NotifyConfig 通知配置(webhook/签名 加密存储)
type NotifyConfig struct {
Enabled bool `json:"enabled"`
Webhook string `json:"webhook"` // ENC: 密文存储
SigningSecret string `json:"signingSecret"` // 飞书签名校验(可选)
}
// NotifyPublicConfig 对外输出形态
type NotifyPublicConfig struct {
Enabled bool `json:"enabled"`
HasWebhook bool `json:"hasWebhook"`
HasSecret bool `json:"hasSecret"`
}
func GetNotifyConfig() *NotifyConfig {
db := repository.GetDB()
db.RLock()
var raw string
for i := range db.Settings {
if db.Settings[i].Key == notifySettingKey {
raw = db.Settings[i].Value
break
}
}
db.RUnlock()
cfg := &NotifyConfig{}
if raw != "" {
_ = json.Unmarshal([]byte(raw), cfg)
}
if c, err := DecryptCred(cfg.Webhook); err == nil {
cfg.Webhook = c
}
if c, err := DecryptCred(cfg.SigningSecret); err == nil {
cfg.SigningSecret = c
}
return cfg
}
func SaveNotifyConfig(enabled bool, webhook, secret string) error {
// 留空 = 保留旧值
old := GetNotifyConfig()
if webhook == "" {
webhook = old.Webhook
} else if enc, err := crypto.EncryptField(webhook); err == nil {
webhook = enc
}
if secret == "" {
secret = old.SigningSecret
} else if enc, err := crypto.EncryptField(secret); err == nil {
secret = enc
}
data, _ := json.Marshal(&NotifyConfig{Enabled: enabled, Webhook: webhook, SigningSecret: secret})
db := repository.GetDB()
newID := db.NextID("settings")
db.Lock()
defer db.Unlock()
for i := range db.Settings {
if db.Settings[i].Key == notifySettingKey {
db.Settings[i].Value = string(data)
db.RequestSave()
return nil
}
}
db.Settings = append(db.Settings, models.SystemSetting{ID: newID, Key: notifySettingKey, Value: string(data)})
db.RequestSave()
return nil
}
// larkSign 飞书签名:timestamp + "\n" + secret 的 HMAC-SHA256,base64
func larkSign(secret string, ts int64) string {
stringToSign := fmt.Sprintf("%d\n%s", ts, secret)
h := hmac.New(sha256.New, []byte(stringToSign))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
// SendLarkCard 发送飞书卡片;title 带颜色(red/orange),lines 为 markdown 内容行
func SendLarkCard(title, color string, lines []string) error {
cfg := GetNotifyConfig()
if !cfg.Enabled || cfg.Webhook == "" {
return fmt.Errorf("通知未启用或未配置 webhook")
}
content := strings.Join(lines, "\n")
payload := map[string]interface{}{
"msg_type": "interactive",
"card": map[string]interface{}{
"config": map[string]interface{}{"wide_screen_mode": true},
"header": map[string]interface{}{
"title": map[string]string{"tag": "plain_text", "content": title},
"template": color,
},
"elements": []interface{}{
map[string]interface{}{"tag": "div", "text": map[string]string{"tag": "lark_md", "content": content}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "opsx-loki 日志告警"}}},
},
},
}
if cfg.SigningSecret != "" {
ts := time.Now().Unix()
payload["timestamp"] = fmt.Sprintf("%d", ts)
payload["sign"] = larkSign(cfg.SigningSecret, ts)
}
body, _ := json.Marshal(payload)
resp, err := (&http.Client{Timeout: notifyHTTPTimeout}).Post(cfg.Webhook, "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("飞书返回 HTTP %d", resp.StatusCode)
}
var out struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
_ = json.NewDecoder(resp.Body).Decode(&out)
if out.Code != 0 {
return fmt.Errorf("飞书拒绝: %s", out.Msg)
}
return nil
}