grafana_gen/dashboard.go

387 lines
12 KiB
Go
Executable file
Raw Permalink 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"
)
// 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