grafana_gen/dashboard_vl.go
2026-03-06 12:00:26 +03:00

396 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"fmt"
"log"
"strings"
)
// generateVLDashboard создаёт дашборд для VictoriaLogs datasource.
// Использует те же панели и layout что и основной дашборд,
// но targets строятся через queryModeRegistryVL с LogsQL запросами.
func generateVLDashboard(clients map[string]ClientData, template *DashboardTemplate, config Config) map[string]interface{} {
panels := []interface{}{}
yPos := 0
panelID := 100
datasourceUID := config.VLDatasourceUID
datasourceType := DefaultVLDatasourceType
useCollapsedRows := template.Layout.UseCollapsedRows
if len(template.Layout.OverviewPanels) > 0 {
overview := createOverviewRowVL(template, datasourceType, datasourceUID, panelID, yPos)
panels = append(panels, overview)
panelID += 50
yPos++
}
for _, client := range clients {
if len(client.Domains) == 0 {
continue
}
if useCollapsedRows {
row := createCollapsedRowVL(client, template, datasourceType, datasourceUID, panelID, yPos)
panels = append(panels, row)
panelID += 100
yPos++
} else {
panels = append(panels, createRowPanel(client.ClientTitle, template, panelID, yPos))
panelID++
yPos++
tenantPanels, nextID, nextY := generateLayoutPanelsVL(
template.Layout.TenantPanels, client, nil,
template, datasourceType, datasourceUID, panelID, yPos,
)
panels = append(panels, tenantPanels...)
panelID = nextID
yPos = nextY
for _, domain := range client.Domains {
domainPanels, nextID, nextY := generateLayoutPanelsVL(
template.Layout.DomainPanels, client, &domain,
template, datasourceType, datasourceUID, panelID, yPos,
)
panels = append(panels, domainPanels...)
panelID = nextID
yPos = nextY
}
interPanels, nextID, nextY := generateLayoutPanelsVL(
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 := generateLayoutPanelsVL(
template.Layout.TrafficDomainPanels, client, &domain,
template, datasourceType, datasourceUID, panelID, yPos,
)
panels = append(panels, trafficPanels...)
panelID = nextID
yPos = nextY
}
}
}
title := config.VLDashboardTitle
dashboard := buildDashboardJSON(panels, title, template, config.VLDashboardUID)
return dashboard
}
// generateLayoutPanelsVL — аналог generateLayoutPanels но использует VL builders.
func generateLayoutPanelsVL(
layouts []PanelLayout,
client ClientData,
domain *DomainInfo,
template *DashboardTemplate,
datasourceType, datasourceUID string,
panelID, yPos int,
) ([]interface{}, int, int) {
panels := []interface{}{}
for _, layout := range layouts {
if layout.Condition != nil {
evaluator, ok := conditionRegistry[layout.Condition.Type]
if !ok || !evaluator(layout.Condition.Value, client, domain) {
continue
}
}
domainInfo := DomainInfo{}
if domain != nil {
domainInfo = *domain
}
width := layout.Width
if width == 0 {
// Берём ширину из gridPos шаблона панели
if panelDef, ok := template.Panels[layout.PanelKey]; ok {
if pm, ok := panelDef.(map[string]interface{}); ok {
if gp, ok := pm["gridPos"].(map[string]interface{}); ok {
if w, ok := gp["w"].(float64); ok && w > 0 {
width = int(w)
}
}
}
}
}
if width == 0 {
width = 12
}
xPos := layout.XOffset
panel := buildPanelVL(layout.PanelKey, template, domainInfo, client, datasourceType, datasourceUID, panelID, yPos, xPos, width)
if panel == nil {
continue
}
// Применяем title format или title suffix
if layout.TitleFormat != "" {
panel["title"] = formatPanelTitle(layout.TitleFormat, client, domainInfo)
} else if layout.TitleSuffix != "" && domainInfo.DomainName != "" {
panel["title"] = fmt.Sprintf("%s %s", domainInfo.DomainName, formatPanelTitle(layout.TitleSuffix, client, domainInfo))
} else if layout.TitleSuffix != "" {
panel["title"] = formatPanelTitle(layout.TitleSuffix, client, domainInfo)
}
panels = append(panels, panel)
panelID++
// Увеличиваем yPos только если следующая панель не рядом (другой x)
if xPos == 0 || xPos+width >= 24 {
yPos += getGridH(panel)
}
}
return panels, panelID, yPos
}
// buildPanelVL — аналог buildPanel но использует queryModeRegistryVL.
func buildPanelVL(
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
}
panel := deepCopyMap(panelMap)
// gridPos
h := 8
if gridPos, ok := panel["gridPos"].(map[string]interface{}); ok {
if hVal, ok := gridPos["h"].(float64); ok {
h = int(hVal)
}
}
panel["gridPos"] = map[string]interface{}{
"h": h, "w": width, "x": xPos, "y": yPos,
}
panel["id"] = id
// Подменяем datasource
panel["datasource"] = map[string]interface{}{
"type": datasourceType,
"uid": datasourceUID,
}
// Строим targets через VL registry
queryMode, _ := panel["query_mode"].(string)
if queryMode == "" || queryMode == "row" {
return panel
}
builder, ok := queryModeRegistryVL[queryMode]
if !ok {
log.Printf("Warning VL: unknown query_mode '%s' for panel '%s'", queryMode, panelKey)
return panel
}
queryConfigRaw, _ := panel["query_config"].(map[string]interface{})
if queryConfigRaw == nil {
queryConfigRaw = map[string]interface{}{}
}
// Строим sidQuery в LogsQL формате
var sidQuery string
if domain.SID != "" {
sidQuery = fmt.Sprintf("SID:%s", domain.SID)
} else {
sidQuery = buildTenantSIDQueryVL(client.Domains)
}
panel["targets"] = builder(queryConfigRaw, sidQuery, datasourceType, datasourceUID, template, domain, client)
// Минимальный интервал агрегации для панелей где нужна агрегация по минутам
// Читаем из шаблона панели поле vl_interval, иначе не устанавливаем (auto)
if vlInterval, ok := panel["vl_interval"].(string); ok && vlInterval != "" {
panel["interval"] = vlInterval
}
delete(panel, "vl_interval")
// Сдвиг на 1 минуту назад чтобы скрыть неполную последнюю минуту
panel["timeShift"] = "1m"
// Подставляем плейсхолдеры в title панели из шаблона
if title, ok := panel["title"].(string); ok {
panel["title"] = formatPanelTitle(title, client, domain)
}
// Для range_percent добавляем transformation которая убирает " count(*)" из названий серий
if queryMode == "range_percent" {
panel["transformations"] = []interface{}{
map[string]interface{}{
"id": "renameByRegex",
"options": map[string]interface{}{
"regex": "^(.+) count[(][*][)]$",
"renamePattern": "$1",
},
},
}
}
// Применяем RPS threshold (те же данные)
applyRPSThreshold(panel, panelKey, client.RPSLimit, client.RPSCommercialLimit)
return panel
}
// createOverviewRowVL — аналог createOverviewRow для VL.
func createOverviewRowVL(template *DashboardTemplate, datasourceType, datasourceUID string, panelID, yPos int) map[string]interface{} {
overviewPanels := []interface{}{}
dummyClient := ClientData{}
for _, layout := range template.Layout.OverviewPanels {
width := layout.Width
if width == 0 {
width = 24
}
panel := buildPanelVL(layout.PanelKey, template, DomainInfo{}, dummyClient, datasourceType, datasourceUID, panelID, 1, layout.XOffset, width)
if panel == nil {
continue
}
if layout.TitleFormat != "" {
panel["title"] = layout.TitleFormat
}
overviewPanels = append(overviewPanels, panel)
panelID++
}
rowTitle := template.Layout.OverviewRowTitle
if rowTitle == "" {
rowTitle = "Обзор"
}
return map[string]interface{}{
"type": "row",
"title": rowTitle,
"id": panelID,
"collapsed": true,
"gridPos": map[string]interface{}{"h": 1, "w": 24, "x": 0, "y": yPos},
"panels": overviewPanels,
}
}
// createCollapsedRowVL — аналог createCollapsedRow для VL.
func createCollapsedRowVL(client ClientData, template *DashboardTemplate, datasourceType, datasourceUID string, panelID, yPos int) map[string]interface{} {
innerPanels := []interface{}{}
currentY := 1
addPanelVL := func(layouts []PanelLayout, domain *DomainInfo, rowStartY int) {
for _, layout := range layouts {
if layout.Condition != nil {
evaluator, ok := conditionRegistry[layout.Condition.Type]
if !ok || !evaluator(layout.Condition.Value, client, domain) {
continue
}
}
domainInfo := DomainInfo{}
if domain != nil {
domainInfo = *domain
}
width := layout.Width
if width == 0 {
if panelDef, ok := template.Panels[layout.PanelKey]; ok {
if pm, ok := panelDef.(map[string]interface{}); ok {
if gp, ok := pm["gridPos"].(map[string]interface{}); ok {
if w, ok := gp["w"].(float64); ok && w > 0 {
width = int(w)
}
}
}
}
}
if width == 0 {
width = 12
}
xPos := layout.XOffset
panel := buildPanelVL(layout.PanelKey, template, domainInfo, client, datasourceType, datasourceUID, panelID, rowStartY, xPos, width)
if panel == nil {
continue
}
if layout.TitleFormat != "" {
panel["title"] = formatPanelTitle(layout.TitleFormat, client, domainInfo)
} else if layout.TitleSuffix != "" && domainInfo.DomainName != "" {
panel["title"] = fmt.Sprintf("%s %s", domainInfo.DomainName, formatPanelTitle(layout.TitleSuffix, client, domainInfo))
} else if layout.TitleSuffix != "" {
panel["title"] = formatPanelTitle(layout.TitleSuffix, client, domainInfo)
}
innerPanels = append(innerPanels, panel)
panelID++
}
}
// tenant_panels
addPanelVL(template.Layout.TenantPanels, nil, currentY)
currentY += 8
// domain_panels (side by side)
for _, domain := range client.Domains {
d := domain
addPanelVL(template.Layout.DomainPanels, &d, currentY)
currentY += 8
}
// inter_domain_panels
if len(template.Layout.InterDomainPanels) > 0 {
addPanelVL(template.Layout.InterDomainPanels, nil, currentY)
currentY += 8
}
// traffic_domain_panels
for _, domain := range client.Domains {
d := domain
addPanelVL(template.Layout.TrafficDomainPanels, &d, currentY)
currentY += 8
}
return map[string]interface{}{
"type": "row",
"title": client.ClientTitle,
"id": panelID,
"collapsed": true,
"gridPos": map[string]interface{}{"h": 1, "w": 24, "x": 0, "y": yPos},
"panels": innerPanels,
}
}
// getGridH возвращает высоту панели из gridPos.
func getGridH(panel map[string]interface{}) int {
if gp, ok := panel["gridPos"].(map[string]interface{}); ok {
if h, ok := gp["h"].(int); ok {
return h
}
if h, ok := gp["h"].(float64); ok {
return int(h)
}
}
return 8
}
// formatPanelTitle форматирует заголовок панели подстановкой переменных.
func formatPanelTitle(format string, client ClientData, domain DomainInfo) string {
title := format
title = strings.ReplaceAll(title, "{client}", client.ClientTitle)
title = strings.ReplaceAll(title, "{domain}", domain.DomainName)
title = strings.ReplaceAll(title, "{sid}", domain.SID)
return title
}
// sendVLDashboardToGrafana отправляет VL дашборд в Grafana.
func sendVLDashboardToGrafana(dashboard map[string]interface{}, config Config) error {
vlConfig := config
if config.VLGrafanaFolder != "" {
vlConfig.GrafanaFolder = config.VLGrafanaFolder
}
// Используем тот же sendToGrafana
return sendToGrafana(dashboard, vlConfig)
}