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) // 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 }