package cmd import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "dbbackup/internal/catalog" "dbbackup/internal/rto" "github.com/spf13/cobra" ) var rtoCmd = &cobra.Command{ Use: "rto", Short: "RTO/RPO analysis and monitoring", Long: `Analyze and monitor Recovery Time Objective (RTO) and Recovery Point Objective (RPO) metrics. RTO: How long to recover from a failure RPO: How much data you can afford to lose Examples: # Analyze RTO/RPO for all databases dbbackup rto analyze # Analyze specific database dbbackup rto analyze --database mydb # Show summary status dbbackup rto status # Set targets and check compliance dbbackup rto check --target-rto 4h --target-rpo 1h`, } var rtoAnalyzeCmd = &cobra.Command{ Use: "analyze", Short: "Analyze RTO/RPO for databases", Long: "Perform detailed RTO/RPO analysis based on backup history", RunE: runRTOAnalyze, } var rtoStatusCmd = &cobra.Command{ Use: "status", Short: "Show RTO/RPO status summary", Long: "Display current RTO/RPO compliance status for all databases", RunE: runRTOStatus, } var rtoCheckCmd = &cobra.Command{ Use: "check", Short: "Check RTO/RPO compliance", Long: "Check if databases meet RTO/RPO targets", RunE: runRTOCheck, } var ( rtoDatabase string rtoTargetRTO string rtoTargetRPO string rtoCatalog string rtoFormat string rtoOutput string ) func init() { rootCmd.AddCommand(rtoCmd) rtoCmd.AddCommand(rtoAnalyzeCmd) rtoCmd.AddCommand(rtoStatusCmd) rtoCmd.AddCommand(rtoCheckCmd) // Analyze command flags rtoAnalyzeCmd.Flags().StringVarP(&rtoDatabase, "database", "d", "", "Database to analyze (all if not specified)") rtoAnalyzeCmd.Flags().StringVar(&rtoTargetRTO, "target-rto", "4h", "Target RTO (e.g., 4h, 30m)") rtoAnalyzeCmd.Flags().StringVar(&rtoTargetRPO, "target-rpo", "1h", "Target RPO (e.g., 1h, 15m)") rtoAnalyzeCmd.Flags().StringVar(&rtoCatalog, "catalog", "", "Path to backup catalog") rtoAnalyzeCmd.Flags().StringVarP(&rtoFormat, "format", "f", "text", "Output format (text, json)") rtoAnalyzeCmd.Flags().StringVarP(&rtoOutput, "output", "o", "", "Output file") // Status command flags rtoStatusCmd.Flags().StringVar(&rtoCatalog, "catalog", "", "Path to backup catalog") rtoStatusCmd.Flags().StringVar(&rtoTargetRTO, "target-rto", "4h", "Target RTO") rtoStatusCmd.Flags().StringVar(&rtoTargetRPO, "target-rpo", "1h", "Target RPO") // Check command flags rtoCheckCmd.Flags().StringVarP(&rtoDatabase, "database", "d", "", "Database to check") rtoCheckCmd.Flags().StringVar(&rtoTargetRTO, "target-rto", "4h", "Target RTO") rtoCheckCmd.Flags().StringVar(&rtoTargetRPO, "target-rpo", "1h", "Target RPO") rtoCheckCmd.Flags().StringVar(&rtoCatalog, "catalog", "", "Path to backup catalog") } func runRTOAnalyze(cmd *cobra.Command, args []string) error { ctx := context.Background() // Parse duration targets targetRTO, err := time.ParseDuration(rtoTargetRTO) if err != nil { return fmt.Errorf("invalid target-rto: %w", err) } targetRPO, err := time.ParseDuration(rtoTargetRPO) if err != nil { return fmt.Errorf("invalid target-rpo: %w", err) } // Get catalog cat, err := openRTOCatalog() if err != nil { return err } defer cat.Close() // Create calculator config := rto.DefaultConfig() config.TargetRTO = targetRTO config.TargetRPO = targetRPO calc := rto.NewCalculator(cat, config) var analyses []*rto.Analysis if rtoDatabase != "" { // Analyze single database analysis, err := calc.Analyze(ctx, rtoDatabase) if err != nil { return fmt.Errorf("analysis failed: %w", err) } analyses = append(analyses, analysis) } else { // Analyze all databases analyses, err = calc.AnalyzeAll(ctx) if err != nil { return fmt.Errorf("analysis failed: %w", err) } } // Output if rtoFormat == "json" { return outputJSON(analyses, rtoOutput) } return outputAnalysisText(analyses) } func runRTOStatus(cmd *cobra.Command, args []string) error { ctx := context.Background() // Parse targets targetRTO, err := time.ParseDuration(rtoTargetRTO) if err != nil { return fmt.Errorf("invalid target-rto: %w", err) } targetRPO, err := time.ParseDuration(rtoTargetRPO) if err != nil { return fmt.Errorf("invalid target-rpo: %w", err) } // Get catalog cat, err := openRTOCatalog() if err != nil { return err } defer cat.Close() // Create calculator and analyze all config := rto.DefaultConfig() config.TargetRTO = targetRTO config.TargetRPO = targetRPO calc := rto.NewCalculator(cat, config) analyses, err := calc.AnalyzeAll(ctx) if err != nil { return fmt.Errorf("analysis failed: %w", err) } // Create summary summary := rto.Summarize(analyses) // Display status fmt.Println() fmt.Println("+-----------------------------------------------------------+") fmt.Println("| RTO/RPO STATUS SUMMARY |") fmt.Println("+-----------------------------------------------------------+") fmt.Printf("| Target RTO: %-15s Target RPO: %-15s |\n", formatDuration(config.TargetRTO), formatDuration(config.TargetRPO)) fmt.Println("+-----------------------------------------------------------+") // Compliance status rpoRate := 0.0 rtoRate := 0.0 fullRate := 0.0 if summary.TotalDatabases > 0 { rpoRate = float64(summary.RPOCompliant) / float64(summary.TotalDatabases) * 100 rtoRate = float64(summary.RTOCompliant) / float64(summary.TotalDatabases) * 100 fullRate = float64(summary.FullyCompliant) / float64(summary.TotalDatabases) * 100 } fmt.Printf("| Databases: %-5d |\n", summary.TotalDatabases) fmt.Printf("| RPO Compliant: %-5d (%.0f%%) |\n", summary.RPOCompliant, rpoRate) fmt.Printf("| RTO Compliant: %-5d (%.0f%%) |\n", summary.RTOCompliant, rtoRate) fmt.Printf("| Fully Compliant: %-3d (%.0f%%) |\n", summary.FullyCompliant, fullRate) if summary.CriticalIssues > 0 { fmt.Printf("| [WARN] Critical Issues: %-3d |\n", summary.CriticalIssues) } fmt.Println("+-----------------------------------------------------------+") fmt.Printf("| Average RPO: %-15s Worst: %-15s |\n", formatDuration(summary.AverageRPO), formatDuration(summary.WorstRPO)) fmt.Printf("| Average RTO: %-15s Worst: %-15s |\n", formatDuration(summary.AverageRTO), formatDuration(summary.WorstRTO)) if summary.WorstRPODatabase != "" { fmt.Printf("| Worst RPO Database: %-38s|\n", summary.WorstRPODatabase) } if summary.WorstRTODatabase != "" { fmt.Printf("| Worst RTO Database: %-38s|\n", summary.WorstRTODatabase) } fmt.Println("+-----------------------------------------------------------+") fmt.Println() // Per-database status if len(analyses) > 0 { fmt.Println("Database Status:") fmt.Println(strings.Repeat("-", 70)) fmt.Printf("%-25s %-12s %-12s %-12s\n", "DATABASE", "RPO", "RTO", "STATUS") fmt.Println(strings.Repeat("-", 70)) for _, a := range analyses { status := "[OK]" if !a.RPOCompliant || !a.RTOCompliant { status = "[FAIL]" } rpoStr := formatDuration(a.CurrentRPO) rtoStr := formatDuration(a.CurrentRTO) if !a.RPOCompliant { rpoStr = "[WARN] " + rpoStr } if !a.RTOCompliant { rtoStr = "[WARN] " + rtoStr } fmt.Printf("%-25s %-12s %-12s %s\n", truncateRTO(a.Database, 24), rpoStr, rtoStr, status) } fmt.Println(strings.Repeat("-", 70)) } return nil } func runRTOCheck(cmd *cobra.Command, args []string) error { ctx := context.Background() // Parse targets targetRTO, err := time.ParseDuration(rtoTargetRTO) if err != nil { return fmt.Errorf("invalid target-rto: %w", err) } targetRPO, err := time.ParseDuration(rtoTargetRPO) if err != nil { return fmt.Errorf("invalid target-rpo: %w", err) } // Get catalog cat, err := openRTOCatalog() if err != nil { return err } defer cat.Close() // Create calculator config := rto.DefaultConfig() config.TargetRTO = targetRTO config.TargetRPO = targetRPO calc := rto.NewCalculator(cat, config) var analyses []*rto.Analysis if rtoDatabase != "" { analysis, err := calc.Analyze(ctx, rtoDatabase) if err != nil { return fmt.Errorf("analysis failed: %w", err) } analyses = append(analyses, analysis) } else { analyses, err = calc.AnalyzeAll(ctx) if err != nil { return fmt.Errorf("analysis failed: %w", err) } } // Check compliance exitCode := 0 for _, a := range analyses { if !a.RPOCompliant { fmt.Printf("[FAIL] %s: RPO violation - current %s exceeds target %s\n", a.Database, formatDuration(a.CurrentRPO), formatDuration(config.TargetRPO)) exitCode = 1 } if !a.RTOCompliant { fmt.Printf("[FAIL] %s: RTO violation - estimated %s exceeds target %s\n", a.Database, formatDuration(a.CurrentRTO), formatDuration(config.TargetRTO)) exitCode = 1 } if a.RPOCompliant && a.RTOCompliant { fmt.Printf("[OK] %s: Compliant (RPO: %s, RTO: %s)\n", a.Database, formatDuration(a.CurrentRPO), formatDuration(a.CurrentRTO)) } } if exitCode != 0 { os.Exit(exitCode) } return nil } func openRTOCatalog() (*catalog.SQLiteCatalog, error) { catalogPath := rtoCatalog if catalogPath == "" { homeDir, _ := os.UserHomeDir() catalogPath = filepath.Join(homeDir, ".dbbackup", "catalog.db") } cat, err := catalog.NewSQLiteCatalog(catalogPath) if err != nil { return nil, fmt.Errorf("failed to open catalog: %w", err) } return cat, nil } func outputJSON(data interface{}, outputPath string) error { jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { return err } if outputPath != "" { return os.WriteFile(outputPath, jsonData, 0644) } fmt.Println(string(jsonData)) return nil } func outputAnalysisText(analyses []*rto.Analysis) error { for _, a := range analyses { fmt.Println() fmt.Println(strings.Repeat("=", 60)) fmt.Printf(" Database: %s\n", a.Database) fmt.Println(strings.Repeat("=", 60)) // Status rpoStatus := "[OK] Compliant" if !a.RPOCompliant { rpoStatus = "[FAIL] Violation" } rtoStatus := "[OK] Compliant" if !a.RTOCompliant { rtoStatus = "[FAIL] Violation" } fmt.Println() fmt.Println(" Recovery Objectives:") fmt.Println(strings.Repeat("-", 50)) fmt.Printf(" RPO (Current): %-15s Target: %s\n", formatDuration(a.CurrentRPO), formatDuration(a.TargetRPO)) fmt.Printf(" RPO Status: %s\n", rpoStatus) fmt.Printf(" RTO (Estimated): %-14s Target: %s\n", formatDuration(a.CurrentRTO), formatDuration(a.TargetRTO)) fmt.Printf(" RTO Status: %s\n", rtoStatus) if a.LastBackup != nil { fmt.Printf(" Last Backup: %s\n", a.LastBackup.Format("2006-01-02 15:04:05")) } if a.BackupInterval > 0 { fmt.Printf(" Backup Interval: %s\n", formatDuration(a.BackupInterval)) } // RTO Breakdown fmt.Println() fmt.Println(" RTO Breakdown:") fmt.Println(strings.Repeat("-", 50)) b := a.RTOBreakdown fmt.Printf(" Detection: %s\n", formatDuration(b.DetectionTime)) fmt.Printf(" Decision: %s\n", formatDuration(b.DecisionTime)) if b.DownloadTime > 0 { fmt.Printf(" Download: %s\n", formatDuration(b.DownloadTime)) } fmt.Printf(" Restore: %s\n", formatDuration(b.RestoreTime)) fmt.Printf(" Startup: %s\n", formatDuration(b.StartupTime)) fmt.Printf(" Validation: %s\n", formatDuration(b.ValidationTime)) fmt.Printf(" Switchover: %s\n", formatDuration(b.SwitchoverTime)) fmt.Println(strings.Repeat("-", 30)) fmt.Printf(" Total: %s\n", formatDuration(b.TotalTime)) // Recommendations if len(a.Recommendations) > 0 { fmt.Println() fmt.Println(" Recommendations:") fmt.Println(strings.Repeat("-", 50)) for _, r := range a.Recommendations { icon := "[TIP]" switch r.Priority { case rto.PriorityCritical: icon = "🔴" case rto.PriorityHigh: icon = "🟠" case rto.PriorityMedium: icon = "🟡" } fmt.Printf(" %s [%s] %s\n", icon, r.Priority, r.Title) fmt.Printf(" %s\n", r.Description) } } } return nil } func formatDuration(d time.Duration) string { if d < time.Minute { return fmt.Sprintf("%.0fs", d.Seconds()) } if d < time.Hour { return fmt.Sprintf("%.0fm", d.Minutes()) } hours := int(d.Hours()) mins := int(d.Minutes()) - hours*60 return fmt.Sprintf("%dh %dm", hours, mins) } func truncateRTO(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen-3] + "..." }