grafana_gen/main.go

472 lines
18 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 (
"flag"
"log"
"os"
"strings"
)
// makeSPAlertsConfig создаёт конфиг для SP алертов с отдельной группой и contact point.
func makeSPAlertsConfig(config Config) Config {
spConfig := config
spConfig.AlertsGroup = config.SPAlertsGroup
spConfig.AlertsReceiver = config.SPAlertsReceiver
return spConfig
}
// makeSPConfig создаёт конфиг для SP дашборда на основе основного конфига.
func makeSPConfig(config Config) Config {
spConfig := config
spConfig.VLDashboardTitle = config.SPDashboardTitle
spConfig.VLDashboardUID = config.SPDashboardUID
spConfig.VLGrafanaFolder = config.SPGrafanaFolder
return spConfig
}
func main() {
// Load environment variables from .env file before parsing flags
// This allows .env file to provide defaults that can be overridden by flags
loadEnvFileWithFallback()
config := parseFlags()
// Initialize state manager
stateManager := NewStateManager(config.StateFile)
if err := stateManager.Load(); err != nil {
log.Printf("Warning: Failed to load state: %v", err)
}
// Show last run info
if lastRun := stateManager.GetLastRun(); !lastRun.IsZero() {
log.Printf("Last successful run: %s", lastRun.Format("2006-01-02 15:04:05"))
clientCount, domainCount, sidCount := stateManager.GetStats()
log.Printf("Previous state: %d clients, %d domains, %d SIDs", clientCount, domainCount, sidCount)
}
// Update templates from Git
var templateCommit string
if !config.SkipGitPull {
commit, err := updateTemplates(config)
if err != nil {
log.Printf("Warning: Failed to update templates from Git: %v", err)
log.Printf("Continuing with existing templates...")
}
templateCommit = commit
} else {
// Get current commit without pulling
templateCommit = getCurrentCommit(config.TemplatesPath)
}
// Load dashboard template
template, err := loadTemplate(config.TemplatesPath)
if err != nil {
log.Fatalf("Failed to load template: %v", err)
}
log.Printf("Loaded template version: %s", template.Version)
validateAndFixTemplate(template)
// Load alert rules template
alertTmpl, err := loadAlertTemplate(config.TemplatesPath)
if err != nil {
log.Printf("Warning: Failed to load alert template: %v", err)
log.Printf("Alert rules will not be generated")
}
// Connect to database
db, err := connectDB(config)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
// Fetch clients data
clients, err := fetchClientsData(db)
if err != nil {
log.Fatalf("Failed to fetch clients data: %v", err)
}
log.Printf("Found %d clients", len(clients))
// Check if there are any clients
if len(clients) == 0 {
log.Printf("No clients found in database. Nothing to generate.")
log.Printf("This is not an error - database is empty or no data matches criteria.")
os.Exit(0)
}
// Filter out clients without domains
validClients := make(map[string]ClientData)
skippedCount := 0
for clientTitle, clientData := range clients {
if len(clientData.Domains) == 0 {
log.Printf("Warning: Client %s has no domains, skipping", clientTitle)
skippedCount++
continue
}
validClients[clientTitle] = clientData
}
if len(validClients) == 0 {
log.Printf("No valid clients with domains found. Nothing to generate.")
os.Exit(0)
}
// Разделяем клиентов по waf_provider
spikClients := map[string]ClientData{}
spClients := map[string]ClientData{}
for k, v := range validClients {
switch strings.ToLower(v.WafProvider) {
case "spik":
spikClients[k] = v
case "sp":
spClients[k] = v
}
}
// Хэш шаблона алертов — считается независимо от дашборда
alertTemplateHash := hashAlertTemplate(config.TemplatesPath)
alertsChanged := stateManager.AlertsNeedUpdate(spikClients) || stateManager.AlertTemplateChanged(alertTemplateHash)
// Check for changes
hasChanges, changeReason := stateManager.CheckChanges(templateCommit, template.Version, clients)
if !hasChanges && !alertsChanged && !config.ForceRun {
log.Printf("=== No Changes Detected ===")
log.Printf("No changes in templates or database since last run.")
log.Printf("Skipping dashboard generation.")
log.Printf("")
log.Printf("Use -force flag to regenerate anyway.")
os.Exit(0)
}
if hasChanges {
log.Printf("=== Changes Detected ===")
log.Printf("%s", changeReason)
log.Printf("")
} else if alertsChanged {
log.Printf("=== Alert Rules Changed ===")
log.Printf("")
}
if config.ForceRun {
log.Printf("Force run enabled - regenerating dashboard")
}
// Generate single dashboard with spik clients
log.Printf("Generating single dashboard: %s", config.DashboardTitle)
log.Printf("Including %d spik clients with %d total domains", len(spikClients), countTotalDomains(spikClients))
dashboard := generateSingleDashboard(spikClients, template, config)
if config.DryRun {
if config.DashboardOSEnabled {
logDryRun(config.DashboardTitle, dashboard)
}
if alertTmpl != nil {
log.Printf("DRY RUN: Generating alert rules preview...")
if config.AlertsOSEnabled {
_ = generateAndSendAlerts(spikClients, *alertTmpl, config, true)
}
if config.AlertsVLEnabled {
_, _ = generateAndSendAlertsVL(spikClients, *alertTmpl, config, true)
}
}
log.Printf("=== Summary ===")
log.Printf("Spik clients: %d, SP clients: %d", len(spikClients), len(spClients))
log.Printf("Skipped: %d", skippedCount)
log.Printf("DRY RUN: Dashboard generated but not sent to Grafana")
if config.VLDatasourceUID != "" && config.DashboardVLEnabled {
vlDashboard := generateVLDashboard(spikClients, template, config)
logDryRun(config.VLDashboardTitle, vlDashboard)
if len(spClients) > 0 && config.SPDashboardTitle != "" {
spConfig := makeSPConfig(config)
spDashboard := generateVLDashboard(spClients, template, spConfig)
logDryRun(config.SPDashboardTitle, spDashboard)
}
}
os.Exit(0)
}
// Send OS dashboard to Grafana
if config.DashboardOSEnabled {
if err := sendToGrafana(dashboard, config); err != nil {
log.Printf("Error sending dashboard: %v", err)
log.Printf("=== Summary ===")
log.Printf("Spik clients: %d", len(spikClients))
log.Printf("Skipped: %d", skippedCount)
log.Printf("Status: FAILED")
os.Exit(1)
}
log.Printf("Successfully created/updated dashboard: %s", config.DashboardTitle)
} else {
log.Printf("Dashboard OS: disabled (DASHBOARD_OS_ENABLED=false)")
}
// Generate VictoriaLogs dashboard for spik clients
if config.VLDatasourceUID != "" && config.DashboardVLEnabled {
log.Printf("Generating VictoriaLogs dashboard: %s", config.VLDashboardTitle)
vlDashboard := generateVLDashboard(spikClients, template, config)
if err := sendVLDashboardToGrafana(vlDashboard, config); err != nil {
log.Printf("Warning: VictoriaLogs dashboard failed: %v", err)
} else {
log.Printf("Successfully created/updated VL dashboard: %s", config.VLDashboardTitle)
}
} else if config.VLDatasourceUID != "" {
log.Printf("Dashboard VL: disabled (DASHBOARD_VL_ENABLED=false)")
}
// Generate SP VictoriaLogs dashboard for sp clients
if config.VLDatasourceUID != "" && config.SPDashboardTitle != "" && len(spClients) > 0 {
log.Printf("Generating SP VictoriaLogs dashboard: %s (%d clients)", config.SPDashboardTitle, len(spClients))
spConfig := makeSPConfig(config)
spDashboard := generateVLDashboard(spClients, template, spConfig)
if err := sendVLDashboardToGrafana(spDashboard, spConfig); err != nil {
log.Printf("Warning: SP dashboard failed: %v", err)
} else {
log.Printf("Successfully created/updated SP dashboard: %s", config.SPDashboardTitle)
}
}
// Generate and send alert rules
if alertTmpl != nil {
if alertsChanged || config.ForceRun {
osErr := false
vlErr := false
if config.AlertsOSEnabled {
if err := generateAndSendAlerts(spikClients, *alertTmpl, config, false); err != nil {
log.Printf("Warning: alerts generation failed: %v", err)
osErr = true
}
} else {
log.Printf("Alerts OS: disabled (ALERTS_OS_ENABLED=false)")
}
allActiveUIDs := map[string]bool{}
if config.AlertsVLEnabled {
spikUIDs, err := generateAndSendAlertsVL(spikClients, *alertTmpl, config, false)
if err != nil {
log.Printf("Warning: VL alerts generation failed: %v", err)
vlErr = true
}
for _, uid := range spikUIDs {
allActiveUIDs[uid] = true
}
// SP алерты — для sp клиентов с отдельным contact point и группой
if len(spClients) > 0 {
spAlertsConfig := makeSPAlertsConfig(config)
spUIDs, err := generateAndSendAlertsVL(spClients, *alertTmpl, spAlertsConfig, false)
if err != nil {
log.Printf("Warning: SP VL alerts generation failed: %v", err)
}
for _, uid := range spUIDs {
allActiveUIDs[uid] = true
}
}
// Статические алерты — отправляем один раз с основным конфигом
// (не с SP конфигом, чтобы попали в правильную группу [SYS] PTAF Grafana)
if !config.AlertsOSEnabled {
if err := sendStaticAlerts(*alertTmpl, config, false); err != nil {
log.Printf("Warning: VL static alerts error: %v", err)
}
for _, a := range alertTmpl.StaticAlerts {
if uid, ok := a["uid"].(string); ok {
allActiveUIDs[uid] = true
}
}
}
if folderUID, err := ensureAlertFolder(config); err == nil {
deleteObsoleteAlerts(allActiveUIDs, folderUID, config)
}
} else {
log.Printf("Alerts VL: disabled (ALERTS_VL_ENABLED=false)")
}
// Сохраняем хэш если не было критических ошибок
// Частичные ошибки отдельных алертов не блокируют сохранение хэша
if !osErr && !vlErr {
stateManager.UpdateAlertsHash(spikClients)
stateManager.UpdateAlertTemplateHash(alertTemplateHash)
}
} else {
log.Printf("Alerts: no changes in rps_limit, skipping")
}
}
// Summary
log.Printf("=== Summary ===")
log.Printf("Dashboard: %s", config.DashboardTitle)
log.Printf("Spik clients: %d, SP clients: %d", len(spikClients), len(spClients))
if skippedCount > 0 {
log.Printf("Skipped: %d", skippedCount)
}
log.Printf("Total domains: %d", countTotalDomains(validClients))
log.Printf("Status: SUCCESS")
// Update state
stateManager.Update(templateCommit, template.Version, clients, changeReason)
if err := stateManager.Save(); err != nil {
log.Printf("Warning: Failed to save state: %v", err)
} else {
log.Printf("State saved to %s", config.StateFile)
}
}
func countTotalDomains(clients map[string]ClientData) int {
count := 0
for _, client := range clients {
count += len(client.Domains)
}
return count
}
func parseFlags() Config {
config := Config{}
// Database flags - resolve from env vars with fallback to defaults
dbUser := getEnvOrDefault("DB_USER", DefaultDBUser)
dbPassword := getEnvOrDefault("DB_PASSWORD", DefaultDBPassword)
// Grafana settings - resolve from env vars with fallback to defaults
grafanaFolder := getEnvOrDefault("GRAFANA_FOLDER", DefaultGrafanaFolder)
dashboardTitle := getEnvOrDefault("DASHBOARD_TITLE", DefaultDashboardTitle)
flag.StringVar(&config.DBHost, "db-host", DefaultPrimaryDBHost, "PostgreSQL host")
flag.IntVar(&config.DBPort, "db-port", DefaultDBPort, "PostgreSQL port")
flag.StringVar(&config.DBUser, "db-user", dbUser, "PostgreSQL user")
flag.StringVar(&config.DBPassword, "db-password", dbPassword, "PostgreSQL password")
flag.StringVar(&config.DBName, "db-name", DefaultDBName, "PostgreSQL database name")
// Grafana flags
var grafanaURLFlag, grafanaAPIKeyFlag string
flag.StringVar(&grafanaURLFlag, "grafana-url", "", "Grafana URL")
flag.StringVar(&grafanaAPIKeyFlag, "grafana-api-key", "", "Grafana API key")
flag.StringVar(&config.GrafanaFolder, "grafana-folder", grafanaFolder, "Grafana folder for dashboards")
flag.StringVar(&config.DashboardTitle, "dashboard-title", dashboardTitle, "Dashboard title")
// Git flags
var gitTokenFlag string
flag.StringVar(&gitTokenFlag, "git-token", "", "Gitea token (overrides env var and hardcoded value)")
flag.StringVar(&config.TemplatesRepo, "templates-repo", DefaultTemplatesRepo, "Git repository URL for templates")
flag.StringVar(&config.TemplatesBranch, "templates-branch", DefaultTemplatesBranch, "Git branch for templates")
flag.StringVar(&config.TemplatesPath, "templates-path", DefaultTemplatesPath, "Local path for templates")
flag.BoolVar(&config.SkipGitPull, "skip-git-pull", false, "Skip updating templates from Git")
// Alerts flags
alertsFolder := getEnvOrDefault("ALERTS_FOLDER", DefaultAlertsFolder)
alertsReceiver := getEnvOrDefault("ALERTS_RECEIVER", DefaultAlertsReceiver)
alertsGroup := getEnvOrDefault("ALERTS_GROUP", DefaultAlertsGroup)
alertsDatasource := getEnvOrDefault("ALERTS_DATASOURCE_UID", DefaultAlertsDatasource)
flag.StringVar(&config.AlertsFolder, "alerts-folder", alertsFolder, "Grafana folder for alert rules")
flag.StringVar(&config.AlertsReceiver, "alerts-receiver", alertsReceiver, "Grafana notification receiver for alerts")
flag.StringVar(&config.AlertsGroup, "alerts-group", alertsGroup, "Grafana alert rule group name")
flag.StringVar(&config.AlertsDatasourceUID, "alerts-datasource-uid", alertsDatasource, "OpenSearch datasource UID for alerts")
// VictoriaLogs second dashboard flags
vlDatasourceUID := getEnvOrDefault("VL_DATASOURCE_UID", DefaultVLDatasourceUID)
vlDashboardTitle := getEnvOrDefault("VL_DASHBOARD_TITLE", DefaultVLDashboardTitle)
vlGrafanaFolder := getEnvOrDefault("VL_GRAFANA_FOLDER", DefaultVLGrafanaFolder)
vlDashboardUID := getEnvOrDefault("VL_DASHBOARD_UID", DefaultVLDashboardUID)
flag.StringVar(&config.VLDatasourceUID, "vl-datasource-uid", vlDatasourceUID, "VictoriaLogs datasource UID (empty = skip VL dashboard)")
flag.StringVar(&config.VLDashboardTitle, "vl-dashboard-title", vlDashboardTitle, "VictoriaLogs dashboard title")
flag.StringVar(&config.VLGrafanaFolder, "vl-grafana-folder", vlGrafanaFolder, "Grafana folder for VictoriaLogs dashboard (empty = same as main)")
flag.StringVar(&config.VLDashboardUID, "vl-dashboard-uid", vlDashboardUID, "VictoriaLogs dashboard UID in Grafana")
// SP dashboard flags
spDashboardTitle := getEnvOrDefault("SP_DASHBOARD_TITLE", DefaultSPDashboardTitle)
spDashboardUID := getEnvOrDefault("SP_DASHBOARD_UID", DefaultSPDashboardUID)
spGrafanaFolder := getEnvOrDefault("SP_GRAFANA_FOLDER", DefaultSPGrafanaFolder)
flag.StringVar(&config.SPDashboardTitle, "sp-dashboard-title", spDashboardTitle, "SP VictoriaLogs dashboard title")
flag.StringVar(&config.SPDashboardUID, "sp-dashboard-uid", spDashboardUID, "SP VictoriaLogs dashboard UID in Grafana")
flag.StringVar(&config.SPGrafanaFolder, "sp-grafana-folder", spGrafanaFolder, "Grafana folder for SP dashboard (empty = same as main)")
config.SPAlertsGroup = getEnvOrDefault("SP_ALERTS_GROUP", DefaultSPAlertsGroup)
config.SPAlertsReceiver = getEnvOrDefault("SP_ALERTS_RECEIVER", DefaultSPAlertsReceiver)
// Other flags
flag.BoolVar(&config.DryRun, "dry-run", false, "Generate JSON but don't send to Grafana")
flag.StringVar(&config.StateFile, "state-file", DefaultStateFile, "Path to state file")
flag.BoolVar(&config.ForceRun, "force", false, "Force regeneration even if no changes detected")
config.AlertsOSEnabled = getEnvOrDefault("ALERTS_OS_ENABLED", "true") != "false"
config.AlertsVLEnabled = getEnvOrDefault("ALERTS_VL_ENABLED", "true") != "false"
config.DashboardOSEnabled = getEnvOrDefault("DASHBOARD_OS_ENABLED", "true") != "false"
config.DashboardVLEnabled = getEnvOrDefault("DASHBOARD_VL_ENABLED", "true") != "false"
flag.Parse()
// Resolve tokens with priority
config.GitToken = resolveToken("Git", gitTokenFlag, "GIT_TOKEN", DefaultGitToken)
config.GrafanaAPIKey = resolveToken("Grafana API", grafanaAPIKeyFlag, "GRAFANA_API_KEY", DefaultGrafanaAPIKey)
config.GrafanaURL = resolveValue("Grafana URL", grafanaURLFlag, "GRAFANA_URL", DefaultGrafanaURL)
// Validate required parameters
if config.GrafanaURL == "" {
log.Fatal("Error: grafana-url is required (use -grafana-url flag, GRAFANA_URL env var, .env file, or set DefaultGrafanaURL in config.go)")
}
if config.GrafanaAPIKey == "" {
log.Fatal("Error: grafana-api-key is required (use -grafana-api-key flag, GRAFANA_API_KEY env var, .env file, or set DefaultGrafanaAPIKey in config.go)")
}
if config.DBUser == "" {
log.Fatal("Error: db-user is required (use -db-user flag, DB_USER env var, or .env file)")
}
if config.DBPassword == "" {
log.Fatal("Error: db-password is required (use -db-password flag, DB_PASSWORD env var, or .env file)")
}
return config
}
// resolveToken определяет токен с приоритетом:
// 1. Флаг командной строки
// 2. Переменная окружения
// 3. Hardcoded константа
func resolveToken(name, flagValue, envVar, defaultValue string) string {
// Приоритет 1: флаг командной строки
if flagValue != "" {
log.Printf("Using %s token from command line flag", name)
return flagValue
}
// Приоритет 2: переменная окружения
if envValue := os.Getenv(envVar); envValue != "" {
log.Printf("Using %s token from %s environment variable", name, envVar)
return envValue
}
// Приоритет 3: hardcoded в config.go
if defaultValue != "" {
log.Printf("Using %s token from config.go (hardcoded)", name)
return defaultValue
}
// Токен не задан
return ""
}
// resolveValue определяет значение с приоритетом (для non-secret параметров)
func resolveValue(name, flagValue, envVar, defaultValue string) string {
// Приоритет 1: флаг командной строки
if flagValue != "" {
return flagValue
}
// Приоритет 2: переменная окружения
if envValue := os.Getenv(envVar); envValue != "" {
log.Printf("Using %s from %s environment variable", name, envVar)
return envValue
}
// Приоритет 3: hardcoded в config.go
if defaultValue != "" {
log.Printf("Using %s from config.go (hardcoded)", name)
return defaultValue
}
return ""
}
// getEnvOrDefault возвращает переменную окружения или default значение
func getEnvOrDefault(envVar, defaultValue string) string {
if value := os.Getenv(envVar); value != "" {
return value
}
return defaultValue
}