387 lines
12 KiB
Go
Executable file
387 lines
12 KiB
Go
Executable file
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"strings"
|
||
)
|
||
|
||
// generateSingleDashboard создает один дашборд со всеми клиентами.
|
||
// Структура, метаданные и логика панелей полностью управляются шаблоном.
|
||
func generateSingleDashboard(clients map[string]ClientData, template *DashboardTemplate, config Config) map[string]interface{} {
|
||
panels := []interface{}{}
|
||
yPos := 0
|
||
panelID := 100
|
||
|
||
datasourceType := template.Datasource["type"].(string)
|
||
datasourceUID := template.Datasource["uid"].(string)
|
||
useCollapsedRows := template.Layout.UseCollapsedRows
|
||
|
||
// Генерация overview row (глобальные панели перед клиентскими строками)
|
||
if len(template.Layout.OverviewPanels) > 0 {
|
||
overview := createOverviewRow(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 := createCollapsedRow(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 := generateLayoutPanels(
|
||
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 := generateLayoutPanels(
|
||
template.Layout.DomainPanels, client, &domain,
|
||
template, datasourceType, datasourceUID, panelID, yPos,
|
||
)
|
||
panels = append(panels, domainPanels...)
|
||
panelID = nextID
|
||
yPos = nextY
|
||
}
|
||
|
||
interPanels, nextID, nextY := generateLayoutPanels(
|
||
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 := generateLayoutPanels(
|
||
template.Layout.TrafficDomainPanels, client, &domain,
|
||
template, datasourceType, datasourceUID, panelID, yPos,
|
||
)
|
||
panels = append(panels, trafficPanels...)
|
||
panelID = nextID
|
||
yPos = nextY
|
||
}
|
||
}
|
||
}
|
||
|
||
// Берём метаданные дашборда из шаблона
|
||
dashboardBase := deepCopy(template.Dashboard).(map[string]interface{})
|
||
applyDashboardMeta(dashboardBase, template, config.DashboardTitle)
|
||
dashboardBase["panels"] = panels
|
||
|
||
return buildDashboardJSON(panels, config.DashboardTitle, template, "")
|
||
}
|
||
|
||
// buildDashboardJSON собирает финальный JSON дашборда из панелей и шаблона.
|
||
// uid — переопределение UID (пустая строка = берётся из шаблона).
|
||
func buildDashboardJSON(panels []interface{}, title string, template *DashboardTemplate, uid string) map[string]interface{} {
|
||
dashboardBase := deepCopy(template.Dashboard).(map[string]interface{})
|
||
applyDashboardMeta(dashboardBase, template, title)
|
||
dashboardBase["panels"] = panels
|
||
|
||
// Если передан явный uid — переопределяем (для VL дашборда)
|
||
if uid != "" {
|
||
dashboardBase["uid"] = uid
|
||
}
|
||
|
||
return map[string]interface{}{
|
||
"dashboard": dashboardBase,
|
||
"overwrite": true,
|
||
}
|
||
}
|
||
|
||
// applyDashboardMeta применяет метаданные из секции dashboard_meta шаблона.
|
||
// Значения из шаблона используются как дефолты; title всегда берётся из config.
|
||
func applyDashboardMeta(dashboardBase map[string]interface{}, template *DashboardTemplate, title string) {
|
||
dashboardBase["title"] = title
|
||
|
||
meta := template.DashboardMeta
|
||
if meta == nil {
|
||
// Минимальные дефолты если секция отсутствует
|
||
dashboardBase["id"] = nil
|
||
dashboardBase["version"] = 0
|
||
return
|
||
}
|
||
|
||
dashboardBase["id"] = nil // всегда nil для overwrite по uid
|
||
|
||
if uid, ok := meta["uid"].(string); ok && uid != "" {
|
||
dashboardBase["uid"] = uid
|
||
}
|
||
if tags, ok := meta["tags"]; ok {
|
||
dashboardBase["tags"] = tags
|
||
}
|
||
if version, ok := meta["version"]; ok {
|
||
dashboardBase["version"] = version
|
||
}
|
||
|
||
// Переменные дашборда (templating) — переносим из шаблона если заданы
|
||
if template.Templating != nil {
|
||
dashboardBase["templating"] = deepCopy(template.Templating)
|
||
}
|
||
}
|
||
|
||
// createCollapsedRow создаёт row с панелями внутри (collapsed=true)
|
||
func createCollapsedRow(
|
||
client ClientData,
|
||
template *DashboardTemplate,
|
||
datasourceType, datasourceUID string,
|
||
startPanelID, yPos int,
|
||
) map[string]interface{} {
|
||
row := deepCopy(template.Panels["row"]).(map[string]interface{})
|
||
row["id"] = startPanelID
|
||
row["title"] = client.ClientTitle
|
||
row["collapsed"] = true
|
||
|
||
gridPos := row["gridPos"].(map[string]interface{})
|
||
gridPos["y"] = yPos
|
||
|
||
insidePanels := []interface{}{}
|
||
panelID := startPanelID + 1
|
||
currentY := 1 // последовательный y для правильного порядка внутри collapsed row
|
||
|
||
// Вспомогательная функция: добавить панель с текущим y и обновить currentY
|
||
addPanel := func(panel map[string]interface{}, h int) {
|
||
if gp, ok := panel["gridPos"].(map[string]interface{}); ok {
|
||
gp["y"] = currentY
|
||
}
|
||
insidePanels = append(insidePanels, panel)
|
||
panelID++
|
||
}
|
||
nextRow := func(h int) { currentY += h }
|
||
|
||
// Tenant panels (RPS, Status Codes)
|
||
for _, layout := range template.Layout.TenantPanels {
|
||
if !evaluateCondition(layout.Condition, client, nil) {
|
||
continue
|
||
}
|
||
domain := tenantDomain(client)
|
||
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||
datasourceType, datasourceUID, panelID, currentY, layout.XOffset, layout.Width)
|
||
if panel == nil {
|
||
continue
|
||
}
|
||
applyTitle(panel, layout, client.ClientTitle, "", "")
|
||
addPanel(panel, 8)
|
||
}
|
||
nextRow(8)
|
||
|
||
// Domain panels — status/response per domain
|
||
for _, domain := range client.Domains {
|
||
rowStart := currentY
|
||
for _, layout := range template.Layout.DomainPanels {
|
||
if !evaluateCondition(layout.Condition, client, &domain) {
|
||
continue
|
||
}
|
||
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||
datasourceType, datasourceUID, panelID, rowStart, layout.XOffset, layout.Width)
|
||
if panel == nil {
|
||
continue
|
||
}
|
||
applyTitle(panel, layout, client.ClientTitle, domain.DomainName, domain.SID)
|
||
panel["description"] = domain.Aliases
|
||
addPanel(panel, 8)
|
||
}
|
||
currentY = rowStart + 8
|
||
}
|
||
|
||
// Inter-domain панели — Total Traffic (один раз)
|
||
for _, layout := range template.Layout.InterDomainPanels {
|
||
if !evaluateCondition(layout.Condition, client, nil) {
|
||
continue
|
||
}
|
||
domain := tenantDomain(client)
|
||
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||
datasourceType, datasourceUID, panelID, currentY, layout.XOffset, layout.Width)
|
||
if panel == nil {
|
||
continue
|
||
}
|
||
applyTitle(panel, layout, client.ClientTitle, "", "")
|
||
addPanel(panel, 8)
|
||
currentY += 8
|
||
}
|
||
|
||
// Traffic domain panels — трафик per domain
|
||
for _, domain := range client.Domains {
|
||
for _, layout := range template.Layout.TrafficDomainPanels {
|
||
if !evaluateCondition(layout.Condition, client, &domain) {
|
||
continue
|
||
}
|
||
panel := buildPanel(layout.PanelKey, template, domain, client,
|
||
datasourceType, datasourceUID, panelID, currentY, layout.XOffset, layout.Width)
|
||
if panel == nil {
|
||
continue
|
||
}
|
||
applyTitle(panel, layout, client.ClientTitle, domain.DomainName, domain.SID)
|
||
panel["description"] = domain.Aliases
|
||
addPanel(panel, 8)
|
||
currentY += 8
|
||
}
|
||
}
|
||
|
||
row["panels"] = insidePanels
|
||
return row
|
||
}
|
||
|
||
// generateLayoutPanels генерирует список панелей по layouts (режим без collapsed rows)
|
||
func generateLayoutPanels(
|
||
layouts []PanelLayout,
|
||
client ClientData,
|
||
domain *DomainInfo,
|
||
template *DashboardTemplate,
|
||
datasourceType, datasourceUID string,
|
||
startPanelID, startY int,
|
||
) ([]interface{}, int, int) {
|
||
panels := []interface{}{}
|
||
panelID := startPanelID
|
||
maxHeight := 0
|
||
|
||
effectiveDomain := tenantDomain(client)
|
||
if domain != nil {
|
||
effectiveDomain = *domain
|
||
}
|
||
|
||
for _, layout := range layouts {
|
||
if !evaluateCondition(layout.Condition, client, domain) {
|
||
continue
|
||
}
|
||
panel := buildPanel(
|
||
layout.PanelKey, template, effectiveDomain, client,
|
||
datasourceType, datasourceUID,
|
||
panelID, startY, layout.XOffset, layout.Width,
|
||
)
|
||
if panel == nil {
|
||
continue
|
||
}
|
||
applyTitle(panel, layout, client.ClientTitle, effectiveDomain.DomainName, effectiveDomain.SID)
|
||
if domain != nil {
|
||
panel["description"] = domain.Aliases
|
||
}
|
||
panels = append(panels, panel)
|
||
panelID++
|
||
|
||
gridPos := panel["gridPos"].(map[string]interface{})
|
||
if h, ok := gridPos["h"].(float64); ok && int(h) > maxHeight {
|
||
maxHeight = int(h)
|
||
}
|
||
}
|
||
|
||
return panels, panelID, startY + maxHeight
|
||
}
|
||
|
||
// applyTitle применяет title_format или title_suffix к панели.
|
||
// Поддерживаемые плейсхолдеры: {client}, {domain}, {sid}, %s (fmt.Sprintf для clientTitle).
|
||
// title_suffix: результат "<domainName> <suffix>" с подстановкой плейсхолдеров.
|
||
// title_format: полный контроль над заголовком через плейсхолдеры.
|
||
func applyTitle(panel map[string]interface{}, layout PanelLayout, clientTitle, domainName, sid string) {
|
||
applyPlaceholders := func(s string) string {
|
||
s = strings.ReplaceAll(s, "{client}", clientTitle)
|
||
s = strings.ReplaceAll(s, "{domain}", domainName)
|
||
s = strings.ReplaceAll(s, "{sid}", sid)
|
||
if strings.Contains(s, "%s") {
|
||
s = fmt.Sprintf(s, clientTitle)
|
||
}
|
||
return s
|
||
}
|
||
|
||
if layout.TitleFormat != "" {
|
||
panel["title"] = applyPlaceholders(layout.TitleFormat)
|
||
} else if layout.TitleSuffix != "" && domainName != "" {
|
||
panel["title"] = fmt.Sprintf("%s %s", domainName, applyPlaceholders(layout.TitleSuffix))
|
||
}
|
||
}
|
||
|
||
// tenantDomain создаёт псевдо-домен для tenant-level панелей (все SID клиента через OR)
|
||
func tenantDomain(client ClientData) DomainInfo {
|
||
return DomainInfo{
|
||
SID: buildTenantSIDQuery(client.Domains),
|
||
DomainName: client.ClientTitle,
|
||
Aliases: fmt.Sprintf("%d domains", len(client.Domains)),
|
||
}
|
||
}
|
||
|
||
func buildTenantSIDQuery(domains []DomainInfo) string {
|
||
sids := make([]string, 0, len(domains))
|
||
for _, d := range domains {
|
||
sids = append(sids, d.SID)
|
||
}
|
||
return strings.Join(sids, " OR ")
|
||
}
|
||
|
||
// createOverviewRow создаёт collapsed row с глобальными обзорными панелями.
|
||
// Панели типа "static" — копируются из шаблона как есть, без подстановки SID из БД.
|
||
func createOverviewRow(
|
||
template *DashboardTemplate,
|
||
datasourceType, datasourceUID string,
|
||
startPanelID, yPos int,
|
||
) map[string]interface{} {
|
||
title := template.Layout.OverviewRowTitle
|
||
if title == "" {
|
||
title = "Обзор"
|
||
}
|
||
|
||
row := deepCopy(template.Panels["row"]).(map[string]interface{})
|
||
row["id"] = startPanelID
|
||
row["title"] = title
|
||
row["collapsed"] = true
|
||
row["gridPos"].(map[string]interface{})["y"] = yPos
|
||
|
||
insidePanels := []interface{}{}
|
||
panelID := startPanelID + 1
|
||
|
||
// Пустой клиент и домен — static-панели их не используют
|
||
emptyClient := ClientData{}
|
||
emptyDomain := DomainInfo{}
|
||
|
||
for _, layout := range template.Layout.OverviewPanels {
|
||
panel := buildPanel(
|
||
layout.PanelKey, template, emptyDomain, emptyClient,
|
||
datasourceType, datasourceUID,
|
||
panelID, layout.YOffset, layout.XOffset, layout.Width,
|
||
)
|
||
if panel == nil {
|
||
continue
|
||
}
|
||
if layout.TitleFormat != "" || layout.TitleSuffix != "" {
|
||
applyTitle(panel, layout, "", "", "")
|
||
}
|
||
insidePanels = append(insidePanels, panel)
|
||
panelID++
|
||
}
|
||
|
||
row["panels"] = insidePanels
|
||
return row
|
||
}
|
||
|
||
func createRowPanel(title string, template *DashboardTemplate, id, yPos int) map[string]interface{} {
|
||
row := deepCopy(template.Panels["row"]).(map[string]interface{})
|
||
row["id"] = id
|
||
row["title"] = title
|
||
row["collapsed"] = false
|
||
row["gridPos"].(map[string]interface{})["y"] = yPos
|
||
return row
|
||
}
|
||
|
||
func yOffsetOrDefault(yOffset, defaultVal int) int {
|
||
if yOffset > 0 {
|
||
return yOffset
|
||
}
|
||
return defaultVal
|
||
}
|
||
|
||
// Заглушка чтобы компилятор не ругался на неиспользуемый импорт
|
||
var _ = log.Printf
|