grafana_gen/panels.go
2026-02-24 12:45:35 +03:00

313 lines
11 KiB
Go

package main
import (
"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,
"multi_target": buildMultiTargets,
"range_percent": buildRangePercentTargets,
"multi_sum_expression": buildMultiSumExpressionTargets,
"sum_expression": buildSumExpressionTargets,
"bucket_logs": buildBucketLogsTargets,
}
// 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)
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 проверяет условие отображения панели через реестр
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 map[string]interface{}{
"datasource": map[string]interface{}{
"name": "Expression",
"type": "__expr__",
"uid": "__expr__",
},
"expression": expression,
"hide": false,
"refId": refID,
"type": "math",
}
}
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")),
}
}
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)
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"}
targets = append(targets,
makeDataTarget(getString(tMap, "alias"), sidQuery, getString(tMap, "ref_id"), sumMetric, true, datasourceType, datasourceUID, bucketAgg),
makeMathTarget(getString(tMap, "expression"), getString(tMap, "expression_ref_id")),
)
}
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")),
}
}
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",
},
}
}