Compare commits

...

31 Commits

Author SHA1 Message Date
ning
b057940134 add stats 2024-08-01 15:18:49 +08:00
ning
4500c4aba8 refactor callback 2024-08-01 15:12:03 +08:00
710leo
726e994d58 merge main 2024-07-30 15:58:31 +08:00
710leo
71c4c24f00 code refactor 2024-07-30 15:57:15 +08:00
710leo
d14a834149 webhook send batch 2024-07-30 15:47:25 +08:00
Yening Qin
5b2513b7a1 feat: support lark and larkcard notify channel (#2061)
* feat: support lark notify channel (#2056)

Co-authored-by: flashbo <36443248+lwb0214@users.noreply.github.com>
Co-authored-by: wenbo <1027758873@qq.com>
2024-07-27 21:21:43 +08:00
ning
7cec16eaf0 update center router init 2024-07-26 14:59:40 +08:00
ning
5f895552a9 done 2024-07-26 12:54:28 +08:00
ning
1e3f62b92f refactor 2024-07-25 17:46:19 +08:00
ning
17dbb3ec77 code refactor 2024-07-25 12:06:10 +08:00
ning
00822c8404 refactor: add ibex enable check 2024-07-25 11:39:43 +08:00
ning
55de30d6c7 refactor: update mute rule api 2024-07-24 11:37:38 +08:00
Yening Qin
8b7dbed27e refactor: modify heartbeat api (#2051) 2024-07-24 11:23:56 +08:00
Dan218
71b8fa27d0 feat: Provide optional style for buildTargetWhere (#2038) 2024-07-24 11:12:17 +08:00
ning
31174d719e refactor: event relabel 2024-07-22 11:45:17 +08:00
ning
5b5bb22ffd fix: event relable process tagsmap 2024-07-22 10:46:29 +08:00
ning
e98fe9ea2e refactor: HandleTSFunc 2024-07-21 15:28:06 +08:00
ning
32e9ded393 refactor: server-clusters api perm 2024-07-21 11:04:35 +08:00
ning
8293ca20be refactor: assets file support md 2024-07-18 15:07:47 +08:00
Yening Qin
6c4ddfc349 refactor: update languageDetector (#2043) 2024-07-18 14:13:48 +08:00
ning
cd0c478515 refactor: event relabel add default value 2024-07-17 22:48:50 +08:00
Yening Qin
2cd25ac0e5 fix: optimize event recovery inhibit (#2042) 2024-07-17 22:30:31 +08:00
ning
bb99ba3d1c update sql 2024-07-17 11:57:20 +08:00
Yening Qin
64405dca5d feat: alert event support relabel (#2041) 2024-07-17 10:30:29 +08:00
ulricqin
69ea9ca8f8 Update README.md 2024-07-17 09:39:00 +08:00
ulricqin
41d0f2fcda Update README.md 2024-07-17 09:36:30 +08:00
710leo
93df1c0fbc docs: add perm point 2024-07-16 23:44:30 +08:00
flashbo
86e952788d refactor: targets get api support backend sorting (#2034)
Co-authored-by: wenbo <bupt.lwb@gmail.com>
2024-07-16 23:38:04 +08:00
ning
e890f2616f refactor: change webhook sleep time 2024-07-13 14:38:32 +08:00
yanli
6c2ee584e5 refactor: MetricDesc defaults to Chinese (#2032) 2024-07-12 21:50:51 +08:00
Dan218
5f07fc3010 Feat: Add skip Verify Insecure ssl/tls in sendWebhook (#2030) 2024-07-12 10:38:33 +08:00
40 changed files with 1471 additions and 222 deletions

View File

@@ -78,22 +78,12 @@
![边缘部署模式](https://download.flashcat.cloud/ulric/20240222102119.png)
## 近期计划
- [ ] 仪表盘:支持内嵌 Grafana
- [ ] 告警规则:通知时支持配置过滤标签,避免告警事件中一堆不重要的标签
- [x] 告警规则:支持配置恢复时的 Promql告警恢复通知也可以带上恢复时的值了
- [ ] 机器管理自定义标签拆分管理agent 自动上报的标签和用户在页面自定义的标签分开管理,对于 agent 自动上报的标签,以 agent 为准,直接覆盖服务端 DB 中的数据
- [ ] 机器管理:机器支持角色字段,即无头标签,用于描述混部场景
- [ ] 机器管理:把业务组的 busigroup 标签迁移到机器的属性里,让机器支持挂到多个业务组
- [ ] 告警规则:增加 Host Metrics 类别,支持按照业务组、角色、标签等筛选机器,规则 promql 支持变量,支持在机器颗粒度配置变量值
- [ ] 告警通知:重构整个通知逻辑,引入事件处理的 pipeline支持对告警事件做自定义处理和灵活分派
## 交流渠道
- 报告Bug优先推荐提交[夜莺GitHub Issue](https://github.com/ccfos/nightingale/issues/new?assignees=&labels=kind%2Fbug&projects=&template=bug_report.yml)
- 推荐完整浏览[夜莺文档站点](https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v7/introduction/),了解更多信息
- 推荐搜索关注夜莺公众号,第一时间获取社区动态:`夜莺监控Nightingale`
- 日常答疑、技术分享、用户之间的交流,统一使用知识星球,大伙可以免费加入交流,[入口在这里](https://download.flashcat.cloud/ulric/20240319095409.png)
- 日常问题交流推荐加入[知识星球](https://download.flashcat.cloud/ulric/20240319095409.png),也可以加我微信 `picobyte`,备注:`夜莺加群-<公司>-<姓名>` 拉入微信群,不过研发人员主要是关注 github issue 和星球,微信群关注较少
## 广受关注
[![Stargazers over time](https://api.star-history.com/svg?repos=ccfos/nightingale&type=Date)](https://star-history.com/#ccfos/nightingale&Date)

View File

@@ -32,6 +32,7 @@ type Alerting struct {
Timeout int64
TemplatesDir string
NotifyConcurrency int
WebhookBatchSend bool
}
type CallPlugin struct {

View File

@@ -100,6 +100,8 @@ func (e *Dispatch) relaodTpls() error {
models.Mm: sender.NewSender(models.Mm, tmpTpls),
models.Telegram: sender.NewSender(models.Telegram, tmpTpls),
models.FeishuCard: sender.NewSender(models.FeishuCard, tmpTpls),
models.Lark: sender.NewSender(models.Lark, tmpTpls),
models.LarkCard: sender.NewSender(models.LarkCard, tmpTpls),
}
// domain -> Callback()
@@ -110,7 +112,9 @@ func (e *Dispatch) relaodTpls() error {
models.TelegramDomain: sender.NewCallBacker(models.TelegramDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
models.FeishuCardDomain: sender.NewCallBacker(models.FeishuCardDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
models.IbexDomain: sender.NewCallBacker(models.IbexDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
models.LarkDomain: sender.NewCallBacker(models.LarkDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
models.DefaultDomain: sender.NewCallBacker(models.DefaultDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
models.LarkCardDomain: sender.NewCallBacker(models.LarkCardDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
}
e.RwLock.RLock()
@@ -261,7 +265,11 @@ func (e *Dispatch) Send(rule *models.AlertRule, event *models.AlertCurEvent, not
e.SendCallbacks(rule, notifyTarget, event)
// handle global webhooks
sender.SendWebhooks(notifyTarget.ToWebhookList(), event, e.Astats)
if e.alerting.WebhookBatchSend {
sender.BatchSendWebhooks(notifyTarget.ToWebhookList(), event, e.Astats)
} else {
sender.SingleSendWebhooks(notifyTarget.ToWebhookList(), event, e.Astats)
}
// handle plugin call
go sender.MayPluginNotify(e.genNoticeBytes(event), e.notifyConfigCache.GetNotifyScript(), e.Astats)
@@ -276,7 +284,7 @@ func (e *Dispatch) SendCallbacks(rule *models.AlertRule, notifyTarget *NotifyTar
continue
}
cbCtx := sender.BuildCallBackContext(e.ctx, urlStr, rule, []*models.AlertCurEvent{event}, uids, e.userCache, e.Astats)
cbCtx := sender.BuildCallBackContext(e.ctx, urlStr, rule, []*models.AlertCurEvent{event}, uids, e.userCache, e.alerting.WebhookBatchSend, e.Astats)
if strings.HasPrefix(urlStr, "${ibex}") {
e.CallBacks[models.IbexDomain].CallBack(cbCtx)
@@ -299,6 +307,12 @@ func (e *Dispatch) SendCallbacks(rule *models.AlertRule, notifyTarget *NotifyTar
continue
}
// process lark card
if parsedURL.Host == models.LarkDomain && parsedURL.Query().Get("card") == "1" {
e.CallBacks[models.LarkCardDomain].CallBack(cbCtx)
continue
}
callBacker, ok := e.CallBacks[parsedURL.Host]
if ok {
callBacker.CallBack(cbCtx)

View File

@@ -79,6 +79,22 @@ func (s *NotifyTarget) ToCallbackList() []string {
func (s *NotifyTarget) ToWebhookList() []*models.Webhook {
webhooks := make([]*models.Webhook, 0, len(s.webhooks))
for _, wh := range s.webhooks {
if wh.Batch == 0 {
wh.Batch = 1000
}
if wh.Timeout == 0 {
wh.Timeout = 10
}
if wh.RetryCount == 0 {
wh.RetryCount = 10
}
if wh.RetryInterval == 0 {
wh.RetryInterval = 10
}
webhooks = append(webhooks, wh)
}
return webhooks

View File

@@ -143,6 +143,7 @@ func (arw *AlertRuleWorker) Eval() {
if p.Severity > point.Severity {
hash := process.Hash(cachedRule.Id, arw.processor.DatasourceId(), p)
arw.processor.DeleteProcessEvent(hash)
models.AlertCurEventDelByHash(arw.ctx, hash)
pointsMap[tagHash] = point
}

View File

@@ -18,6 +18,9 @@ import (
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/tplx"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/prometheus/prometheus/prompb"
"github.com/toolkits/pkg/logger"
"github.com/toolkits/pkg/str"
)
@@ -218,9 +221,57 @@ func (p *Processor) BuildEvent(anomalyPoint common.AnomalyPoint, from string, no
} else {
event.LastEvalTime = event.TriggerTime
}
// 生成事件之后,立马进程 relabel 处理
Relabel(p.rule, event)
return event
}
func Relabel(rule *models.AlertRule, event *models.AlertCurEvent) {
if rule == nil {
return
}
// need to keep the original label
event.OriginalTags = event.Tags
event.OriginalTagsJSON = make([]string, len(event.TagsJSON))
labels := make([]prompb.Label, len(event.TagsJSON))
for i, tag := range event.TagsJSON {
label := strings.Split(tag, "=")
if len(label) != 2 {
logger.Errorf("event%+v relabel: the label length is not 2:%v", event, label)
continue
}
event.OriginalTagsJSON[i] = tag
labels[i] = prompb.Label{Name: label[0], Value: label[1]}
}
for i := 0; i < len(rule.EventRelabelConfig); i++ {
if rule.EventRelabelConfig[i].Replacement == "" {
rule.EventRelabelConfig[i].Replacement = "$1"
}
if rule.EventRelabelConfig[i].Separator == "" {
rule.EventRelabelConfig[i].Separator = ";"
}
if rule.EventRelabelConfig[i].Regex == "" {
rule.EventRelabelConfig[i].Regex = "(.*)"
}
}
// relabel process
relabels := writer.Process(labels, rule.EventRelabelConfig...)
event.TagsJSON = make([]string, len(relabels))
event.TagsMap = make(map[string]string, len(relabels))
for i, label := range relabels {
event.TagsJSON[i] = fmt.Sprintf("%s=%s", label.Name, label.Value)
event.TagsMap[label.Name] = label.Value
}
event.Tags = strings.Join(event.TagsJSON, ",,")
}
func (p *Processor) HandleRecover(alertingKeys map[string]struct{}, now int64, inhibit bool) {
for _, hash := range p.pendings.Keys() {
if _, has := alertingKeys[hash]; has {
@@ -271,6 +322,7 @@ func (p *Processor) HandleRecoverEvent(hashArr []string, now int64, inhibit bool
// hash 对应的恢复事件的被抑制了,把之前的事件删除
p.fires.Delete(e.Hash)
p.pendings.Delete(e.Hash)
models.AlertCurEventDelByHash(p.ctx, e.Hash)
eventMap[event.Tags] = *event
}
}

View File

@@ -29,13 +29,14 @@ type (
Rule *models.AlertRule
Events []*models.AlertCurEvent
Stats *astats.Stats
BatchSend bool
}
DefaultCallBacker struct{}
)
func BuildCallBackContext(ctx *ctx.Context, callBackURL string, rule *models.AlertRule, events []*models.AlertCurEvent,
uids []int64, userCache *memsto.UserCacheType, stats *astats.Stats) CallBackContext {
uids []int64, userCache *memsto.UserCacheType, batchSend bool, stats *astats.Stats) CallBackContext {
users := userCache.GetByUserIds(uids)
newCallBackUrl, _ := events[0].ParseURL(callBackURL)
@@ -45,6 +46,7 @@ func BuildCallBackContext(ctx *ctx.Context, callBackURL string, rule *models.Ale
Rule: rule,
Events: events,
Users: users,
BatchSend: batchSend,
Stats: stats,
}
}
@@ -96,6 +98,10 @@ func NewCallBacker(
// return &MmSender{tpl: tpls[models.Mm]}
case models.TelegramDomain:
return &TelegramSender{tpl: tpls[models.Telegram]}
case models.LarkDomain:
return &LarkSender{tpl: tpls[models.Lark]}
case models.LarkCardDomain:
return &LarkCardSender{tpl: tpls[models.LarkCard]}
}
return nil
@@ -108,6 +114,21 @@ func (c *DefaultCallBacker) CallBack(ctx CallBackContext) {
event := ctx.Events[0]
if ctx.BatchSend {
webhookConf := &models.Webhook{
Type: models.RuleCallback,
Enable: true,
Url: ctx.CallBackURL,
Timeout: 5,
RetryCount: 3,
RetryInterval: 10,
Batch: 1000,
}
PushCallbackEvent(webhookConf, event, ctx.Stats)
return
}
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
resp, code, err := poster.PostJSON(ctx.CallBackURL, 5*time.Second, event, 3)
if err != nil {
@@ -136,3 +157,27 @@ type TaskCreateReply struct {
Err string `json:"err"`
Dat int64 `json:"dat"` // task.id
}
func PushCallbackEvent(webhook *models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
CallbackEventQueueLock.RLock()
queue := CallbackEventQueue[webhook.Url]
CallbackEventQueueLock.RUnlock()
if queue == nil {
queue = &WebhookQueue{
list: NewSafeListLimited(QueueMaxSize),
closeCh: make(chan struct{}),
}
CallbackEventQueueLock.Lock()
CallbackEventQueue[webhook.Url] = queue
CallbackEventQueueLock.Unlock()
StartConsumer(queue, webhook.Batch, webhook, stats)
}
succ := queue.list.PushFront(event)
if !succ {
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.list.Len(), event)
}
}

64
alert/sender/lark.go Normal file
View File

@@ -0,0 +1,64 @@
package sender
import (
"html/template"
"strings"
"github.com/ccfos/nightingale/v6/models"
)
var (
_ CallBacker = (*LarkSender)(nil)
)
type LarkSender struct {
tpl *template.Template
}
func (lk *LarkSender) CallBack(ctx CallBackContext) {
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
return
}
body := feishu{
Msgtype: "text",
Content: feishuContent{
Text: BuildTplMessage(models.Lark, lk.tpl, ctx.Events),
},
}
doSend(ctx.CallBackURL, body, models.Lark, ctx.Stats)
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
}
func (lk *LarkSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
return
}
urls := lk.extract(ctx.Users)
message := BuildTplMessage(models.Lark, lk.tpl, ctx.Events)
for _, url := range urls {
body := feishu{
Msgtype: "text",
Content: feishuContent{
Text: message,
},
}
doSend(url, body, models.Lark, ctx.Stats)
}
}
func (lk *LarkSender) extract(users []*models.User) []string {
urls := make([]string, 0, len(users))
for _, user := range users {
if token, has := user.ExtractToken(models.Lark); has {
url := token
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
url = "https://open.larksuite.com/open-apis/bot/v2/hook/" + token
}
urls = append(urls, url)
}
}
return urls
}

98
alert/sender/larkcard.go Normal file
View File

@@ -0,0 +1,98 @@
package sender
import (
"fmt"
"html/template"
"net/url"
"strings"
"github.com/ccfos/nightingale/v6/models"
)
type LarkCardSender struct {
tpl *template.Template
}
func (fs *LarkCardSender) CallBack(ctx CallBackContext) {
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
return
}
ats := ExtractAtsParams(ctx.CallBackURL)
message := BuildTplMessage(models.LarkCard, fs.tpl, ctx.Events)
if len(ats) > 0 {
atTags := ""
for _, at := range ats {
if strings.Contains(at, "@") {
atTags += fmt.Sprintf("<at email=\"%s\" ></at>", at)
} else {
atTags += fmt.Sprintf("<at id=\"%s\" ></at>", at)
}
}
message = atTags + message
}
color := "red"
lowerUnicode := strings.ToLower(message)
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
color = "orange"
} else if strings.Count(lowerUnicode, Recovered) > 0 {
color = "green"
}
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
body.Card.Header.Title.Content = SendTitle
body.Card.Header.Template = color
body.Card.Elements[0].Text.Content = message
body.Card.Elements[2].Elements[0].Content = SendTitle
// This is to be compatible with the Larkcard interface, if with query string parameters, the request will fail
// Remove query parameters from the URL,
parsedURL, err := url.Parse(ctx.CallBackURL)
if err != nil {
return
}
parsedURL.RawQuery = ""
doSend(parsedURL.String(), body, models.LarkCard, ctx.Stats)
}
func (fs *LarkCardSender) Send(ctx MessageContext) {
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
return
}
urls, _ := fs.extract(ctx.Users)
message := BuildTplMessage(models.LarkCard, fs.tpl, ctx.Events)
color := "red"
lowerUnicode := strings.ToLower(message)
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
color = "orange"
} else if strings.Count(lowerUnicode, Recovered) > 0 {
color = "green"
}
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
body.Card.Header.Title.Content = SendTitle
body.Card.Header.Template = color
body.Card.Elements[0].Text.Content = message
body.Card.Elements[2].Elements[0].Content = SendTitle
for _, url := range urls {
doSend(url, body, models.LarkCard, ctx.Stats)
}
}
func (fs *LarkCardSender) extract(users []*models.User) ([]string, []string) {
urls := make([]string, 0, len(users))
ats := make([]string, 0)
for i := range users {
if token, has := users[i].ExtractToken(models.Lark); has {
url := token
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
url = "https://open.larksuite.com/open-apis/bot/v2/hook/" + strings.TrimSpace(token)
}
urls = append(urls, url)
}
}
return urls, ats
}

View File

@@ -41,6 +41,10 @@ func NewSender(key string, tpls map[string]*template.Template, smtp ...aconf.SMT
return &MmSender{tpl: tpls[models.Mm]}
case models.Telegram:
return &TelegramSender{tpl: tpls[models.Telegram]}
case models.Lark:
return &LarkSender{tpl: tpls[models.Lark]}
case models.LarkCard:
return &LarkCardSender{tpl: tpls[models.LarkCard]}
}
return nil
}

View File

@@ -2,9 +2,11 @@ package sender
import (
"bytes"
"crypto/tls"
"encoding/json"
"io"
"net/http"
"sync"
"time"
"github.com/ccfos/nightingale/v6/alert/astats"
@@ -13,14 +15,19 @@ import (
"github.com/toolkits/pkg/logger"
)
func sendWebhook(webhook *models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) bool {
func sendWebhook(webhook *models.Webhook, event interface{}, stats *astats.Stats) bool {
channel := "webhook"
if webhook.Type == models.RuleCallback {
channel = "callback"
}
conf := webhook
if conf.Url == "" || !conf.Enable {
return false
}
bs, err := json.Marshal(event)
if err != nil {
logger.Errorf("alertingWebhook failed to marshal event:%+v err:%v", event, err)
logger.Errorf("%s alertingWebhook failed to marshal event:%+v err:%v", channel, event, err)
return false
}
@@ -28,7 +35,7 @@ func sendWebhook(webhook *models.Webhook, event *models.AlertCurEvent, stats *as
req, err := http.NewRequest("POST", conf.Url, bf)
if err != nil {
logger.Warningf("alertingWebhook failed to new reques event:%+v err:%v", event, err)
logger.Warningf("%s alertingWebhook failed to new reques event:%s err:%v", channel, string(bs), err)
return true
}
@@ -46,18 +53,23 @@ func sendWebhook(webhook *models.Webhook, event *models.AlertCurEvent, stats *as
req.Header.Set(conf.Headers[i], conf.Headers[i+1])
}
}
// todo add skip verify
insecureSkipVerify := false
if webhook != nil {
insecureSkipVerify = webhook.SkipVerify
}
client := http.Client{
Timeout: time.Duration(conf.Timeout) * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify},
},
}
stats.AlertNotifyTotal.WithLabelValues("webhook").Inc()
stats.AlertNotifyTotal.WithLabelValues(channel).Inc()
var resp *http.Response
resp, err = client.Do(req)
if err != nil {
stats.AlertNotifyErrorTotal.WithLabelValues("webhook").Inc()
logger.Errorf("event_webhook_fail, ruleId: [%d], eventId: [%d], event:%+v, url: [%s], error: [%s]", event.RuleId, event.Id, event, conf.Url, err)
stats.AlertNotifyErrorTotal.WithLabelValues(channel).Inc()
logger.Errorf("event_%s_fail, event:%s, url: [%s], error: [%s]", channel, string(bs), conf.Url, err)
return true
}
@@ -68,15 +80,15 @@ func sendWebhook(webhook *models.Webhook, event *models.AlertCurEvent, stats *as
}
if resp.StatusCode == 429 {
logger.Errorf("event_webhook_fail, url: %s, response code: %d, body: %s event:%+v", conf.Url, resp.StatusCode, string(body), event)
logger.Errorf("event_%s_fail, url: %s, response code: %d, body: %s event:%s", channel, conf.Url, resp.StatusCode, string(body), string(bs))
return true
}
logger.Debugf("event_webhook_succ, url: %s, response code: %d, body: %s event:%+v", conf.Url, resp.StatusCode, string(body), event)
logger.Debugf("event_%s_succ, url: %s, response code: %d, body: %s event:%s", channel, conf.Url, resp.StatusCode, string(body), string(bs))
return false
}
func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
func SingleSendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
for _, conf := range webhooks {
retryCount := 0
for retryCount < 3 {
@@ -85,7 +97,77 @@ func SendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent, stats
break
}
retryCount++
time.Sleep(time.Second * 10 * time.Duration(retryCount))
time.Sleep(time.Minute * 1 * time.Duration(retryCount))
}
}
}
func BatchSendWebhooks(webhooks []*models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
for _, conf := range webhooks {
logger.Infof("push event:%+v to queue:%v", event, conf)
PushEvent(conf, event, stats)
}
}
var EventQueue = make(map[string]*WebhookQueue)
var CallbackEventQueue = make(map[string]*WebhookQueue)
var CallbackEventQueueLock sync.RWMutex
var EventQueueLock sync.RWMutex
const QueueMaxSize = 100000
type WebhookQueue struct {
list *SafeListLimited
closeCh chan struct{}
}
func PushEvent(webhook *models.Webhook, event *models.AlertCurEvent, stats *astats.Stats) {
EventQueueLock.RLock()
queue := EventQueue[webhook.Url]
EventQueueLock.RUnlock()
if queue == nil {
queue = &WebhookQueue{
list: NewSafeListLimited(QueueMaxSize),
closeCh: make(chan struct{}),
}
EventQueueLock.Lock()
EventQueue[webhook.Url] = queue
EventQueueLock.Unlock()
StartConsumer(queue, webhook.Batch, webhook, stats)
}
succ := queue.list.PushFront(event)
if !succ {
stats.AlertNotifyErrorTotal.WithLabelValues("push_event_queue").Inc()
logger.Warningf("Write channel(%s) full, current channel size: %d event:%v", webhook.Url, queue.list.Len(), event)
}
}
func StartConsumer(queue *WebhookQueue, popSize int, webhook *models.Webhook, stats *astats.Stats) {
for {
select {
case <-queue.closeCh:
logger.Infof("event queue:%v closed", queue)
return
default:
events := queue.list.PopBack(popSize)
if len(events) == 0 {
time.Sleep(time.Millisecond * 400)
continue
}
retryCount := 0
for retryCount < webhook.RetryCount {
needRetry := sendWebhook(webhook, events, stats)
if !needRetry {
break
}
retryCount++
time.Sleep(time.Second * time.Duration(webhook.RetryInterval) * time.Duration(retryCount))
}
}
}
}

View File

@@ -0,0 +1,111 @@
package sender
import (
"container/list"
"sync"
"github.com/ccfos/nightingale/v6/models"
)
type SafeList struct {
sync.RWMutex
L *list.List
}
func NewSafeList() *SafeList {
return &SafeList{L: list.New()}
}
func (sl *SafeList) PushFront(v interface{}) *list.Element {
sl.Lock()
e := sl.L.PushFront(v)
sl.Unlock()
return e
}
func (sl *SafeList) PushFrontBatch(vs []interface{}) {
sl.Lock()
for _, item := range vs {
sl.L.PushFront(item)
}
sl.Unlock()
}
func (sl *SafeList) PopBack(max int) []*models.AlertCurEvent {
sl.Lock()
count := sl.L.Len()
if count == 0 {
sl.Unlock()
return []*models.AlertCurEvent{}
}
if count > max {
count = max
}
items := make([]*models.AlertCurEvent, 0, count)
for i := 0; i < count; i++ {
item := sl.L.Remove(sl.L.Back())
sample, ok := item.(*models.AlertCurEvent)
if ok {
items = append(items, sample)
}
}
sl.Unlock()
return items
}
func (sl *SafeList) RemoveAll() {
sl.Lock()
sl.L.Init()
sl.Unlock()
}
func (sl *SafeList) Len() int {
sl.RLock()
size := sl.L.Len()
sl.RUnlock()
return size
}
// SafeList with Limited Size
type SafeListLimited struct {
maxSize int
SL *SafeList
}
func NewSafeListLimited(maxSize int) *SafeListLimited {
return &SafeListLimited{SL: NewSafeList(), maxSize: maxSize}
}
func (sll *SafeListLimited) PopBack(max int) []*models.AlertCurEvent {
return sll.SL.PopBack(max)
}
func (sll *SafeListLimited) PushFront(v interface{}) bool {
if sll.SL.Len() >= sll.maxSize {
return false
}
sll.SL.PushFront(v)
return true
}
func (sll *SafeListLimited) PushFrontBatch(vs []interface{}) bool {
if sll.SL.Len() >= sll.maxSize {
return false
}
sll.SL.PushFrontBatch(vs)
return true
}
func (sll *SafeListLimited) RemoveAll() {
sll.SL.RemoveAll()
}
func (sll *SafeListLimited) Len() int {
return sll.SL.Len()
}

View File

@@ -18,20 +18,28 @@ var MetricDesc MetricDescType
// GetMetricDesc , if metric is not registered, empty string will be returned
func GetMetricDesc(lang, metric string) string {
var m map[string]string
if lang == "zh" {
m = MetricDesc.Zh
} else {
switch lang {
case "en":
m = MetricDesc.En
default:
m = MetricDesc.Zh
}
if m != nil {
if desc, has := m[metric]; has {
if desc, ok := m[metric]; ok {
return desc
}
}
return MetricDesc.CommonDesc[metric]
}
if MetricDesc.CommonDesc != nil {
if desc, ok := MetricDesc.CommonDesc[metric]; ok {
return desc
}
}
return ""
}
func LoadMetricsYaml(configDir, metricsYamlFile string) error {
fp := metricsYamlFile
if fp == "" {

View File

@@ -78,6 +78,7 @@ ops:
- "/dashboards/del"
- "/embedded-dashboards/put"
- "/embedded-dashboards"
- "/public-dashboards"
- name: alert
cname: 告警规则

View File

@@ -107,7 +107,7 @@ func Initialize(configDir string, cryptoKey string) (func(), error) {
go version.GetGithubVersion()
alertrtRouter := alertrt.New(config.HTTP, config.Alert, alertMuteCache, targetCache, busiGroupCache, alertStats, ctx, externalProcessors)
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
centerRouter := centerrt.New(config.HTTP, config.Center, config.Alert, config.Ibex, cconf.Operations, dsCache, notifyConfigCache, promClients, tdengineClients,
redis, sso, ctx, metas, idents, targetCache, userCache, userGroupCache)
pushgwRouter := pushgwrt.New(config.HTTP, config.Pushgw, config.Alert, targetCache, busiGroupCache, idents, metas, writers, ctx)

View File

@@ -13,6 +13,7 @@ import (
"github.com/ccfos/nightingale/v6/center/cstats"
"github.com/ccfos/nightingale/v6/center/metas"
"github.com/ccfos/nightingale/v6/center/sso"
"github.com/ccfos/nightingale/v6/conf"
_ "github.com/ccfos/nightingale/v6/front/statik"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/pkg/aop"
@@ -34,6 +35,7 @@ import (
type Router struct {
HTTP httpx.Config
Center cconf.Center
Ibex conf.Ibex
Alert aconf.Alert
Operations cconf.Operation
DatasourceCache *memsto.DatasourceCacheType
@@ -48,13 +50,15 @@ type Router struct {
UserCache *memsto.UserCacheType
UserGroupCache *memsto.UserGroupCacheType
Ctx *ctx.Context
HeartbeatHook HeartbeatHookFunc
}
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType, pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis, sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set, tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, ibex conf.Ibex, operations cconf.Operation, ds *memsto.DatasourceCacheType, ncc *memsto.NotifyConfigCacheType, pc *prom.PromClientMap, tdendgineClients *tdengine.TdengineClientMap, redis storage.Redis, sso *sso.SsoClient, ctx *ctx.Context, metaSet *metas.Set, idents *idents.Set, tc *memsto.TargetCacheType, uc *memsto.UserCacheType, ugc *memsto.UserGroupCacheType) *Router {
return &Router{
HTTP: httpConfig,
Center: center,
Alert: alert,
Ibex: ibex,
Operations: operations,
DatasourceCache: ds,
NotifyConfigCache: ncc,
@@ -68,6 +72,7 @@ func New(httpConfig httpx.Config, center cconf.Center, alert aconf.Alert, operat
UserCache: uc,
UserGroupCache: ugc,
Ctx: ctx,
HeartbeatHook: func(ident string) map[string]interface{} { return nil },
}
}
@@ -91,7 +96,9 @@ func languageDetector(i18NHeaderKey string) gin.HandlerFunc {
if headerKey != "" {
lang := c.GetHeader(headerKey)
if lang != "" {
if strings.HasPrefix(lang, "zh") {
if strings.HasPrefix(lang, "zh_HK") {
c.Request.Header.Set("X-Language", "zh_HK")
} else if strings.HasPrefix(lang, "zh") {
c.Request.Header.Set("X-Language", "zh_CN")
} else if strings.HasPrefix(lang, "en") {
c.Request.Header.Set("X-Language", "en")
@@ -112,7 +119,7 @@ func (rt *Router) configNoRoute(r *gin.Engine, fs *http.FileSystem) {
suffix := arr[len(arr)-1]
switch suffix {
case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map", "ttf":
case "png", "jpeg", "jpg", "svg", "ico", "gif", "css", "js", "html", "htm", "gz", "zip", "map", "ttf", "md":
if !rt.Center.UseFileAssets {
c.FileFromFS(c.Request.URL.Path, *fs)
} else {
@@ -314,6 +321,7 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/alert-rule/:arid", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRuleGet)
pages.GET("/alert-rule/:arid/pure", rt.auth(), rt.user(), rt.perm("/alert-rules"), rt.alertRulePureGet)
pages.PUT("/busi-group/alert-rule/validate", rt.auth(), rt.user(), rt.perm("/alert-rules/put"), rt.alertRuleValidation)
pages.POST("/relabel-test", rt.auth(), rt.user(), rt.relabelTest)
pages.GET("/busi-groups/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGetsByGids)
pages.GET("/busi-group/:id/recording-rules", rt.auth(), rt.user(), rt.perm("/recording-rules"), rt.recordingRuleGets)
@@ -373,8 +381,8 @@ func (rt *Router) Config(r *gin.Engine) {
pages.GET("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks"), rt.bgro(), rt.taskGets)
pages.POST("/busi-group/:id/tasks", rt.auth(), rt.user(), rt.perm("/job-tasks/add"), rt.bgrw(), rt.taskAdd)
pages.GET("/servers", rt.auth(), rt.user(), rt.perm("/help/servers"), rt.serversGet)
pages.GET("/server-clusters", rt.auth(), rt.user(), rt.perm("/help/servers"), rt.serverClustersGet)
pages.GET("/servers", rt.auth(), rt.user(), rt.serversGet)
pages.GET("/server-clusters", rt.auth(), rt.user(), rt.serverClustersGet)
pages.POST("/datasource/list", rt.auth(), rt.user(), rt.datasourceList)
pages.POST("/datasource/plugin/list", rt.auth(), rt.pluginList)

View File

@@ -1,14 +1,18 @@
package router
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/ccfos/nightingale/v6/pushgw/writer"
"github.com/gin-gonic/gin"
"github.com/prometheus/prometheus/prompb"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
"github.com/toolkits/pkg/str"
@@ -402,3 +406,50 @@ func (rt *Router) alertRuleCallbacks(c *gin.Context) {
ginx.NewRender(c).Data(callbacks, nil)
}
type alertRuleTestForm struct {
Configs []*pconf.RelabelConfig `json:"configs"`
Tags []string `json:"tags"`
}
func (rt *Router) relabelTest(c *gin.Context) {
var f alertRuleTestForm
ginx.BindJSON(c, &f)
if len(f.Tags) == 0 || len(f.Configs) == 0 {
ginx.Bomb(http.StatusBadRequest, "relabel config is empty")
}
labels := make([]prompb.Label, len(f.Tags))
for i, tag := range f.Tags {
label := strings.Split(tag, "=")
if len(label) != 2 {
ginx.Bomb(http.StatusBadRequest, "tag:%s format error", tag)
}
labels[i] = prompb.Label{Name: label[0], Value: label[1]}
}
for i := 0; i < len(f.Configs); i++ {
if f.Configs[i].Replacement == "" {
f.Configs[i].Replacement = "$1"
}
if f.Configs[i].Separator == "" {
f.Configs[i].Separator = ";"
}
if f.Configs[i].Regex == "" {
f.Configs[i].Regex = "(.*)"
}
}
relabels := writer.Process(labels, f.Configs...)
var tags []string
for _, label := range relabels {
tags = append(tags, fmt.Sprintf("%s=%s", label.Name, label.Value))
}
ginx.NewRender(c).Data(tags, nil)
}

View File

@@ -3,19 +3,33 @@ package router
import (
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"sort"
"strings"
"time"
"github.com/ccfos/nightingale/v6/center/metas"
"github.com/ccfos/nightingale/v6/memsto"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pushgw/idents"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
type HeartbeatHookFunc func(ident string) map[string]interface{}
func (rt *Router) heartbeat(c *gin.Context) {
req, err := HandleHeartbeat(c, rt.Ctx, rt.Alert.Heartbeat.EngineName, rt.MetaSet, rt.IdentSet, rt.TargetCache)
ginx.Dangerous(err)
m := rt.HeartbeatHook(req.Hostname)
ginx.NewRender(c).Data(m, err)
}
func HandleHeartbeat(c *gin.Context, ctx *ctx.Context, engineName string, metaSet *metas.Set, identSet *idents.Set, targetCache *memsto.TargetCacheType) (models.HostMeta, error) {
var bs []byte
var err error
var r *gzip.Reader
@@ -24,7 +38,7 @@ func (rt *Router) heartbeat(c *gin.Context) {
r, err = gzip.NewReader(c.Request.Body)
if err != nil {
c.String(400, err.Error())
return
return req, err
}
defer r.Close()
bs, err = ioutil.ReadAll(r)
@@ -32,14 +46,18 @@ func (rt *Router) heartbeat(c *gin.Context) {
} else {
defer c.Request.Body.Close()
bs, err = ioutil.ReadAll(c.Request.Body)
ginx.Dangerous(err)
if err != nil {
return req, err
}
}
err = json.Unmarshal(bs, &req)
ginx.Dangerous(err)
if err != nil {
return req, err
}
if req.Hostname == "" {
ginx.Dangerous("hostname is required", 400)
return req, fmt.Errorf("hostname is required", 400)
}
// maybe from pushgw
@@ -52,54 +70,65 @@ func (rt *Router) heartbeat(c *gin.Context) {
}
if req.EngineName == "" {
req.EngineName = rt.Alert.Heartbeat.EngineName
req.EngineName = engineName
}
rt.MetaSet.Set(req.Hostname, req)
metaSet.Set(req.Hostname, req)
var items = make(map[string]struct{})
items[req.Hostname] = struct{}{}
rt.IdentSet.MSet(items)
identSet.MSet(items)
if target, has := rt.TargetCache.Get(req.Hostname); has && target != nil {
if target, has := targetCache.Get(req.Hostname); has && target != nil {
gid := ginx.QueryInt64(c, "gid", 0)
hostIp := strings.TrimSpace(req.HostIp)
filed := make(map[string]interface{})
field := make(map[string]interface{})
if gid != 0 && gid != target.GroupId {
filed["group_id"] = gid
field["group_id"] = gid
}
if hostIp != "" && hostIp != target.HostIp {
filed["host_ip"] = hostIp
field["host_ip"] = hostIp
}
if len(req.GlobalLabels) > 0 {
tagsMap := target.GetTagsMap()
tagNeedUpdate := false
for k, v := range req.GlobalLabels {
if v == "" {
continue
}
if tagv, ok := tagsMap[k]; !ok || tagv != v {
tagNeedUpdate = true
tagsMap[k] = v
}
}
if tagNeedUpdate {
lst := []string{}
for k, v := range req.GlobalLabels {
if v == "" {
continue
}
for k, v := range tagsMap {
lst = append(lst, k+"="+v)
}
sort.Strings(lst)
labels := strings.Join(lst, " ")
if target.Tags != labels {
filed["tags"] = labels
}
labels := strings.Join(lst, " ") + " "
field["tags"] = labels
}
if req.EngineName != "" && req.EngineName != target.EngineName {
filed["engine_name"] = req.EngineName
field["engine_name"] = req.EngineName
}
if len(filed) > 0 {
err := target.UpdateFieldsMap(rt.Ctx, filed)
if req.AgentVersion != "" && req.AgentVersion != target.AgentVersion {
field["agent_version"] = req.AgentVersion
}
if len(field) > 0 {
err := target.UpdateFieldsMap(ctx, field)
if err != nil {
logger.Errorf("update target fields failed, err: %v", err)
}
}
logger.Debugf("heartbeat field:%+v target: %v", filed, *target)
logger.Debugf("heartbeat field:%+v target: %v", field, *target)
}
ginx.NewRender(c).Message(err)
return req, nil
}

View File

@@ -95,7 +95,8 @@ func (rt *Router) alertMuteAddByService(c *gin.Context) {
var f models.AlertMute
ginx.BindJSON(c, &f)
ginx.NewRender(c).Message(f.Add(rt.Ctx))
err := f.Add(rt.Ctx)
ginx.NewRender(c).Data(f.Id, err)
}
func (rt *Router) alertMuteDel(c *gin.Context) {

View File

@@ -90,7 +90,8 @@ func (rt *Router) notifyChannelPuts(c *gin.Context) {
var notifyChannels []models.NotifyChannel
ginx.BindJSON(c, &notifyChannels)
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram, models.Email}
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram,
models.Email, models.Lark, models.LarkCard}
m := make(map[string]struct{})
for _, v := range notifyChannels {
@@ -126,7 +127,8 @@ func (rt *Router) notifyContactPuts(c *gin.Context) {
var notifyContacts []models.NotifyContact
ginx.BindJSON(c, &notifyContacts)
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey, models.TelegramKey}
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey,
models.TelegramKey, models.LarkKey}
m := make(map[string]struct{})
for _, v := range notifyContacts {

View File

@@ -49,6 +49,9 @@ func (rt *Router) targetGets(c *gin.Context) {
downtime := ginx.QueryInt64(c, "downtime", 0)
dsIds := queryDatasourceIds(c)
order := ginx.QueryStr(c, "order", "ident")
desc := ginx.QueryBool(c, "desc", false)
var err error
if len(bgids) == 0 {
user := c.MustGet("user").(*models.User)
@@ -62,11 +65,17 @@ func (rt *Router) targetGets(c *gin.Context) {
bgids = append(bgids, 0)
}
}
total, err := models.TargetTotal(rt.Ctx, bgids, dsIds, query, downtime)
options := []models.BuildTargetWhereOption{
models.BuildTargetWhereWithBgids(bgids),
models.BuildTargetWhereWithDsIds(dsIds),
models.BuildTargetWhereWithQuery(query),
models.BuildTargetWhereWithDowntime(downtime),
}
total, err := models.TargetTotal(rt.Ctx, options...)
ginx.Dangerous(err)
list, err := models.TargetGets(rt.Ctx, bgids, dsIds, query, downtime, limit, ginx.Offset(c, limit))
list, err := models.TargetGets(rt.Ctx, limit,
ginx.Offset(c, limit), order, desc, options...)
ginx.Dangerous(err)
if err == nil {

View File

@@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
"github.com/toolkits/pkg/str"
)
@@ -104,6 +105,11 @@ func (rt *Router) taskRecordAdd(c *gin.Context) {
}
func (rt *Router) taskAdd(c *gin.Context) {
if !rt.Ibex.Enable {
ginx.Bomb(400, i18n.Sprintf(c.GetHeader("X-Language"), "This functionality has not been enabled. Please contact the system administrator to activate it."))
return
}
var f models.TaskForm
ginx.BindJSON(c, &f)

View File

@@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/i18n"
"github.com/toolkits/pkg/str"
)
@@ -118,6 +119,11 @@ type taskTplForm struct {
}
func (rt *Router) taskTplAdd(c *gin.Context) {
if !rt.Ibex.Enable {
ginx.Bomb(400, i18n.Sprintf(c.GetHeader("X-Language"), "This functionality has not been enabled. Please contact the system administrator to activate it."))
return
}
var f taskTplForm
ginx.BindJSON(c, &f)

View File

@@ -441,7 +441,7 @@ CREATE TABLE `alert_cur_event` (
`prom_for_duration` int not null comment 'prometheus for, unit:s',
`prom_ql` varchar(8192) not null comment 'promql',
`prom_eval_interval` int not null comment 'evaluate interval',
`callbacks` varchar(255) not null default '' comment 'split by space: http://a.com/api/x http://a.com/api/y',
`callbacks` varchar(2048) not null default '' comment 'split by space: http://a.com/api/x http://a.com/api/y',
`runbook_url` varchar(255),
`notify_recovered` tinyint(1) not null comment 'whether notify when recovery',
`notify_channels` varchar(255) not null default '' comment 'split by space: sms voice email dingtalk wecom',
@@ -456,6 +456,7 @@ CREATE TABLE `alert_cur_event` (
`annotations` text not null comment 'annotations',
`rule_config` text not null comment 'annotations',
`tags` varchar(1024) not null default '' comment 'merge data_tags rule_tags, split by ,,',
`original_tags` text comment 'labels key=val,,k2=v2',
PRIMARY KEY (`id`),
KEY (`hash`),
KEY (`rule_id`),
@@ -481,7 +482,7 @@ CREATE TABLE `alert_his_event` (
`prom_for_duration` int not null comment 'prometheus for, unit:s',
`prom_ql` varchar(8192) not null comment 'promql',
`prom_eval_interval` int not null comment 'evaluate interval',
`callbacks` varchar(255) not null default '' comment 'split by space: http://a.com/api/x http://a.com/api/y',
`callbacks` varchar(2048) not null default '' comment 'split by space: http://a.com/api/x http://a.com/api/y',
`runbook_url` varchar(255),
`notify_recovered` tinyint(1) not null comment 'whether notify when recovery',
`notify_channels` varchar(255) not null default '' comment 'split by space: sms voice email dingtalk wecom',
@@ -495,6 +496,7 @@ CREATE TABLE `alert_his_event` (
`recover_time` bigint not null default 0,
`last_eval_time` bigint not null default 0 comment 'for time filter',
`tags` varchar(1024) not null default '' comment 'merge data_tags rule_tags, split by ,,',
`original_tags` text comment 'labels key=val,,k2=v2',
`annotations` text not null comment 'annotations',
`rule_config` text not null comment 'annotations',
PRIMARY KEY (`id`),

View File

@@ -79,4 +79,8 @@ CREATE TABLE `builtin_payloads` (
ALTER TABLE users ADD COLUMN last_active_time BIGINT NOT NULL DEFAULT 0;
/* v7.0.0-beta.13 */
ALTER TABLE recording_rule ADD COLUMN cron_pattern VARCHAR(255) DEFAULT '' COMMENT 'cron pattern';
ALTER TABLE recording_rule ADD COLUMN cron_pattern VARCHAR(255) DEFAULT '' COMMENT 'cron pattern';
/* v7.0.0-beta.14 */
ALTER TABLE alert_cur_event ADD COLUMN original_tags TEXT COMMENT 'labels key=val,,k2=v2';
ALTER TABLE alert_his_event ADD COLUMN original_tags TEXT COMMENT 'labels key=val,,k2=v2';

View File

@@ -52,6 +52,8 @@ type AlertCurEvent struct {
Tags string `json:"-"` // for db
TagsJSON []string `json:"tags" gorm:"-"` // for fe
TagsMap map[string]string `json:"tags_map" gorm:"-"` // for internal usage
OriginalTags string `json:"-"` // for db
OriginalTagsJSON []string `json:"original_tags" gorm:"-"` // for fe
Annotations string `json:"-"` //
AnnotationsJSON map[string]string `json:"annotations" gorm:"-"` // for fe
IsRecovered bool `json:"is_recovered" gorm:"-"` // for notify.py
@@ -289,6 +291,7 @@ func (e *AlertCurEvent) ToHis(ctx *ctx.Context) *AlertHisEvent {
TriggerTime: e.TriggerTime,
TriggerValue: e.TriggerValue,
Tags: e.Tags,
OriginalTags: e.OriginalTags,
RecoverTime: recoverTime,
LastEvalTime: e.LastEvalTime,
NotifyCurNumber: e.NotifyCurNumber,
@@ -301,6 +304,7 @@ func (e *AlertCurEvent) DB2FE() error {
e.NotifyGroupsJSON = strings.Fields(e.NotifyGroups)
e.CallbacksJSON = strings.Fields(e.Callbacks)
e.TagsJSON = strings.Split(e.Tags, ",,")
e.OriginalTagsJSON = strings.Split(e.OriginalTags, ",,")
json.Unmarshal([]byte(e.Annotations), &e.AnnotationsJSON)
json.Unmarshal([]byte(e.RuleConfig), &e.RuleConfigJson)
return nil
@@ -311,6 +315,7 @@ func (e *AlertCurEvent) FE2DB() {
e.NotifyGroups = strings.Join(e.NotifyGroupsJSON, " ")
e.Callbacks = strings.Join(e.CallbacksJSON, " ")
e.Tags = strings.Join(e.TagsJSON, ",,")
e.OriginalTags = strings.Join(e.OriginalTagsJSON, ",,")
b, _ := json.Marshal(e.AnnotationsJSON)
e.Annotations = string(b)

View File

@@ -48,6 +48,8 @@ type AlertHisEvent struct {
LastEvalTime int64 `json:"last_eval_time"`
Tags string `json:"-"`
TagsJSON []string `json:"tags" gorm:"-"`
OriginalTags string `json:"-"` // for db
OriginalTagsJSON []string `json:"original_tags" gorm:"-"` // for fe
Annotations string `json:"-"`
AnnotationsJSON map[string]string `json:"annotations" gorm:"-"` // for fe
NotifyCurNumber int `json:"notify_cur_number"` // notify: current number
@@ -68,6 +70,7 @@ func (e *AlertHisEvent) DB2FE() {
e.NotifyGroupsJSON = strings.Fields(e.NotifyGroups)
e.CallbacksJSON = strings.Fields(e.Callbacks)
e.TagsJSON = strings.Split(e.Tags, ",,")
e.OriginalTagsJSON = strings.Split(e.OriginalTags, ",,")
if len(e.Annotations) > 0 {
err := json.Unmarshal([]byte(e.Annotations), &e.AnnotationsJSON)
@@ -306,13 +309,14 @@ func EventPersist(ctx *ctx.Context, event *AlertCurEvent) error {
return nil
}
// use his id as cur id
event.Id = his.Id
if event.IsRecovered {
// alert_cur_event表里没有数据表示之前没告警结果现在报了恢复神奇....理论上不应该出现的
return nil
}
// use his id as cur id
event.Id = his.Id
if event.Id > 0 {
if err := event.Add(ctx); err != nil {
return fmt.Errorf("add cur event error:%v", err)

View File

@@ -9,6 +9,7 @@ import (
"github.com/ccfos/nightingale/v6/pkg/ctx"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/pkg/errors"
"github.com/toolkits/pkg/logger"
@@ -26,60 +27,61 @@ const (
)
type AlertRule struct {
Id int64 `json:"id" gorm:"primaryKey"`
GroupId int64 `json:"group_id"` // busi group id
Cate string `json:"cate"` // alert rule cate (prometheus|elasticsearch)
DatasourceIds string `json:"-" gorm:"datasource_ids"` // datasource ids
DatasourceIdsJson []int64 `json:"datasource_ids" gorm:"-"` // for fe
Cluster string `json:"cluster"` // take effect by clusters, seperated by space
Name string `json:"name"` // rule name
Note string `json:"note"` // will sent in notify
Prod string `json:"prod"` // product empty means n9e
Algorithm string `json:"algorithm"` // algorithm (''|holtwinters), empty means threshold
AlgoParams string `json:"-" gorm:"algo_params"` // params algorithm need
AlgoParamsJson interface{} `json:"algo_params" gorm:"-"` // for fe
Delay int `json:"delay"` // Time (in seconds) to delay evaluation
Severity int `json:"severity"` // 1: Emergency 2: Warning 3: Notice
Severities []int `json:"severities" gorm:"-"` // 1: Emergency 2: Warning 3: Notice
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
PromForDuration int `json:"prom_for_duration"` // prometheus for, unit:s
PromQl string `json:"prom_ql"` // just one ql
RuleConfig string `json:"-" gorm:"rule_config"` // rule config
RuleConfigJson interface{} `json:"rule_config" gorm:"-"` // rule config for fe
PromEvalInterval int `json:"prom_eval_interval"` // unit:s
EnableStime string `json:"-"` // split by space: "00:00 10:00 12:00"
EnableStimeJSON string `json:"enable_stime" gorm:"-"` // for fe
EnableStimesJSON []string `json:"enable_stimes" gorm:"-"` // for fe
EnableEtime string `json:"-"` // split by space: "00:00 10:00 12:00"
EnableEtimeJSON string `json:"enable_etime" gorm:"-"` // for fe
EnableEtimesJSON []string `json:"enable_etimes" gorm:"-"` // for fe
EnableDaysOfWeek string `json:"-"` // eg: "0 1 2 3 4 5 6 ; 0 1 2"
EnableDaysOfWeekJSON []string `json:"enable_days_of_week" gorm:"-"` // for fe
EnableDaysOfWeeksJSON [][]string `json:"enable_days_of_weeks" gorm:"-"` // for fe
EnableInBG int `json:"enable_in_bg"` // 0: global 1: enable one busi-group
NotifyRecovered int `json:"notify_recovered"` // whether notify when recovery
NotifyChannels string `json:"-"` // split by space: sms voice email dingtalk wecom
NotifyChannelsJSON []string `json:"notify_channels" gorm:"-"` // for fe
NotifyGroups string `json:"-"` // split by space: 233 43
NotifyGroupsObj []UserGroup `json:"notify_groups_obj" gorm:"-"` // for fe
NotifyGroupsJSON []string `json:"notify_groups" gorm:"-"` // for fe
NotifyRepeatStep int `json:"notify_repeat_step"` // notify repeat interval, unit: min
NotifyMaxNumber int `json:"notify_max_number"` // notify: max number
RecoverDuration int64 `json:"recover_duration"` // unit: s
Callbacks string `json:"-"` // split by space: http://a.com/api/x http://a.com/api/y'
CallbacksJSON []string `json:"callbacks" gorm:"-"` // for fe
RunbookUrl string `json:"runbook_url"` // sop url
AppendTags string `json:"-"` // split by space: service=n9e mod=api
AppendTagsJSON []string `json:"append_tags" gorm:"-"` // for fe
Annotations string `json:"-"` //
AnnotationsJSON map[string]string `json:"annotations" gorm:"-"` // for fe
ExtraConfig string `json:"-" gorm:"extra_config"` // extra config
ExtraConfigJSON interface{} `json:"extra_config" gorm:"-"` // for fe
CreateAt int64 `json:"create_at"`
CreateBy string `json:"create_by"`
UpdateAt int64 `json:"update_at"`
UpdateBy string `json:"update_by"`
UUID int64 `json:"uuid" gorm:"-"` // tpl identifier
Id int64 `json:"id" gorm:"primaryKey"`
GroupId int64 `json:"group_id"` // busi group id
Cate string `json:"cate"` // alert rule cate (prometheus|elasticsearch)
DatasourceIds string `json:"-" gorm:"datasource_ids"` // datasource ids
DatasourceIdsJson []int64 `json:"datasource_ids" gorm:"-"` // for fe
Cluster string `json:"cluster"` // take effect by clusters, seperated by space
Name string `json:"name"` // rule name
Note string `json:"note"` // will sent in notify
Prod string `json:"prod"` // product empty means n9e
Algorithm string `json:"algorithm"` // algorithm (''|holtwinters), empty means threshold
AlgoParams string `json:"-" gorm:"algo_params"` // params algorithm need
AlgoParamsJson interface{} `json:"algo_params" gorm:"-"` // for fe
Delay int `json:"delay"` // Time (in seconds) to delay evaluation
Severity int `json:"severity"` // 1: Emergency 2: Warning 3: Notice
Severities []int `json:"severities" gorm:"-"` // 1: Emergency 2: Warning 3: Notice
Disabled int `json:"disabled"` // 0: enabled, 1: disabled
PromForDuration int `json:"prom_for_duration"` // prometheus for, unit:s
PromQl string `json:"prom_ql"` // just one ql
RuleConfig string `json:"-" gorm:"rule_config"` // rule config
RuleConfigJson interface{} `json:"rule_config" gorm:"-"` // rule config for fe
EventRelabelConfig []*pconf.RelabelConfig `json:"event_relabel_config" gorm:"-"` // event relabel config
PromEvalInterval int `json:"prom_eval_interval"` // unit:s
EnableStime string `json:"-"` // split by space: "00:00 10:00 12:00"
EnableStimeJSON string `json:"enable_stime" gorm:"-"` // for fe
EnableStimesJSON []string `json:"enable_stimes" gorm:"-"` // for fe
EnableEtime string `json:"-"` // split by space: "00:00 10:00 12:00"
EnableEtimeJSON string `json:"enable_etime" gorm:"-"` // for fe
EnableEtimesJSON []string `json:"enable_etimes" gorm:"-"` // for fe
EnableDaysOfWeek string `json:"-"` // eg: "0 1 2 3 4 5 6 ; 0 1 2"
EnableDaysOfWeekJSON []string `json:"enable_days_of_week" gorm:"-"` // for fe
EnableDaysOfWeeksJSON [][]string `json:"enable_days_of_weeks" gorm:"-"` // for fe
EnableInBG int `json:"enable_in_bg"` // 0: global 1: enable one busi-group
NotifyRecovered int `json:"notify_recovered"` // whether notify when recovery
NotifyChannels string `json:"-"` // split by space: sms voice email dingtalk wecom
NotifyChannelsJSON []string `json:"notify_channels" gorm:"-"` // for fe
NotifyGroups string `json:"-"` // split by space: 233 43
NotifyGroupsObj []UserGroup `json:"notify_groups_obj" gorm:"-"` // for fe
NotifyGroupsJSON []string `json:"notify_groups" gorm:"-"` // for fe
NotifyRepeatStep int `json:"notify_repeat_step"` // notify repeat interval, unit: min
NotifyMaxNumber int `json:"notify_max_number"` // notify: max number
RecoverDuration int64 `json:"recover_duration"` // unit: s
Callbacks string `json:"-"` // split by space: http://a.com/api/x http://a.com/api/y'
CallbacksJSON []string `json:"callbacks" gorm:"-"` // for fe
RunbookUrl string `json:"runbook_url"` // sop url
AppendTags string `json:"-"` // split by space: service=n9e mod=api
AppendTagsJSON []string `json:"append_tags" gorm:"-"` // for fe
Annotations string `json:"-"` //
AnnotationsJSON map[string]string `json:"annotations" gorm:"-"` // for fe
ExtraConfig string `json:"-" gorm:"extra_config"` // extra config
ExtraConfigJSON interface{} `json:"extra_config" gorm:"-"` // for fe
CreateAt int64 `json:"create_at"`
CreateBy string `json:"create_by"`
UpdateAt int64 `json:"update_at"`
UpdateBy string `json:"update_by"`
UUID int64 `json:"uuid" gorm:"-"` // tpl identifier
}
type PromRuleConfig struct {
@@ -622,6 +624,13 @@ func (ar *AlertRule) DB2FE() error {
json.Unmarshal([]byte(ar.Annotations), &ar.AnnotationsJSON)
json.Unmarshal([]byte(ar.ExtraConfig), &ar.ExtraConfigJSON)
// 解析 RuleConfig 字段
var ruleConfig struct {
EventRelabelConfig []*pconf.RelabelConfig `json:"event_relabel_config"`
}
json.Unmarshal([]byte(ar.RuleConfig), &ruleConfig)
ar.EventRelabelConfig = ruleConfig.EventRelabelConfig
err := ar.FillDatasourceIds()
return err
}

View File

@@ -60,8 +60,23 @@ func MigrateTables(db *gorm.DB) error {
&Board{}, &BoardBusigroup{}, &Users{}, &SsoConfig{}, &models.BuiltinMetric{},
&models.MetricFilter{}, &models.BuiltinComponent{}}
if !columnHasIndex(db, &AlertHisEvent{}, "last_eval_time") {
dts = append(dts, &AlertHisEvent{})
if !columnHasIndex(db, &AlertHisEvent{}, "original_tags") ||
!columnHasIndex(db, &AlertCurEvent{}, "original_tags") {
asyncDts := []interface{}{&AlertHisEvent{}, &AlertCurEvent{}}
go func() {
defer func() {
if r := recover(); r != nil {
logger.Errorf("panic to migrate table: %v", r)
}
}()
for _, dt := range asyncDts {
if err := db.AutoMigrate(dt); err != nil {
logger.Errorf("failed to migrate table: %v", err)
}
}
}()
}
if !db.Migrator().HasTable(&models.BuiltinPayload{}) {
@@ -203,8 +218,14 @@ type TaskRecord struct {
EventId int64 `gorm:"column:event_id;bigint(20);not null;default:0;comment:event id;index:idx_event_id"`
}
type AlertHisEvent struct {
LastEvalTime int64 `gorm:"column:last_eval_time;bigint(20);not null;default:0;comment:for time filter;index:idx_last_eval_time"`
LastEvalTime int64 `gorm:"column:last_eval_time;bigint(20);not null;default:0;comment:for time filter;index:idx_last_eval_time"`
OriginalTags string `gorm:"column:original_tags;type:text;comment:labels key=val,,k2=v2"`
}
type AlertCurEvent struct {
OriginalTags string `gorm:"column:original_tags;type:text;comment:labels key=val,,k2=v2"`
}
type Target struct {
HostIp string `gorm:"column:host_ip;varchar(15);default:'';comment:IPv4 string;index:idx_host_ip"`
AgentVersion string `gorm:"column:agent_version;varchar(255);default:'';comment:agent version;index:idx_agent_version"`

View File

@@ -7,7 +7,11 @@ const NOTIFYCONTACT = "notify_contact"
const SMTP = "smtp_config"
const IBEX = "ibex_server"
var GlobalCallback = 0
var RuleCallback = 1
type Webhook struct {
Type int `json:"type"`
Enable bool `json:"enable"`
Url string `json:"url"`
BasicAuthUser string `json:"basic_auth_user"`
@@ -17,6 +21,9 @@ type Webhook struct {
Headers []string `json:"headers_str"`
SkipVerify bool `json:"skip_verify"`
Note string `json:"note"`
RetryCount int `json:"retry_count"`
RetryInterval int `json:"retry_interval"`
Batch int `json:"batch"`
}
type NotifyScript struct {

View File

@@ -171,8 +171,7 @@ func InitNotifyConfig(c *ctx.Context, tplDir string) {
if cval == "" {
var notifyContacts []NotifyContact
contacts := []string{DingtalkKey, WecomKey, FeishuKey, MmKey, TelegramKey}
for _, contact := range contacts {
for _, contact := range DefaultContacts {
notifyContacts = append(notifyContacts, NotifyContact{Ident: contact, Name: contact, BuiltIn: true})
}
@@ -182,6 +181,35 @@ func InitNotifyConfig(c *ctx.Context, tplDir string) {
logger.Errorf("failed to set notify contact config: %v", err)
return
}
} else {
var contacts []NotifyContact
if err = json.Unmarshal([]byte(cval), &contacts); err != nil {
logger.Errorf("failed to unmarshal notify channel config: %v", err)
return
}
contactMap := make(map[string]struct{})
for _, contact := range contacts {
contactMap[contact.Ident] = struct{}{}
}
var newContacts []NotifyContact
for _, contact := range DefaultContacts {
if _, ok := contactMap[contact]; !ok {
newContacts = append(newContacts, NotifyContact{Ident: contact, Name: contact, BuiltIn: true})
}
}
if len(newContacts) > 0 {
contacts = append(contacts, newContacts...)
data, err := json.Marshal(contacts)
if err != nil {
logger.Errorf("failed to marshal contacts: %v", err)
return
}
if err = ConfigsSet(c, NOTIFYCONTACT, string(data)); err != nil {
logger.Errorf("failed to set notify contact config: %v", err)
return
}
}
}
// init notify tpl
@@ -254,7 +282,8 @@ var TplMap = map[string]string{
- {{$key}}: {{$val}}
{{- end}}
{{- end}}
`,
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
Email: `<!DOCTYPE html>
<html lang="en">
<head>
@@ -478,7 +507,10 @@ var TplMap = map[string]string{
监控指标: {{.TagsJSON}}
{{if .IsRecovered}}恢复时间:{{timeformat .LastEvalTime}}{{else}}触发时间: {{timeformat .TriggerTime}}
触发时值: {{.TriggerValue}}{{end}}
发送时间: {{timestamp}}`,
发送时间: {{timestamp}}
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
事件详情: {{$domain}}/alert-his-events/{{.Id}}
屏蔽1小时: {{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}}`,
FeishuCard: `{{ if .IsRecovered }}
{{- if ne .Cate "host"}}
**告警集群:** {{.Cluster}}{{end}}
@@ -495,7 +527,9 @@ var TplMap = map[string]string{
**发送时间:** {{timestamp}}
**触发时值:** {{.TriggerValue}}
{{if .RuleNote }}**告警描述:** **{{.RuleNote}}**{{end}}
{{- end -}}`,
{{- end -}}
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
EmailSubject: `{{if .IsRecovered}}Recovered{{else}}Triggered{{end}}: {{.RuleName}} {{.TagsJSON}}`,
Mm: `级别状态: S{{.Severity}} {{if .IsRecovered}}Recovered{{else}}Triggered{{end}}
规则名称: {{.RuleName}}{{if .RuleNote}}
@@ -521,5 +555,38 @@ var TplMap = map[string]string{
**触发时值**: {{.TriggerValue}}{{end}}
{{if .IsRecovered}}**恢复时间**: {{timeformat .LastEvalTime}}{{else}}**首次触发时间**: {{timeformat .FirstTriggerTime}}{{end}}
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**距离首次告警**: {{humanizeDurationInterface $time_duration}}
**发送时间**: {{timestamp}}`,
**发送时间**: {{timestamp}}
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
Lark: `级别状态: S{{.Severity}} {{if .IsRecovered}}Recovered{{else}}Triggered{{end}}
规则名称: {{.RuleName}}{{if .RuleNote}}
规则备注: {{.RuleNote}}{{end}}
监控指标: {{.TagsJSON}}
{{if .IsRecovered}}恢复时间:{{timeformat .LastEvalTime}}{{else}}触发时间: {{timeformat .TriggerTime}}
触发时值: {{.TriggerValue}}{{end}}
发送时间: {{timestamp}}
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
事件详情: {{$domain}}/alert-his-events/{{.Id}}
屏蔽1小时: {{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}}`,
LarkCard: `{{ if .IsRecovered }}
{{- if ne .Cate "host"}}
**告警集群:** {{.Cluster}}{{end}}
**级别状态:** S{{.Severity}} Recovered
**告警名称:** {{.RuleName}}
**恢复时间:** {{timeformat .LastEvalTime}}
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**持续时长**: {{humanizeDurationInterface $time_duration}}
**告警描述:** **服务已恢复**
{{- else }}
{{- if ne .Cate "host"}}
**告警集群:** {{.Cluster}}{{end}}
**级别状态:** S{{.Severity}} Triggered
**告警名称:** {{.RuleName}}
**触发时间:** {{timeformat .TriggerTime}}
**发送时间:** {{timestamp}}
**触发时值:** {{.TriggerValue}}
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**持续时长**: {{humanizeDurationInterface $time_duration}}
{{if .RuleNote }}**告警描述:** **{{.RuleNote}}**{{end}}
{{- end -}}
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
}

View File

@@ -85,43 +85,68 @@ func TargetDel(ctx *ctx.Context, idents []string) error {
return DB(ctx).Where("ident in ?", idents).Delete(new(Target)).Error
}
func buildTargetWhere(ctx *ctx.Context, bgids []int64, dsIds []int64, query string, downtime int64) *gorm.DB {
session := DB(ctx).Model(&Target{})
type BuildTargetWhereOption func(session *gorm.DB) *gorm.DB
if len(bgids) > 0 {
session = session.Where("group_id in (?)", bgids)
}
if len(dsIds) > 0 {
session = session.Where("datasource_id in (?)", dsIds)
}
if downtime > 0 {
session = session.Where("update_at < ?", time.Now().Unix()-downtime)
}
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
q := "%" + arr[i] + "%"
session = session.Where("ident like ? or note like ? or tags like ?", q, q, q)
func BuildTargetWhereWithBgids(bgids []int64) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if len(bgids) > 0 {
session = session.Where("group_id in (?)", bgids)
}
return session
}
}
func BuildTargetWhereWithDsIds(dsIds []int64) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if len(dsIds) > 0 {
session = session.Where("datasource_id in (?)", dsIds)
}
return session
}
}
func BuildTargetWhereWithQuery(query string) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if query != "" {
arr := strings.Fields(query)
for i := 0; i < len(arr); i++ {
q := "%" + arr[i] + "%"
session = session.Where("ident like ? or note like ? or tags like ?", q, q, q)
}
}
return session
}
}
func BuildTargetWhereWithDowntime(downtime int64) BuildTargetWhereOption {
return func(session *gorm.DB) *gorm.DB {
if downtime > 0 {
session = session.Where("update_at < ?", time.Now().Unix()-downtime)
}
return session
}
}
func buildTargetWhere(ctx *ctx.Context, options ...BuildTargetWhereOption) *gorm.DB {
session := DB(ctx).Model(&Target{})
for _, opt := range options {
session = opt(session)
}
return session
}
func TargetTotalCount(ctx *ctx.Context) (int64, error) {
return Count(DB(ctx).Model(new(Target)))
func TargetTotal(ctx *ctx.Context, options ...BuildTargetWhereOption) (int64, error) {
return Count(buildTargetWhere(ctx, options...))
}
func TargetTotal(ctx *ctx.Context, bgids []int64, dsIds []int64, query string, downtime int64) (int64, error) {
return Count(buildTargetWhere(ctx, bgids, dsIds, query, downtime))
}
func TargetGets(ctx *ctx.Context, bgids []int64, dsIds []int64, query string, downtime int64, limit, offset int) ([]*Target, error) {
func TargetGets(ctx *ctx.Context, limit, offset int, order string, desc bool, options ...BuildTargetWhereOption) ([]*Target, error) {
var lst []*Target
err := buildTargetWhere(ctx, bgids, dsIds, query, downtime).Order("ident").Limit(limit).Offset(offset).Find(&lst).Error
if desc {
order += " desc"
} else {
order += " asc"
}
err := buildTargetWhere(ctx, options...).Order(order).Limit(limit).Offset(offset).Find(&lst).Error
if err == nil {
for i := 0; i < len(lst); i++ {
lst[i].TagsJSON = strings.Fields(lst[i].Tags)

View File

@@ -30,26 +30,32 @@ const (
Telegram = "telegram"
Email = "email"
EmailSubject = "mailsubject"
Lark = "lark"
LarkCard = "larkcard"
DingtalkKey = "dingtalk_robot_token"
WecomKey = "wecom_robot_token"
FeishuKey = "feishu_robot_token"
MmKey = "mm_webhook_url"
TelegramKey = "telegram_robot_token"
LarkKey = "lark_robot_token"
DingtalkDomain = "oapi.dingtalk.com"
WecomDomain = "qyapi.weixin.qq.com"
FeishuDomain = "open.feishu.cn"
LarkDomain = "open.larksuite.com"
// FeishuCardDomain The domain name of the feishu card is the same as the feishu,distinguished by the parameter
FeishuCardDomain = "open.feishu.cn?card=1"
LarkCardDomain = "open.larksuite.com?card=1"
TelegramDomain = "api.telegram.org"
IbexDomain = "ibex"
DefaultDomain = "default"
)
var (
DefaultChannels = []string{Dingtalk, Wecom, Feishu, Mm, Telegram, Email, FeishuCard}
DefaultChannels = []string{Dingtalk, Wecom, Feishu, Mm, Telegram, Email, FeishuCard, Lark, LarkCard}
DefaultContacts = []string{DingtalkKey, WecomKey, FeishuKey, MmKey, TelegramKey, LarkKey}
)
type User struct {
@@ -825,6 +831,9 @@ func (u *User) ExtractToken(key string) (string, bool) {
return ret.String(), ret.Exists()
case Email:
return u.Email, u.Email != ""
case Lark, LarkCard:
ret := gjson.GetBytes(bs, LarkKey)
return ret.String(), ret.Exists()
default:
return "", false
}

View File

@@ -49,7 +49,8 @@ var I18N = `
"url path invalid":"url非法",
"no such server":"无此实例",
"admin role can not be modified":"管理员角色不允许修改",
"builtin payload already exists":"内置模板已存在"
"builtin payload already exists":"内置模板已存在",
"This functionality has not been enabled. Please contact the system administrator to activate it.":"此功能尚未启用。请联系系统管理员启用"
},
"zh_CN": {
"Username or password invalid": "用户名或密码错误",
@@ -101,7 +102,8 @@ var I18N = `
"admin role can not be modified":"管理员角色不允许修改",
"builtin payload already exists":"内置模板已存在",
"builtin metric already exists":"内置指标已存在",
"AlertRule already exists":"告警规则已存在"
"AlertRule already exists":"告警规则已存在",
"This functionality has not been enabled. Please contact the system administrator to activate it.":"此功能尚未启用。请联系系统管理员启用"
}
}
`

View File

@@ -52,14 +52,16 @@ type WriterOptions struct {
}
type RelabelConfig struct {
SourceLabels model.LabelNames
Separator string
Regex string
SourceLabels model.LabelNames `json:"source_labels"`
Separator string `json:"separator"`
Regex string `json:"regex"`
RegexCompiled *regexp.Regexp
Modulus uint64
TargetLabel string
Replacement string
Action string
If string `json:"if"`
IfRegex *regexp.Regexp
Modulus uint64 `json:"modulus"`
TargetLabel string `json:"target_label"`
Replacement string `json:"replacement"`
Action string `json:"action"`
}
func (p *Pushgw) PreCheck() {

View File

@@ -136,7 +136,7 @@ func matchSample(filterMap, sampleMap map[string]string) bool {
}
func (rt *Router) ForwardByIdent(clientIP string, ident string, v *prompb.TimeSeries) {
rt.BeforePush(clientIP, v)
v = rt.BeforePush(clientIP, v)
if v == nil {
return
}
@@ -157,7 +157,7 @@ func (rt *Router) ForwardByIdent(clientIP string, ident string, v *prompb.TimeSe
}
func (rt *Router) ForwardByMetric(clientIP string, metric string, v *prompb.TimeSeries) {
rt.BeforePush(clientIP, v)
v = rt.BeforePush(clientIP, v)
if v == nil {
return
}
@@ -177,7 +177,7 @@ func (rt *Router) ForwardByMetric(clientIP string, metric string, v *prompb.Time
rt.Writers.PushSample(hashkey, *v)
}
func (rt *Router) BeforePush(clientIP string, v *prompb.TimeSeries) {
rt.HandleTS(v)
func (rt *Router) BeforePush(clientIP string, v *prompb.TimeSeries) *prompb.TimeSeries {
rt.debugSample(clientIP, v)
return rt.HandleTS(v)
}

View File

@@ -14,7 +14,7 @@ import (
"github.com/ccfos/nightingale/v6/pushgw/writer"
)
type HandleTSFunc func(pt *prompb.TimeSeries)
type HandleTSFunc func(pt *prompb.TimeSeries) *prompb.TimeSeries
type Router struct {
HTTP httpx.Config
@@ -27,6 +27,7 @@ type Router struct {
Writers *writer.WritersType
Ctx *ctx.Context
HandleTS HandleTSFunc
HeartbeartApi string
}
func New(httpConfig httpx.Config, pushgw pconf.Pushgw, aconf aconf.Alert, tc *memsto.TargetCacheType, bg *memsto.BusiGroupCacheType,
@@ -42,7 +43,7 @@ func New(httpConfig httpx.Config, pushgw pconf.Pushgw, aconf aconf.Alert, tc *me
BusiGroupCache: bg,
IdentSet: idents,
MetaSet: metas,
HandleTS: func(pt *prompb.TimeSeries) {},
HandleTS: func(pt *prompb.TimeSeries) *prompb.TimeSeries { return pt },
}
}

View File

@@ -6,14 +6,32 @@ import (
"io"
"time"
"github.com/ccfos/nightingale/v6/center/metas"
"github.com/ccfos/nightingale/v6/models"
"github.com/ccfos/nightingale/v6/pkg/poster"
"github.com/gin-gonic/gin"
"github.com/toolkits/pkg/ginx"
"github.com/toolkits/pkg/logger"
)
// heartbeat Forward heartbeat request to the center.
func (rt *Router) heartbeat(c *gin.Context) {
gid := ginx.QueryStr(c, "gid", "")
req, err := HandleHeartbeat(c, rt.Aconf.Heartbeat.EngineName, rt.MetaSet)
if err != nil {
logger.Warningf("req:%v heartbeat failed to handle heartbeat err:%v", req, err)
ginx.Dangerous(err)
}
api := "/v1/n9e/heartbeat"
if rt.HeartbeartApi != "" {
api = rt.HeartbeartApi
}
ret, err := poster.PostByUrlsWithResp[map[string]interface{}](rt.Ctx, api+"?gid="+gid, req)
ginx.NewRender(c).Data(ret, err)
}
func HandleHeartbeat(c *gin.Context, engineName string, metaSet *metas.Set) (models.HostMeta, error) {
var bs []byte
var err error
var r *gzip.Reader
@@ -21,20 +39,26 @@ func (rt *Router) heartbeat(c *gin.Context) {
if c.GetHeader("Content-Encoding") == "gzip" {
r, err = gzip.NewReader(c.Request.Body)
if err != nil {
c.String(400, err.Error())
return
return req, err
}
defer r.Close()
bs, err = io.ReadAll(r)
ginx.Dangerous(err)
if err != nil {
return req, err
}
} else {
defer c.Request.Body.Close()
bs, err = io.ReadAll(c.Request.Body)
ginx.Dangerous(err)
if err != nil {
return req, err
}
}
err = json.Unmarshal(bs, &req)
ginx.Dangerous(err)
if err != nil {
return req, err
}
if req.Hostname == "" {
ginx.Dangerous("hostname is required", 400)
@@ -42,11 +66,8 @@ func (rt *Router) heartbeat(c *gin.Context) {
req.Offset = (time.Now().UnixMilli() - req.UnixTime)
req.RemoteAddr = c.ClientIP()
gid := ginx.QueryStr(c, "gid", "")
req.EngineName = engineName
metaSet.Set(req.Hostname, req)
req.EngineName = rt.Aconf.Heartbeat.EngineName
rt.MetaSet.Set(req.Hostname, req)
ginx.NewRender(c).Message(poster.PostByUrls(rt.Ctx, "/v1/n9e/heartbeat?gid="+gid, req))
return req, nil
}

View File

@@ -3,24 +3,28 @@ package writer
import (
"crypto/md5"
"fmt"
"regexp"
"sort"
"strings"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/toolkits/pkg/logger"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/prompb"
)
const (
Replace string = "replace"
Keep string = "keep"
Drop string = "drop"
HashMod string = "hashmod"
LabelMap string = "labelmap"
LabelDrop string = "labeldrop"
LabelKeep string = "labelkeep"
Lowercase string = "lowercase"
Uppercase string = "uppercase"
Replace string = "replace"
Keep string = "keep"
Drop string = "drop"
HashMod string = "hashmod"
LabelMap string = "labelmap"
LabelDrop string = "labeldrop"
LabelKeep string = "labelkeep"
Lowercase string = "lowercase"
Uppercase string = "uppercase"
DropIfEqual string = "drop_if_equal"
)
func Process(labels []prompb.Label, cfgs ...*pconf.RelabelConfig) []prompb.Label {
@@ -55,10 +59,6 @@ func newBuilder(ls []prompb.Label) *LabelBuilder {
}
func (l *LabelBuilder) set(k, v string) *LabelBuilder {
if v == "" {
return l.del(k)
}
l.LabelSet[k] = v
return l
}
@@ -96,9 +96,17 @@ func relabel(lset []prompb.Label, cfg *pconf.RelabelConfig) []prompb.Label {
}
regx := cfg.RegexCompiled
if regx == nil {
regx = compileRegex(cfg.Regex)
}
if regx == nil {
return lset
}
val := strings.Join(values, cfg.Separator)
lb := newBuilder(lset)
switch cfg.Action {
case Drop:
if regx.MatchString(val) {
@@ -109,21 +117,7 @@ func relabel(lset []prompb.Label, cfg *pconf.RelabelConfig) []prompb.Label {
return nil
}
case Replace:
indexes := regx.FindStringSubmatchIndex(val)
if indexes == nil {
break
}
target := model.LabelName(regx.ExpandString([]byte{}, cfg.TargetLabel, val, indexes))
if !target.IsValid() {
lb.del(cfg.TargetLabel)
break
}
res := regx.ExpandString([]byte{}, cfg.Replacement, val, indexes)
if len(res) == 0 {
lb.del(cfg.TargetLabel)
break
}
lb.set(string(target), string(res))
return handleReplace(lb, regx, cfg, val, lset)
case Lowercase:
lb.set(cfg.TargetLabel, strings.ToLower(val))
case Uppercase:
@@ -150,13 +144,84 @@ func relabel(lset []prompb.Label, cfg *pconf.RelabelConfig) []prompb.Label {
lb.del(l.Name)
}
}
case DropIfEqual:
return handleDropIfEqual(lb, cfg, lset)
default:
panic(fmt.Errorf("relabel: unknown relabel action type %q", cfg.Action))
logger.Errorf("relabel: unknown relabel action type %q", cfg.Action)
}
return lb.labels()
}
func handleReplace(lb *LabelBuilder, regx *regexp.Regexp, cfg *pconf.RelabelConfig, val string, lset []prompb.Label) []prompb.Label {
// 如果没有 source_labels直接设置标签新增标签
if len(cfg.SourceLabels) == 0 {
lb.set(cfg.TargetLabel, cfg.Replacement)
return lb.labels()
}
// 如果 Replacement 为空, separator 不为空, 则用已有标签构建新标签
if cfg.Replacement == "" && len(cfg.SourceLabels) > 1 {
lb.set(cfg.TargetLabel, val)
return lb.labels()
}
// 处理正则表达式替换的情况(修改标签值,正则)
if regx != nil {
indexes := regx.FindStringSubmatchIndex(val)
if indexes == nil {
return lb.labels()
}
target := model.LabelName(cfg.TargetLabel)
if !target.IsValid() {
lb.del(cfg.TargetLabel)
return lb.labels()
}
res := regx.ExpandString([]byte{}, cfg.Replacement, val, indexes)
if len(res) == 0 {
lb.del(cfg.TargetLabel)
} else {
lb.set(string(target), string(res))
}
return lb.labels()
}
// 默认情况,直接设置目标标签值
lb.set(cfg.TargetLabel, cfg.Replacement)
return lb.labels()
}
func handleDropIfEqual(lb *LabelBuilder, cfg *pconf.RelabelConfig, lset []prompb.Label) []prompb.Label {
if len(cfg.SourceLabels) < 2 {
return lb.labels()
}
firstVal := getValue(lset, cfg.SourceLabels[0])
equal := true
for _, label := range cfg.SourceLabels[1:] {
if getValue(lset, label) != firstVal {
equal = false
break
}
}
if equal {
return nil
}
return lb.labels()
}
func compileRegex(expr string) *regexp.Regexp {
regex, err := regexp.Compile(expr)
if err != nil {
logger.Error("failed to compile regexp:", expr, "error:", err)
return nil
}
return regex
}
func sum64(hash [md5.Size]byte) uint64 {
var s uint64

View File

@@ -0,0 +1,406 @@
// @Author: Ciusyan 6/19/24
package writer
import (
"reflect"
"sort"
"testing"
"github.com/ccfos/nightingale/v6/pushgw/pconf"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/prompb"
)
func TestProcess(t *testing.T) {
tests := []struct {
name string
labels []prompb.Label
cfgs []*pconf.RelabelConfig
expected []prompb.Label
}{
// 1. 添加新标签 (Adding new label)
{
name: "Adding new label",
labels: []prompb.Label{{Name: "job", Value: "aa"}},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
TargetLabel: "foo",
Replacement: "bar",
},
},
expected: []prompb.Label{{Name: "job", Value: "aa"}, {Name: "foo", Value: "bar"}},
},
// 2. 更新现有标签 (Updating existing label)
{
name: "Updating existing label",
labels: []prompb.Label{{Name: "foo", Value: "aaaa"}},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
TargetLabel: "foo",
Replacement: "bar",
},
},
expected: []prompb.Label{{Name: "foo", Value: "bar"}},
},
// 3. 重写现有标签 (Rewriting existing label)
{
name: "Rewriting existing label",
labels: []prompb.Label{{Name: "instance", Value: "bar:123"}},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
SourceLabels: model.LabelNames{"instance"},
Regex: "([^:]+):.+",
TargetLabel: "instance",
Replacement: "$1",
},
},
expected: []prompb.Label{{Name: "instance", Value: "bar"}},
},
{
name: "Rewriting existing label",
labels: []prompb.Label{{Name: "instance", Value: "bar:123"}},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
SourceLabels: model.LabelNames{"instance"},
Regex: ":([0-9]+)$",
TargetLabel: "port",
Replacement: "$1",
},
},
expected: []prompb.Label{{Name: "port", Value: "123"}, {Name: "instance", Value: "bar:123"}},
},
// 4. 更新度量标准名称 (Updating metric name)
{
name: "Updating metric name",
labels: []prompb.Label{{Name: "__name__", Value: "foo_suffix"}},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
SourceLabels: model.LabelNames{"__name__"},
Regex: "(.+)_suffix",
TargetLabel: "__name__",
Replacement: "prefix_$1",
},
},
expected: []prompb.Label{{Name: "__name__", Value: "prefix_foo"}},
},
// 5. 删除不需要/保持需要 的标签 (Removing unneeded labels)
{
name: "Removing unneeded labels",
labels: []prompb.Label{
{Name: "job", Value: "a"},
{Name: "instance", Value: "xyz"},
{Name: "foobar", Value: "baz"},
{Name: "foox", Value: "aaa"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "labeldrop",
Regex: "foo.+",
},
},
expected: []prompb.Label{
{Name: "job", Value: "a"},
{Name: "instance", Value: "xyz"},
},
},
{
name: "keep needed labels",
labels: []prompb.Label{
{Name: "job", Value: "a"},
{Name: "instance", Value: "xyz"},
{Name: "foobar", Value: "baz"},
{Name: "foox", Value: "aaa"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "labelkeep",
Regex: "foo.+",
},
},
expected: []prompb.Label{
{Name: "foobar", Value: "baz"},
{Name: "foox", Value: "aaa"},
},
},
// 6. 删除特定标签值 (Removing the specific label value)
{
name: "Removing the specific label value",
labels: []prompb.Label{
{Name: "foo", Value: "bar"},
{Name: "baz", Value: "x"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
SourceLabels: model.LabelNames{"foo"},
Regex: "bar",
TargetLabel: "foo",
Replacement: "",
},
},
expected: []prompb.Label{
{Name: "baz", Value: "x"},
},
},
// 7. 删除不需要的度量标准 (Removing unneeded metrics)
{
name: "Removing unneeded metrics",
labels: []prompb.Label{
{Name: "instance", Value: "foobar1"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "drop",
SourceLabels: model.LabelNames{"instance"},
Regex: "foobar.+",
},
},
expected: nil,
},
{
name: "Removing unneeded metrics 2",
labels: []prompb.Label{
{Name: "instance", Value: "foobar2"},
{Name: "job", Value: "xxx"},
{Name: "aaa", Value: "bb"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "drop",
SourceLabels: model.LabelNames{"instance"},
Regex: "foobar.+",
},
},
expected: nil,
},
{
name: "Removing unneeded metrics 3",
labels: []prompb.Label{
{Name: "instance", Value: "xxx"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "drop",
SourceLabels: model.LabelNames{"instance"},
Regex: "foobar.+",
},
},
expected: []prompb.Label{
{Name: "instance", Value: "xxx"},
},
},
{
name: "Removing unneeded metrics 4",
labels: []prompb.Label{
{Name: "instance", Value: "abc"},
{Name: "job", Value: "xyz"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "drop",
SourceLabels: model.LabelNames{"instance"},
Regex: "foobar.+",
},
},
expected: []prompb.Label{
{Name: "instance", Value: "abc"},
{Name: "job", Value: "xyz"},
},
},
{
name: "Removing unneeded metrics with multiple labels",
labels: []prompb.Label{
{Name: "job", Value: "foo"},
{Name: "instance", Value: "bar"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "drop",
SourceLabels: model.LabelNames{"job", "instance"},
Regex: "foo;bar",
Separator: ";",
},
},
expected: nil,
},
// 8. 按条件删除度量标准 (Dropping metrics on certain condition)
{
name: "Dropping metrics on certain condition",
labels: []prompb.Label{
{Name: "real_port", Value: "123"},
{Name: "needed_port", Value: "123"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "drop_if_equal",
SourceLabels: model.LabelNames{"real_port", "needed_port"},
},
},
expected: nil,
},
{
name: "Dropping metrics on certain condition 2",
labels: []prompb.Label{
{Name: "real_port", Value: "123"},
{Name: "needed_port", Value: "456"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "drop_if_equal",
SourceLabels: model.LabelNames{"real_port", "needed_port"},
},
},
expected: []prompb.Label{
{Name: "real_port", Value: "123"},
{Name: "needed_port", Value: "456"},
},
},
// 9. 修改标签名称 (Modifying label names)
{
name: "Modifying label names",
labels: []prompb.Label{
{Name: "foo_xx", Value: "bb"},
{Name: "job", Value: "qq"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "labelmap",
Regex: "foo_(.+)",
Replacement: "bar_$1",
},
},
expected: []prompb.Label{
{Name: "foo_xx", Value: "bb"},
{Name: "bar_xx", Value: "bb"},
{Name: "job", Value: "qq"},
},
},
// 10. 从多个现有标签构建新标签 (Constructing a label from multiple existing labels)
{
name: "Constructing a label from multiple existing labels",
labels: []prompb.Label{
{Name: "host", Value: "hostname"},
{Name: "port", Value: "9090"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
SourceLabels: model.LabelNames{"host", "port"},
Separator: ":",
TargetLabel: "address",
},
},
expected: []prompb.Label{
{Name: "host", Value: "hostname"},
{Name: "port", Value: "9090"},
{Name: "address", Value: "hostname:9090"},
},
},
// 11. 链式重标记规则 (Chaining relabeling rules)
{
name: "Chaining relabeling rules",
labels: []prompb.Label{
{Name: "instance", Value: "hostname:9090"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
TargetLabel: "foo",
Replacement: "bar",
},
{
Action: "replace",
SourceLabels: model.LabelNames{"instance"},
Regex: "([^:]+):.*",
TargetLabel: "instance",
Replacement: "$1",
},
},
expected: []prompb.Label{
{Name: "instance", Value: "hostname"},
{Name: "foo", Value: "bar"},
},
},
// 12. 条件重标记 (Conditional relabeling)
{
name: "Conditional relabeling matches",
labels: []prompb.Label{
{Name: "label", Value: "x"},
{Name: "foo", Value: "aaa"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
If: `label="x|y"`,
TargetLabel: "foo",
Replacement: "bar",
IfRegex: compileRegex(`label="x|y"`),
},
},
expected: []prompb.Label{
{Name: "label", Value: "x"},
{Name: "foo", Value: "bar"},
},
},
{
name: "Conditional relabeling matches alternative",
labels: []prompb.Label{
{Name: "label", Value: "y"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
If: `label="x|y"`,
TargetLabel: "foo",
Replacement: "bar",
IfRegex: compileRegex(`label="x|y"`),
},
},
expected: []prompb.Label{
{Name: "label", Value: "y"},
{Name: "foo", Value: "bar"},
},
},
{
name: "Conditional relabeling does not match",
labels: []prompb.Label{
{Name: "label", Value: "z"},
},
cfgs: []*pconf.RelabelConfig{
{
Action: "replace",
If: `label="x|y"`,
TargetLabel: "foo",
Replacement: "bar",
IfRegex: compileRegex(`label="x|y"`),
},
},
expected: []prompb.Label{
{Name: "label", Value: "z"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Process(tt.labels, tt.cfgs...)
// Sort the slices before comparison
sort.Slice(got, func(i, j int) bool {
return got[i].Name < got[j].Name
})
sort.Slice(tt.expected, func(i, j int) bool {
return tt.expected[i].Name < tt.expected[j].Name
})
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("Process() = %v, want %v", got, tt.expected)
}
})
}
}