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.
147 lines
4.1 KiB
Go
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
|
|
}
|