454 lines
14 KiB
Go
454 lines
14 KiB
Go
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++
|
||
}
|
||
|
||
if len(template.Layout.ErrorPanels) > 0 {
|
||
errorsRow := createErrorRowVL(template, datasourceType, datasourceUID, panelID, yPos)
|
||
panels = append(panels, errorsRow)
|
||
panelID += 10
|
||
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)
|
||
}
|
||
|
||
// Для rps панелей убираем суффикс " rps" из названий серий
|
||
if queryMode == "rps" || queryMode == "rps_multi" || queryMode == "rps_var" {
|
||
panel["transformations"] = []interface{}{
|
||
map[string]interface{}{
|
||
"id": "renameByRegex",
|
||
"options": map[string]interface{}{
|
||
"regex": "^(.+) rps$",
|
||
"renamePattern": "$1",
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// Для 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)
|
||
}
|
||
|
||
// createErrorRowVL создаёт collapsed row с панелями ошибок (только для VL).
|
||
func createErrorRowVL(template *DashboardTemplate, datasourceType, datasourceUID string, panelID, yPos int) map[string]interface{} {
|
||
innerPanels := []interface{}{}
|
||
currentY := 1
|
||
dummyClient := ClientData{}
|
||
|
||
for _, layout := range template.Layout.ErrorPanels {
|
||
width := layout.Width
|
||
if width == 0 {
|
||
width = 24
|
||
}
|
||
panel := buildPanelVL(layout.PanelKey, template, DomainInfo{}, dummyClient, datasourceType, datasourceUID, panelID, currentY, layout.XOffset, width)
|
||
if panel == nil {
|
||
continue
|
||
}
|
||
if layout.TitleFormat != "" {
|
||
panel["title"] = layout.TitleFormat
|
||
}
|
||
innerPanels = append(innerPanels, panel)
|
||
panelID++
|
||
currentY += getGridH(panel)
|
||
}
|
||
|
||
rowTitle := template.Layout.ErrorRowTitle
|
||
if rowTitle == "" {
|
||
rowTitle = "Errors"
|
||
}
|
||
|
||
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": innerPanels,
|
||
}
|
||
}
|