grafana_gen/main.go
2026-03-23 15:39:07 +03:00

350 lines
13 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 (
"flag"
"log"
"os"
)
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)
}
// Хэш шаблона алертов — считается независимо от дашборда
alertTemplateHash := hashAlertTemplate(config.TemplatesPath)
alertsChanged := stateManager.AlertsNeedUpdate(validClients) || 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 all clients
log.Printf("Generating single dashboard: %s", config.DashboardTitle)
log.Printf("Including %d clients with %d total domains", len(validClients), countTotalDomains(validClients))
dashboard := generateSingleDashboard(validClients, template, config)
if config.DryRun {
logDryRun(config.DashboardTitle, dashboard)
if alertTmpl != nil {
log.Printf("DRY RUN: Generating alert rules preview...")
_ = generateAndSendAlerts(validClients, *alertTmpl, config, true)
}
log.Printf("=== Summary ===")
log.Printf("Clients: %d", len(validClients))
log.Printf("Skipped: %d", skippedCount)
log.Printf("DRY RUN: Dashboard generated but not sent to Grafana")
if config.VLDatasourceUID != "" {
vlDashboard := generateVLDashboard(validClients, template, config)
logDryRun(config.VLDashboardTitle, vlDashboard)
}
os.Exit(0)
}
// Send to Grafana
if err := sendToGrafana(dashboard, config); err != nil {
log.Printf("Error sending dashboard: %v", err)
log.Printf("=== Summary ===")
log.Printf("Clients: %d", len(validClients))
log.Printf("Skipped: %d", skippedCount)
log.Printf("Status: FAILED")
os.Exit(1)
}
log.Printf("Successfully created/updated dashboard: %s", config.DashboardTitle)
// Generate VictoriaLogs dashboard if configured
if config.VLDatasourceUID != "" {
log.Printf("Generating VictoriaLogs dashboard: %s", config.VLDashboardTitle)
vlDashboard := generateVLDashboard(validClients, 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)
}
}
// Generate and send alert rules
if alertTmpl != nil {
if alertsChanged || config.ForceRun {
if err := generateAndSendAlerts(validClients, *alertTmpl, config, false); err != nil {
log.Printf("Warning: alerts generation failed: %v", err)
} else {
stateManager.UpdateAlertsHash(validClients)
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("Clients: %d", len(validClients))
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")
// 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")
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
}