Added multoresource
This commit is contained in:
parent
7b711c2317
commit
28b299fedd
4 changed files with 260 additions and 26 deletions
103
alerts.go
103
alerts.go
|
|
@ -20,6 +20,13 @@ type AlertRulesTemplate struct {
|
||||||
Defaults AlertDefaults `json:"defaults"`
|
Defaults AlertDefaults `json:"defaults"`
|
||||||
RPSAlert map[string]interface{} `json:"rps_alert"`
|
RPSAlert map[string]interface{} `json:"rps_alert"`
|
||||||
StaticAlerts []map[string]interface{} `json:"static_alerts"` // Алерты без привязки к БД
|
StaticAlerts []map[string]interface{} `json:"static_alerts"` // Алерты без привязки к БД
|
||||||
|
ContactPoint ContactPointConfig `json:"contact_point"` // Настройки шаблона уведомлений
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactPointConfig описывает кастомный шаблон сообщения для Contact Point
|
||||||
|
type ContactPointConfig struct {
|
||||||
|
ReceiverName string `json:"receiver_name"` // Имя contact point в Grafana
|
||||||
|
MessageTemplate string `json:"message_template"` // Go-шаблон сообщения Telegram
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertDefaults struct {
|
type AlertDefaults struct {
|
||||||
|
|
@ -86,7 +93,7 @@ func buildRPSAlertRule(client ClientData, tmpl AlertRulesTemplate, config Config
|
||||||
uid := generateAlertUID(client.ClientTitle, "rps")
|
uid := generateAlertUID(client.ClientTitle, "rps")
|
||||||
dataRefID := "A"
|
dataRefID := "A"
|
||||||
rpsRefID := "RPS " + client.ClientTitle
|
rpsRefID := "RPS " + client.ClientTitle
|
||||||
reduceRefID := "Текущий RPS"
|
reduceRefID := "Значение"
|
||||||
thresholdRefID := "Превышение"
|
thresholdRefID := "Превышение"
|
||||||
|
|
||||||
rule := map[string]interface{}{
|
rule := map[string]interface{}{
|
||||||
|
|
@ -364,6 +371,11 @@ func generateAndSendAlerts(clients map[string]ClientData, tmpl AlertRulesTemplat
|
||||||
log.Printf("Warning: static alerts error: %v", err)
|
log.Printf("Warning: static alerts error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем шаблон сообщения Contact Point
|
||||||
|
if err := upsertContactPoint(tmpl, config, dryRun); err != nil {
|
||||||
|
log.Printf("Warning: failed to update contact point message template: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,9 +413,9 @@ func buildErrorRateAlertRule(client ClientData, errCode, limitPct int, tmpl Aler
|
||||||
uid := generateAlertUID(client.ClientTitle, codeName)
|
uid := generateAlertUID(client.ClientTitle, codeName)
|
||||||
totalRefID := "Total"
|
totalRefID := "Total"
|
||||||
errRefID := "Errors"
|
errRefID := "Errors"
|
||||||
totalReduceID := "TotalReduce"
|
totalReduceID := "_TotalReduce"
|
||||||
errReduceID := "ErrorsReduce"
|
errReduceID := "_ErrorsReduce"
|
||||||
percentRefID := "ErrorPercent"
|
percentRefID := "Значение"
|
||||||
thresholdRefID := "Превышение"
|
thresholdRefID := "Превышение"
|
||||||
|
|
||||||
summary := fmt.Sprintf("🚨 %s — %s ошибок >%d%% трафика", client.ClientTitle, codeName, limitPct)
|
summary := fmt.Sprintf("🚨 %s — %s ошибок >%d%% трафика", client.ClientTitle, codeName, limitPct)
|
||||||
|
|
@ -737,6 +749,89 @@ func ensureAlertFolder(config Config) (string, error) {
|
||||||
return "", fmt.Errorf("failed to get folder UID")
|
return "", fmt.Errorf("failed to get folder UID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upsertContactPoint обновляет шаблон сообщения у существующего Contact Point.
|
||||||
|
// Не создаёт contact point — только обновляет message у уже настроенного в Grafana.
|
||||||
|
func upsertContactPoint(tmpl AlertRulesTemplate, config Config, dryRun bool) error {
|
||||||
|
if tmpl.ContactPoint.ReceiverName == "" || tmpl.ContactPoint.MessageTemplate == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverName := orDefault(config.AlertsReceiver, tmpl.ContactPoint.ReceiverName)
|
||||||
|
baseURL := strings.TrimRight(config.GrafanaURL, "/")
|
||||||
|
|
||||||
|
// Получить список contact points и найти нужный по имени
|
||||||
|
req, _ := http.NewRequest("GET", baseURL+"/api/v1/provisioning/contact-points", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+config.GrafanaAPIKey)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list contact points: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
var contactPoints []map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &contactPoints); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse contact points: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем contact point с нужным именем
|
||||||
|
var target map[string]interface{}
|
||||||
|
for _, cp := range contactPoints {
|
||||||
|
if name, _ := cp["name"].(string); name == receiverName {
|
||||||
|
target = cp
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if target == nil {
|
||||||
|
log.Printf("Contact point '%s' not found, skipping message template update", receiverName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, _ := target["uid"].(string)
|
||||||
|
|
||||||
|
// Contact point — одиночный объект с полем type и settings
|
||||||
|
if cpType, _ := target["type"].(string); cpType != "telegram" {
|
||||||
|
log.Printf("Contact point '%s' is not Telegram (type: %s), skipping", receiverName, cpType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, ok := target["settings"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
settings = make(map[string]interface{})
|
||||||
|
target["settings"] = settings
|
||||||
|
}
|
||||||
|
settings["message"] = tmpl.ContactPoint.MessageTemplate
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
data, _ := json.MarshalIndent(target, "", " ")
|
||||||
|
log.Printf("DRY RUN: Contact point update for '%s':\n%s", receiverName, string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/v1/provisioning/contact-points/{uid}
|
||||||
|
payload, _ := json.Marshal(target)
|
||||||
|
updReq, _ := http.NewRequest("PUT", baseURL+"/api/v1/provisioning/contact-points/"+uid, bytes.NewBuffer(payload))
|
||||||
|
updReq.Header.Set("Authorization", "Bearer "+config.GrafanaAPIKey)
|
||||||
|
updReq.Header.Set("Content-Type", "application/json")
|
||||||
|
updReq.Header.Set("X-Disable-Provenance", "true")
|
||||||
|
|
||||||
|
updResp, err := http.DefaultClient.Do(updReq)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update contact point: %w", err)
|
||||||
|
}
|
||||||
|
defer updResp.Body.Close()
|
||||||
|
|
||||||
|
respBody, _ := io.ReadAll(updResp.Body)
|
||||||
|
if updResp.StatusCode < 200 || updResp.StatusCode >= 300 {
|
||||||
|
return fmt.Errorf("contact point update failed: status %d: %s", updResp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Contact point '%s' message template updated", receiverName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// orDefault возвращает val если не пустой, иначе def
|
// orDefault возвращает val если не пустой, иначе def
|
||||||
func orDefault(val, def string) string {
|
func orDefault(val, def string) string {
|
||||||
if val != "" {
|
if val != "" {
|
||||||
|
|
|
||||||
13
config.go
13
config.go
|
|
@ -130,11 +130,14 @@ type DashboardTemplate struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayoutConfig struct {
|
type LayoutConfig struct {
|
||||||
TenantPanels []PanelLayout `json:"tenant_panels"`
|
TenantPanels []PanelLayout `json:"tenant_panels"`
|
||||||
DomainPanels []PanelLayout `json:"domain_panels"`
|
PreDomainPanels []PanelLayout `json:"pre_domain_panels"` // Панели один раз перед всеми доменными
|
||||||
UseCollapsedRows bool `json:"use_collapsed_rows"`
|
DomainPanels []PanelLayout `json:"domain_panels"`
|
||||||
OverviewPanels []PanelLayout `json:"overview_panels"` // Панели обзора перед клиентскими строками
|
InterDomainPanels []PanelLayout `json:"inter_domain_panels"` // Панели один раз между группами доменных
|
||||||
OverviewRowTitle string `json:"overview_row_title"` // Заголовок collapsed row обзора
|
TrafficDomainPanels []PanelLayout `json:"traffic_domain_panels"` // Панели трафика per-domain после inter_domain
|
||||||
|
UseCollapsedRows bool `json:"use_collapsed_rows"`
|
||||||
|
OverviewPanels []PanelLayout `json:"overview_panels"` // Панели обзора перед клиентскими строками
|
||||||
|
OverviewRowTitle string `json:"overview_row_title"` // Заголовок collapsed row обзора
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanelLayout struct {
|
type PanelLayout struct {
|
||||||
|
|
|
||||||
146
dashboard.go
146
dashboard.go
|
|
@ -17,6 +17,14 @@ func generateSingleDashboard(clients map[string]ClientData, template *DashboardT
|
||||||
datasourceUID := template.Datasource["uid"].(string)
|
datasourceUID := template.Datasource["uid"].(string)
|
||||||
useCollapsedRows := template.Layout.UseCollapsedRows
|
useCollapsedRows := template.Layout.UseCollapsedRows
|
||||||
|
|
||||||
|
// Генерация overview row (глобальные панели перед клиентскими строками)
|
||||||
|
if len(template.Layout.OverviewPanels) > 0 {
|
||||||
|
overview := createOverviewRow(template, datasourceType, datasourceUID, panelID, yPos)
|
||||||
|
panels = append(panels, overview)
|
||||||
|
panelID += 50
|
||||||
|
yPos++
|
||||||
|
}
|
||||||
|
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if len(client.Domains) == 0 {
|
if len(client.Domains) == 0 {
|
||||||
continue
|
continue
|
||||||
|
|
@ -49,6 +57,24 @@ func generateSingleDashboard(clients map[string]ClientData, template *DashboardT
|
||||||
panelID = nextID
|
panelID = nextID
|
||||||
yPos = nextY
|
yPos = nextY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interPanels, nextID, nextY := generateLayoutPanels(
|
||||||
|
template.Layout.InterDomainPanels, client, nil,
|
||||||
|
template, datasourceType, datasourceUID, panelID, yPos,
|
||||||
|
)
|
||||||
|
panels = append(panels, interPanels...)
|
||||||
|
panelID = nextID
|
||||||
|
yPos = nextY
|
||||||
|
|
||||||
|
for _, domain := range client.Domains {
|
||||||
|
trafficPanels, nextID, nextY := generateLayoutPanels(
|
||||||
|
template.Layout.TrafficDomainPanels, client, &domain,
|
||||||
|
template, datasourceType, datasourceUID, panelID, yPos,
|
||||||
|
)
|
||||||
|
panels = append(panels, trafficPanels...)
|
||||||
|
panelID = nextID
|
||||||
|
yPos = nextY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,6 +113,11 @@ func applyDashboardMeta(dashboardBase map[string]interface{}, template *Dashboar
|
||||||
if version, ok := meta["version"]; ok {
|
if version, ok := meta["version"]; ok {
|
||||||
dashboardBase["version"] = version
|
dashboardBase["version"] = version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Переменные дашборда (templating) — переносим из шаблона если заданы
|
||||||
|
if template.Templating != nil {
|
||||||
|
dashboardBase["templating"] = deepCopy(template.Templating)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// createCollapsedRow создаёт row с панелями внутри (collapsed=true)
|
// createCollapsedRow создаёт row с панелями внутри (collapsed=true)
|
||||||
|
|
@ -106,42 +137,84 @@ func createCollapsedRow(
|
||||||
|
|
||||||
insidePanels := []interface{}{}
|
insidePanels := []interface{}{}
|
||||||
panelID := startPanelID + 1
|
panelID := startPanelID + 1
|
||||||
|
currentY := 1 // последовательный y для правильного порядка внутри collapsed row
|
||||||
|
|
||||||
|
// Вспомогательная функция: добавить панель с текущим y и обновить currentY
|
||||||
|
addPanel := func(panel map[string]interface{}, h int) {
|
||||||
|
if gp, ok := panel["gridPos"].(map[string]interface{}); ok {
|
||||||
|
gp["y"] = currentY
|
||||||
|
}
|
||||||
|
insidePanels = append(insidePanels, panel)
|
||||||
|
panelID++
|
||||||
|
}
|
||||||
|
nextRow := func(h int) { currentY += h }
|
||||||
|
|
||||||
|
// Tenant panels (RPS, Status Codes)
|
||||||
for _, layout := range template.Layout.TenantPanels {
|
for _, layout := range template.Layout.TenantPanels {
|
||||||
if !evaluateCondition(layout.Condition, client, nil) {
|
if !evaluateCondition(layout.Condition, client, nil) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domain := tenantDomain(client)
|
domain := tenantDomain(client)
|
||||||
panel := buildPanel(
|
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||||||
layout.PanelKey, template, domain, client,
|
datasourceType, datasourceUID, panelID, currentY, layout.XOffset, layout.Width)
|
||||||
datasourceType, datasourceUID,
|
|
||||||
panelID, yOffsetOrDefault(layout.YOffset, 1), layout.XOffset, layout.Width,
|
|
||||||
)
|
|
||||||
if panel == nil {
|
if panel == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
applyTitle(panel, layout, client.ClientTitle, "", "")
|
applyTitle(panel, layout, client.ClientTitle, "", "")
|
||||||
insidePanels = append(insidePanels, panel)
|
addPanel(panel, 8)
|
||||||
panelID++
|
|
||||||
}
|
}
|
||||||
|
nextRow(8)
|
||||||
|
|
||||||
|
// Domain panels — status/response per domain
|
||||||
for _, domain := range client.Domains {
|
for _, domain := range client.Domains {
|
||||||
|
rowStart := currentY
|
||||||
for _, layout := range template.Layout.DomainPanels {
|
for _, layout := range template.Layout.DomainPanels {
|
||||||
if !evaluateCondition(layout.Condition, client, &domain) {
|
if !evaluateCondition(layout.Condition, client, &domain) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
panel := buildPanel(
|
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||||||
layout.PanelKey, template, domain, client,
|
datasourceType, datasourceUID, panelID, rowStart, layout.XOffset, layout.Width)
|
||||||
datasourceType, datasourceUID,
|
|
||||||
panelID, layout.YOffset, layout.XOffset, layout.Width,
|
|
||||||
)
|
|
||||||
if panel == nil {
|
if panel == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
applyTitle(panel, layout, client.ClientTitle, domain.DomainName, domain.SID)
|
applyTitle(panel, layout, client.ClientTitle, domain.DomainName, domain.SID)
|
||||||
panel["description"] = domain.Aliases
|
panel["description"] = domain.Aliases
|
||||||
insidePanels = append(insidePanels, panel)
|
addPanel(panel, 8)
|
||||||
panelID++
|
}
|
||||||
|
currentY = rowStart + 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inter-domain панели — Total Traffic (один раз)
|
||||||
|
for _, layout := range template.Layout.InterDomainPanels {
|
||||||
|
if !evaluateCondition(layout.Condition, client, nil) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
domain := tenantDomain(client)
|
||||||
|
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||||||
|
datasourceType, datasourceUID, panelID, currentY, layout.XOffset, layout.Width)
|
||||||
|
if panel == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
applyTitle(panel, layout, client.ClientTitle, "", "")
|
||||||
|
addPanel(panel, 8)
|
||||||
|
currentY += 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traffic domain panels — трафик per domain
|
||||||
|
for _, domain := range client.Domains {
|
||||||
|
for _, layout := range template.Layout.TrafficDomainPanels {
|
||||||
|
if !evaluateCondition(layout.Condition, client, &domain) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||||||
|
datasourceType, datasourceUID, panelID, currentY, layout.XOffset, layout.Width)
|
||||||
|
if panel == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
applyTitle(panel, layout, client.ClientTitle, domain.DomainName, domain.SID)
|
||||||
|
panel["description"] = domain.Aliases
|
||||||
|
addPanel(panel, 8)
|
||||||
|
currentY += 8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,6 +307,51 @@ func buildTenantSIDQuery(domains []DomainInfo) string {
|
||||||
return strings.Join(sids, " OR ")
|
return strings.Join(sids, " OR ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createOverviewRow создаёт collapsed row с глобальными обзорными панелями.
|
||||||
|
// Панели типа "static" — копируются из шаблона как есть, без подстановки SID из БД.
|
||||||
|
func createOverviewRow(
|
||||||
|
template *DashboardTemplate,
|
||||||
|
datasourceType, datasourceUID string,
|
||||||
|
startPanelID, yPos int,
|
||||||
|
) map[string]interface{} {
|
||||||
|
title := template.Layout.OverviewRowTitle
|
||||||
|
if title == "" {
|
||||||
|
title = "Обзор"
|
||||||
|
}
|
||||||
|
|
||||||
|
row := deepCopy(template.Panels["row"]).(map[string]interface{})
|
||||||
|
row["id"] = startPanelID
|
||||||
|
row["title"] = title
|
||||||
|
row["collapsed"] = true
|
||||||
|
row["gridPos"].(map[string]interface{})["y"] = yPos
|
||||||
|
|
||||||
|
insidePanels := []interface{}{}
|
||||||
|
panelID := startPanelID + 1
|
||||||
|
|
||||||
|
// Пустой клиент и домен — static-панели их не используют
|
||||||
|
emptyClient := ClientData{}
|
||||||
|
emptyDomain := DomainInfo{}
|
||||||
|
|
||||||
|
for _, layout := range template.Layout.OverviewPanels {
|
||||||
|
panel := buildPanel(
|
||||||
|
layout.PanelKey, template, emptyDomain, emptyClient,
|
||||||
|
datasourceType, datasourceUID,
|
||||||
|
panelID, layout.YOffset, layout.XOffset, layout.Width,
|
||||||
|
)
|
||||||
|
if panel == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if layout.TitleFormat != "" || layout.TitleSuffix != "" {
|
||||||
|
applyTitle(panel, layout, "", "", "")
|
||||||
|
}
|
||||||
|
insidePanels = append(insidePanels, panel)
|
||||||
|
panelID++
|
||||||
|
}
|
||||||
|
|
||||||
|
row["panels"] = insidePanels
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
func createRowPanel(title string, template *DashboardTemplate, id, yPos int) map[string]interface{} {
|
func createRowPanel(title string, template *DashboardTemplate, id, yPos int) map[string]interface{} {
|
||||||
row := deepCopy(template.Panels["row"]).(map[string]interface{})
|
row := deepCopy(template.Panels["row"]).(map[string]interface{})
|
||||||
row["id"] = id
|
row["id"] = id
|
||||||
|
|
|
||||||
24
panels.go
24
panels.go
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -226,7 +227,11 @@ func makeDataTarget(alias, query, refID string, metric map[string]interface{}, h
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeMathTarget(expression, refID string) map[string]interface{} {
|
func makeMathTarget(expression, refID string) map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return makeMathTargetWithAlias(expression, refID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMathTargetWithAlias(expression, refID, alias string) map[string]interface{} {
|
||||||
|
t := map[string]interface{}{
|
||||||
"datasource": map[string]interface{}{
|
"datasource": map[string]interface{}{
|
||||||
"name": "Expression",
|
"name": "Expression",
|
||||||
"type": "__expr__",
|
"type": "__expr__",
|
||||||
|
|
@ -237,6 +242,10 @@ func makeMathTarget(expression, refID string) map[string]interface{} {
|
||||||
"refId": refID,
|
"refId": refID,
|
||||||
"type": "math",
|
"type": "math",
|
||||||
}
|
}
|
||||||
|
if alias != "" {
|
||||||
|
t["legendFormat"] = alias
|
||||||
|
}
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func getString(m map[string]interface{}, key string) string {
|
func getString(m map[string]interface{}, key string) string {
|
||||||
|
|
@ -321,6 +330,10 @@ func buildMultiSumExpressionTargets(cfg map[string]interface{}, sidQuery, dataso
|
||||||
targetsRaw, _ := cfg["targets"].([]interface{})
|
targetsRaw, _ := cfg["targets"].([]interface{})
|
||||||
bucketAgg := getBucketAgg(template)
|
bucketAgg := getBucketAgg(template)
|
||||||
|
|
||||||
|
// Уникальный суффикс на основе sidQuery чтобы избежать конфликта refId на дашборде
|
||||||
|
h := md5.Sum([]byte(sidQuery))
|
||||||
|
uidSuffix := fmt.Sprintf("%x", h[:3])
|
||||||
|
|
||||||
targets := []interface{}{}
|
targets := []interface{}{}
|
||||||
for _, t := range targetsRaw {
|
for _, t := range targetsRaw {
|
||||||
tMap, ok := t.(map[string]interface{})
|
tMap, ok := t.(map[string]interface{})
|
||||||
|
|
@ -328,9 +341,14 @@ func buildMultiSumExpressionTargets(cfg map[string]interface{}, sidQuery, dataso
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sumMetric := map[string]interface{}{"field": getString(tMap, "field"), "id": "1", "type": "sum"}
|
sumMetric := map[string]interface{}{"field": getString(tMap, "field"), "id": "1", "type": "sum"}
|
||||||
|
alias := getString(tMap, "alias")
|
||||||
|
dataRefID := getString(tMap, "ref_id") + "_" + uidSuffix
|
||||||
|
exprRefID := getString(tMap, "expression_ref_id") + "_" + uidSuffix
|
||||||
|
// Подставляем оригинальный ref_id в expression и заменяем на уникальный
|
||||||
|
expr := strings.ReplaceAll(getString(tMap, "expression"), "$"+getString(tMap, "ref_id"), "$"+dataRefID)
|
||||||
targets = append(targets,
|
targets = append(targets,
|
||||||
makeDataTarget(getString(tMap, "alias"), sidQuery, getString(tMap, "ref_id"), sumMetric, true, datasourceType, datasourceUID, bucketAgg),
|
makeDataTarget(alias, sidQuery, dataRefID, sumMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||||||
makeMathTarget(getString(tMap, "expression"), getString(tMap, "expression_ref_id")),
|
makeMathTargetWithAlias(expr, exprRefID, alias),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return targets
|
return targets
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue