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", }, } }