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.
This commit is contained in:
1 parent
f9c6c1d826
commit
2d73661dd1
127 files changed
+8117
-2876
No files matched your search
@@ -0,0 +1,146 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user