461 lines
17 KiB
Go
Executable file
461 lines
17 KiB
Go
Executable file
package main
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"fmt"
|
||
"log"
|
||
"strings"
|
||
)
|
||
|
||
// targetBuilder — тип функции-строителя targets для конкретного query_mode.
|
||
type targetBuilder func(cfg map[string]interface{}, sidQuery string, datasourceType, datasourceUID string, template *DashboardTemplate, domain DomainInfo, client ClientData) []interface{}
|
||
|
||
// queryModeRegistry — реестр строителей targets по query_mode.
|
||
// Чтобы добавить новый тип панели в Go: одна строка здесь + функция ниже.
|
||
// Всё остальное — только в JSON.
|
||
var queryModeRegistry = map[string]targetBuilder{
|
||
"rps": buildRPSTargets,
|
||
"rps_var": buildRPSVarTargets,
|
||
"rps_multi": buildRPSMultiTargets,
|
||
"multi_target": buildMultiTargets,
|
||
"range_percent": buildRangePercentTargets,
|
||
"multi_sum_expression": buildMultiSumExpressionTargets,
|
||
"sum_expression": buildSumExpressionTargets,
|
||
"bucket_logs": buildBucketLogsTargets,
|
||
"static": buildStaticTargets,
|
||
}
|
||
|
||
// conditionEvaluator — тип функции-вычислителя условия отображения панели.
|
||
type conditionEvaluator func(value interface{}, client ClientData, domain *DomainInfo) bool
|
||
|
||
// conditionRegistry — реестр вычислителей условий по типу.
|
||
// Чтобы добавить новый тип условия: одна строка здесь + лямбда.
|
||
var conditionRegistry = map[string]conditionEvaluator{
|
||
"min_domains": func(value interface{}, client ClientData, _ *DomainInfo) bool {
|
||
return len(client.Domains) >= int(value.(float64))
|
||
},
|
||
"max_domains": func(value interface{}, client ClientData, _ *DomainInfo) bool {
|
||
return len(client.Domains) <= int(value.(float64))
|
||
},
|
||
"client_name_matches": func(value interface{}, client ClientData, _ *DomainInfo) bool {
|
||
return strings.Contains(client.ClientTitle, value.(string))
|
||
},
|
||
}
|
||
|
||
// buildPanel — универсальная функция построения панели.
|
||
// Тип панели и логика запросов полностью определяются полем query_mode в JSON-шаблоне.
|
||
func buildPanel(
|
||
panelKey string,
|
||
template *DashboardTemplate,
|
||
domain DomainInfo,
|
||
client ClientData,
|
||
datasourceType, datasourceUID string,
|
||
id, yPos, xPos, width int,
|
||
) map[string]interface{} {
|
||
panelDef, ok := template.Panels[panelKey]
|
||
if !ok {
|
||
log.Printf("Warning: panel '%s' not found in template", panelKey)
|
||
return nil
|
||
}
|
||
|
||
panelMap, ok := panelDef.(map[string]interface{})
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
queryMode, _ := panelMap["query_mode"].(string)
|
||
if queryMode == "" {
|
||
log.Printf("Warning: panel '%s' has no query_mode", panelKey)
|
||
return nil
|
||
}
|
||
|
||
builder, ok := queryModeRegistry[queryMode]
|
||
if !ok {
|
||
log.Printf("Warning: unknown query_mode '%s' for panel '%s'. Available: %v", queryMode, panelKey, registeredModes())
|
||
return nil
|
||
}
|
||
|
||
queryConfigRaw, _ := panelMap["query_config"].(map[string]interface{})
|
||
panel := copyPanelBase(panelMap, id, yPos, xPos, width)
|
||
applyRPSThreshold(panel, panelKey, client.RPSLimit, client.RPSCommercialLimit)
|
||
sidQuery := buildSIDQuery(domain.SID)
|
||
|
||
panel["targets"] = builder(queryConfigRaw, sidQuery, datasourceType, datasourceUID, template, domain, client)
|
||
panel["datasource"] = map[string]interface{}{
|
||
"type": datasourceType,
|
||
"uid": datasourceUID,
|
||
}
|
||
|
||
return panel
|
||
}
|
||
|
||
// evaluateCondition проверяет условие отображения панели через реестр
|
||
// applyRPSThreshold подставляет rps_limit и rps_commercial_limit клиента в threshold панели rps.
|
||
// Красная линия — rps_limit (если 0, берётся из шаблона).
|
||
// Синяя линия — rps_commercial_limit (если 0, ставится 100).
|
||
func applyRPSThreshold(panel map[string]interface{}, panelKey string, rpsLimit, rpsCommercialLimit int) {
|
||
if panelKey != "rps" && panelKey != "rps_filtered" && panelKey != "rps_multi" {
|
||
return
|
||
}
|
||
// Ничего менять не нужно если оба лимита не заданы
|
||
if rpsLimit == 0 && rpsCommercialLimit == 0 {
|
||
return
|
||
}
|
||
|
||
fc, ok := panel["fieldConfig"].(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
defaults, ok := fc["defaults"].(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
thresholds, ok := defaults["thresholds"].(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
// Коммерческий лимит: если не задан — 100
|
||
commercial := rpsCommercialLimit
|
||
if commercial == 0 {
|
||
commercial = 100
|
||
}
|
||
|
||
steps := []interface{}{
|
||
map[string]interface{}{"color": "green", "value": nil},
|
||
map[string]interface{}{"color": "blue", "value": commercial},
|
||
}
|
||
if rpsLimit > 0 {
|
||
steps = append(steps, map[string]interface{}{"color": "dark-red", "value": rpsLimit})
|
||
}
|
||
thresholds["steps"] = steps
|
||
|
||
// Включаем отображение линий если ещё не задано
|
||
custom, ok := defaults["custom"].(map[string]interface{})
|
||
if ok {
|
||
if _, hasStyle := custom["thresholdsStyle"]; !hasStyle {
|
||
custom["thresholdsStyle"] = map[string]interface{}{"mode": "line"}
|
||
}
|
||
}
|
||
}
|
||
|
||
func evaluateCondition(condition *PanelCondition, client ClientData, domain *DomainInfo) bool {
|
||
if condition == nil {
|
||
return true
|
||
}
|
||
evaluator, ok := conditionRegistry[condition.Type]
|
||
if !ok {
|
||
log.Printf("Warning: unknown condition type '%s'. Available: %v", condition.Type, registeredConditions())
|
||
return true
|
||
}
|
||
return evaluator(condition.Value, client, domain)
|
||
}
|
||
|
||
func registeredModes() []string {
|
||
modes := make([]string, 0, len(queryModeRegistry))
|
||
for k := range queryModeRegistry {
|
||
modes = append(modes, k)
|
||
}
|
||
return modes
|
||
}
|
||
|
||
func registeredConditions() []string {
|
||
types := make([]string, 0, len(conditionRegistry))
|
||
for k := range conditionRegistry {
|
||
types = append(types, k)
|
||
}
|
||
return types
|
||
}
|
||
|
||
// copyPanelBase копирует визуальную часть панели, убирая служебные поля шаблона
|
||
func copyPanelBase(panelMap map[string]interface{}, id, yPos, xPos, width int) map[string]interface{} {
|
||
panel := deepCopy(panelMap).(map[string]interface{})
|
||
delete(panel, "query_mode")
|
||
delete(panel, "query_config")
|
||
|
||
panel["id"] = id
|
||
gridPos, ok := panel["gridPos"].(map[string]interface{})
|
||
if !ok {
|
||
gridPos = map[string]interface{}{}
|
||
panel["gridPos"] = gridPos
|
||
}
|
||
gridPos["y"] = yPos
|
||
gridPos["x"] = xPos
|
||
if width > 0 {
|
||
gridPos["w"] = width
|
||
}
|
||
return panel
|
||
}
|
||
|
||
func buildSIDQuery(sid string) string {
|
||
if strings.Contains(sid, " OR ") {
|
||
return fmt.Sprintf("SID:(%s)", sid)
|
||
}
|
||
return fmt.Sprintf("SID:%s", sid)
|
||
}
|
||
|
||
func replacePlaceholders(s, sid, clientTitle string) string {
|
||
s = strings.ReplaceAll(s, "{sid}", sid)
|
||
s = strings.ReplaceAll(s, "{sids}", sid)
|
||
s = strings.ReplaceAll(s, "{client_title}", clientTitle)
|
||
return s
|
||
}
|
||
|
||
func getBucketAgg(template *DashboardTemplate) map[string]interface{} {
|
||
return deepCopy(template.Queries["bucket_agg"]).(map[string]interface{})
|
||
}
|
||
|
||
func makeDataTarget(alias, query, refID string, metric map[string]interface{}, hidden bool, datasourceType, datasourceUID string, bucketAgg map[string]interface{}) map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"alias": alias,
|
||
"bucketAggs": []interface{}{
|
||
map[string]interface{}{
|
||
"field": bucketAgg["field"],
|
||
"id": "2",
|
||
"settings": bucketAgg["settings"],
|
||
"type": bucketAgg["type"],
|
||
},
|
||
},
|
||
"datasource": map[string]interface{}{"type": datasourceType, "uid": datasourceUID},
|
||
"format": "table",
|
||
"hide": hidden,
|
||
"luceneQueryType": "Metric",
|
||
"metrics": []interface{}{metric},
|
||
"query": query,
|
||
"queryType": "lucene",
|
||
"refId": refID,
|
||
"timeField": "@timestamp",
|
||
}
|
||
}
|
||
|
||
func makeMathTarget(expression, refID string) map[string]interface{} {
|
||
return makeMathTargetWithAlias(expression, refID, "")
|
||
}
|
||
|
||
func makeMathTargetWithAlias(expression, refID, alias string) map[string]interface{} {
|
||
t := map[string]interface{}{
|
||
"datasource": map[string]interface{}{
|
||
"name": "Expression",
|
||
"type": "__expr__",
|
||
"uid": "__expr__",
|
||
},
|
||
"expression": expression,
|
||
"hide": false,
|
||
"refId": refID,
|
||
"type": "math",
|
||
}
|
||
if alias != "" {
|
||
t["legendFormat"] = alias
|
||
}
|
||
return t
|
||
}
|
||
|
||
func getString(m map[string]interface{}, key string) string {
|
||
if v, ok := m[key].(string); ok {
|
||
return v
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// --- Строители targets ---
|
||
|
||
func buildRPSTargets(cfg map[string]interface{}, sidQuery, datasourceType, datasourceUID string, template *DashboardTemplate, _ DomainInfo, client ClientData) []interface{} {
|
||
query := replacePlaceholders(getString(cfg, "base_query"), sidQuery, client.ClientTitle)
|
||
alias := replacePlaceholders(getString(cfg, "alias"), sidQuery, client.ClientTitle)
|
||
bucketAgg := getBucketAgg(template)
|
||
countMetric := map[string]interface{}{"id": "1", "type": "count"}
|
||
|
||
return []interface{}{
|
||
makeDataTarget(alias, query, "A", countMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
makeMathTarget(getString(cfg, "expression"), getString(cfg, "expression_ref_id")),
|
||
}
|
||
}
|
||
|
||
// buildRPSVarTargets — RPS с фильтрацией через Grafana-переменную $rps_sid.
|
||
// base_query может содержать ${rps_sid:lucene} напрямую — Go не трогает эти плейсхолдеры.
|
||
func buildRPSVarTargets(cfg map[string]interface{}, sidQuery, datasourceType, datasourceUID string, template *DashboardTemplate, _ DomainInfo, client ClientData) []interface{} {
|
||
query := replacePlaceholders(getString(cfg, "base_query"), sidQuery, client.ClientTitle)
|
||
alias := replacePlaceholders(getString(cfg, "alias"), sidQuery, client.ClientTitle)
|
||
bucketAgg := getBucketAgg(template)
|
||
countMetric := map[string]interface{}{"id": "1", "type": "count"}
|
||
|
||
return []interface{}{
|
||
makeDataTarget(alias, query, "A", countMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
makeMathTarget(getString(cfg, "expression"), getString(cfg, "expression_ref_id")),
|
||
}
|
||
}
|
||
|
||
func buildMultiTargets(cfg map[string]interface{}, sidQuery, datasourceType, datasourceUID string, template *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
|
||
targetsRaw, _ := cfg["targets"].([]interface{})
|
||
bucketAgg := getBucketAgg(template)
|
||
countMetric := map[string]interface{}{"id": "1", "type": "count"}
|
||
|
||
targets := []interface{}{}
|
||
for _, t := range targetsRaw {
|
||
tMap, ok := t.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
query := sidQuery
|
||
if suffix := getString(tMap, "query_suffix"); suffix != "" {
|
||
query = sidQuery + " " + suffix
|
||
}
|
||
targets = append(targets, makeDataTarget(
|
||
getString(tMap, "alias"), query, getString(tMap, "ref_id"),
|
||
countMetric, false, datasourceType, datasourceUID, bucketAgg,
|
||
))
|
||
}
|
||
return targets
|
||
}
|
||
|
||
func buildRangePercentTargets(cfg map[string]interface{}, sidQuery, datasourceType, datasourceUID string, template *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
|
||
fieldFilter := getString(cfg, "field_filter")
|
||
totalRefID := getString(cfg, "total_ref_id")
|
||
rangeField := getString(cfg, "range_field")
|
||
rangesRaw, _ := cfg["ranges"].([]interface{})
|
||
bucketAgg := getBucketAgg(template)
|
||
countMetric := map[string]interface{}{"id": "1", "type": "count"}
|
||
|
||
totalQuery := sidQuery
|
||
if fieldFilter != "" {
|
||
totalQuery = sidQuery + " AND " + fieldFilter
|
||
}
|
||
|
||
targets := []interface{}{
|
||
makeDataTarget("Total", totalQuery, totalRefID, countMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
}
|
||
|
||
for _, r := range rangesRaw {
|
||
rMap, ok := r.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
label := getString(rMap, "label")
|
||
refID := getString(rMap, "ref_id")
|
||
query := fmt.Sprintf("%s AND %s:[%s TO %s]", sidQuery, rangeField, getString(rMap, "from"), getString(rMap, "to"))
|
||
|
||
targets = append(targets,
|
||
makeDataTarget(label, query, refID, countMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
makeMathTarget(fmt.Sprintf("($%s / $%s) * 100", refID, totalRefID), label),
|
||
)
|
||
}
|
||
return targets
|
||
}
|
||
|
||
func buildMultiSumExpressionTargets(cfg map[string]interface{}, sidQuery, datasourceType, datasourceUID string, template *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
|
||
targetsRaw, _ := cfg["targets"].([]interface{})
|
||
bucketAgg := getBucketAgg(template)
|
||
|
||
// Уникальный суффикс на основе sidQuery чтобы избежать конфликта refId на дашборде
|
||
h := md5.Sum([]byte(sidQuery))
|
||
uidSuffix := fmt.Sprintf("%x", h[:3])
|
||
|
||
targets := []interface{}{}
|
||
for _, t := range targetsRaw {
|
||
tMap, ok := t.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
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,
|
||
makeDataTarget(alias, sidQuery, dataRefID, sumMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
makeMathTargetWithAlias(expr, exprRefID, alias),
|
||
)
|
||
}
|
||
return targets
|
||
}
|
||
|
||
func buildSumExpressionTargets(cfg map[string]interface{}, sidQuery, datasourceType, datasourceUID string, template *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
|
||
bucketAgg := getBucketAgg(template)
|
||
sumMetric := map[string]interface{}{"field": getString(cfg, "field"), "id": "1", "type": "sum"}
|
||
|
||
return []interface{}{
|
||
makeDataTarget("", sidQuery, getString(cfg, "ref_id"), sumMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
makeMathTarget(getString(cfg, "expression"), getString(cfg, "expression_ref_id")),
|
||
}
|
||
}
|
||
|
||
// buildStaticTargets: панель без подстановки SID из БД.
|
||
// targets берутся из query_config.targets как есть — для глобальных/обзорных панелей.
|
||
// Поддерживает произвольные bucket_aggs из шаблона.
|
||
func buildStaticTargets(cfg map[string]interface{}, _ string, datasourceType, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
|
||
targetsRaw, ok := cfg["targets"].([]interface{})
|
||
if !ok || len(targetsRaw) == 0 {
|
||
return []interface{}{}
|
||
}
|
||
// Копируем targets как есть, только проставляем datasource
|
||
targets := []interface{}{}
|
||
for _, t := range targetsRaw {
|
||
target := deepCopy(t).(map[string]interface{})
|
||
if _, hasDatasource := target["datasource"]; !hasDatasource {
|
||
target["datasource"] = map[string]interface{}{
|
||
"type": datasourceType,
|
||
"uid": datasourceUID,
|
||
}
|
||
}
|
||
targets = append(targets, target)
|
||
}
|
||
return targets
|
||
}
|
||
|
||
func buildBucketLogsTargets(cfg map[string]interface{}, sidQuery, datasourceType, datasourceUID string, _ *DashboardTemplate, _ DomainInfo, _ ClientData) []interface{} {
|
||
query := strings.ReplaceAll(getString(cfg, "base_query"), "{sid}", sidQuery)
|
||
return []interface{}{
|
||
map[string]interface{}{
|
||
"bucketAggs": deepCopy(cfg["bucket_aggs"]),
|
||
"datasource": map[string]interface{}{"type": datasourceType, "uid": datasourceUID},
|
||
"format": "table",
|
||
"luceneQueryType": "Logs",
|
||
"metrics": []interface{}{map[string]interface{}{"id": "1", "type": "count"}},
|
||
"query": query,
|
||
"queryType": "lucene",
|
||
"refId": "A",
|
||
"timeField": "@timestamp",
|
||
},
|
||
}
|
||
}
|
||
|
||
// buildRPSMultiTargets — RPS с одной суммарной линией + по линии на каждый SID клиента.
|
||
func buildRPSMultiTargets(cfg map[string]interface{}, _ string, datasourceType, datasourceUID string, template *DashboardTemplate, _ DomainInfo, client ClientData) []interface{} {
|
||
bucketAgg := getBucketAgg(template)
|
||
countMetric := map[string]interface{}{"id": "1", "type": "count"}
|
||
expr := getString(cfg, "expression")
|
||
targets := []interface{}{}
|
||
|
||
// Суммарная линия по всем SID
|
||
allSIDs := buildTenantSIDQuery(client.Domains)
|
||
allQuery := fmt.Sprintf("SID:(%s)", allSIDs)
|
||
totalLabel := client.ClientTitle + " Total"
|
||
targets = append(targets,
|
||
makeDataTarget(totalLabel, allQuery, "A", countMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
makeMathTargetWithAlias(strings.ReplaceAll(expr, "$A", "$A"), "RPS_Total", totalLabel),
|
||
)
|
||
|
||
// Линия на каждый домен — refId используется как имя серии в Grafana
|
||
for i, domain := range client.Domains {
|
||
dataRefID := fmt.Sprintf("D%d", i)
|
||
// exprRefID = читаемое имя: "domain.name_SID" — Grafana показывает его в легенде
|
||
name := domain.SID
|
||
if domain.DomainName != "" {
|
||
// Заменяем пробелы и точки на _ для валидного refId
|
||
safeName := strings.NewReplacer(" ", "_", ".", "_").Replace(domain.DomainName)
|
||
name = fmt.Sprintf("%s_%s", safeName, domain.SID)
|
||
}
|
||
domainQuery := fmt.Sprintf("SID:%s", domain.SID)
|
||
domainExpr := strings.ReplaceAll(expr, "$A", "$"+dataRefID)
|
||
label := domain.SID
|
||
if domain.DomainName != "" {
|
||
label = fmt.Sprintf("%s %s", domain.DomainName, domain.SID)
|
||
}
|
||
targets = append(targets,
|
||
makeDataTarget(label, domainQuery, dataRefID, countMetric, true, datasourceType, datasourceUID, bucketAgg),
|
||
makeMathTargetWithAlias(domainExpr, name, label),
|
||
)
|
||
}
|
||
return targets
|
||
}
|