From 881ff8cc72476f95528e402495040ded3b732495 Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 17 Aug 2025 10:14:22 -0700 Subject: first command added: pviz `pviz` is a command-line tool that helps you understand the real impact of service availability targets (SLAs) by converting availability percentages into actual downtime durations across different time periods. --- cmd/pviz/main.go | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 cmd/pviz/main.go (limited to 'cmd/pviz') diff --git a/cmd/pviz/main.go b/cmd/pviz/main.go new file mode 100644 index 0000000..58040be --- /dev/null +++ b/cmd/pviz/main.go @@ -0,0 +1,258 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "text/tabwriter" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type Period string + +const ( + PeriodDay Period = "day" + PeriodWeek Period = "week" + PeriodMonth Period = "month" + PeriodQuarter Period = "quarter" + PeriodYear Period = "year" +) + +var periodMinutes = map[Period]float64{ + PeriodDay: 24 * 60, + PeriodWeek: 7 * 24 * 60, + PeriodMonth: 30 * 24 * 60, + PeriodQuarter: 90 * 24 * 60, + PeriodYear: 365 * 24 * 60, +} + +var periodOrder = []Period{PeriodDay, PeriodWeek, PeriodMonth, PeriodQuarter, PeriodYear} + +type TimeUnit struct { + Minutes float64 + Formatted string +} + +type AvailabilityCalculator struct { + availability float64 +} + +func NewAvailabilityCalculator(availabilityPercentage float64) (*AvailabilityCalculator, error) { + if availabilityPercentage < 0 || availabilityPercentage > 100 { + return nil, fmt.Errorf("availability must be between 0 and 100") + } + return &AvailabilityCalculator{ + availability: availabilityPercentage / 100.0, + }, nil +} + +func (ac *AvailabilityCalculator) CalculateDowntime(period Period) TimeUnit { + totalMinutes := periodMinutes[period] + downtimeMinutes := totalMinutes * (1 - ac.availability) + return TimeUnit{ + Minutes: downtimeMinutes, + Formatted: formatDuration(downtimeMinutes), + } +} + +func (ac *AvailabilityCalculator) CalculateAllPeriods() map[Period]TimeUnit { + result := make(map[Period]TimeUnit) + for _, period := range periodOrder { + result[period] = ac.CalculateDowntime(period) + } + return result +} + +func formatDuration(minutes float64) string { + if minutes < 1 { + return fmt.Sprintf("%.1f seconds", minutes*60) + } + + hours := int(minutes / 60) + remainingMinutes := minutes - float64(hours*60) + + if hours == 0 { + return fmt.Sprintf("%.1f minutes", remainingMinutes) + } else if remainingMinutes == 0 { + return fmt.Sprintf("%d hours", hours) + } else { + return fmt.Sprintf("%d hours %.1f minutes", hours, remainingMinutes) + } +} + +func printTable(headers []string, rows [][]string) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + + for i, header := range headers { + if i > 0 { + fmt.Fprint(w, "\t") + } + fmt.Fprint(w, header) + } + fmt.Fprintln(w) + + for _, row := range rows { + for i, cell := range row { + if i > 0 { + fmt.Fprint(w, "\t") + } + fmt.Fprint(w, cell) + } + fmt.Fprintln(w) + } + + w.Flush() +} + +func analyzeSingle(availability float64, focusPeriod string) error { + calc, err := NewAvailabilityCalculator(availability) + if err != nil { + return err + } + + var periods []Period + if focusPeriod != "" { + periods = []Period{Period(focusPeriod)} + } else { + periods = periodOrder + } + + headers := []string{"Time Period", "Maximum Downtime"} + var rows [][]string + + caser := cases.Title(language.English) + for _, period := range periods { + downtime := calc.CalculateDowntime(period) + rows = append(rows, []string{ + caser.String(string(period)), + downtime.Formatted, + }) + } + + printTable(headers, rows) + return nil +} + +func compareAvailabilities(availability1, availability2 float64) error { + if availability1 == availability2 { + return fmt.Errorf("cannot compare identical availability values") + } + + calc1, err := NewAvailabilityCalculator(availability1) + if err != nil { + return err + } + + calc2, err := NewAvailabilityCalculator(availability2) + if err != nil { + return err + } + + improvement := ((100 - availability1) - (100 - availability2)) / (100 - availability1) * 100 + fmt.Printf("Downtime reduction: %.1f%%\n\n", improvement) + + headers := []string{"Time Period", fmt.Sprintf("%.2f%%", availability1), fmt.Sprintf("%.2f%%", availability2)} + var rows [][]string + + caser := cases.Title(language.English) + for _, period := range periodOrder { + downtime1 := calc1.CalculateDowntime(period) + downtime2 := calc2.CalculateDowntime(period) + rows = append(rows, []string{ + caser.String(string(period)), + downtime1.Formatted, + downtime2.Formatted, + }) + } + + printTable(headers, rows) + return nil +} + +func printUsage() { + fmt.Println("Usage:") + fmt.Println(" pviz analyze - Analyze a single availability target") + fmt.Println(" pviz analyze --focus - Focus on a specific time period") + fmt.Println(" pviz compare - Compare two availability targets") + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" pviz analyze 99.99") + fmt.Println(" pviz analyze 99.99 --focus quarter") + fmt.Println(" pviz compare 99.9 99.95") + fmt.Println() + fmt.Println("Valid periods: day, week, month, quarter, year") +} + +func main() { + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + command := os.Args[1] + + switch command { + case "analyze": + if len(os.Args) < 3 { + fmt.Fprintf(os.Stderr, "Error: analyze command requires availability argument\n") + os.Exit(1) + } + + availability, err := strconv.ParseFloat(os.Args[2], 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: invalid availability value: %v\n", err) + os.Exit(1) + } + + var focusPeriod string + if len(os.Args) >= 5 && os.Args[3] == "--focus" { + focusPeriod = os.Args[4] + validPeriod := false + for _, p := range periodOrder { + if string(p) == focusPeriod { + validPeriod = true + break + } + } + if !validPeriod { + fmt.Fprintf(os.Stderr, "Error: invalid period '%s'. Valid periods: day, week, month, quarter, year\n", focusPeriod) + os.Exit(1) + } + } + + if err := analyzeSingle(availability, focusPeriod); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + case "compare": + if len(os.Args) < 4 { + fmt.Fprintf(os.Stderr, "Error: compare command requires two availability arguments\n") + os.Exit(1) + } + + availability1, err := strconv.ParseFloat(os.Args[2], 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: invalid first availability value: %v\n", err) + os.Exit(1) + } + + availability2, err := strconv.ParseFloat(os.Args[3], 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: invalid second availability value: %v\n", err) + os.Exit(1) + } + + if err := compareAvailabilities(availability1, availability2); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + default: + fmt.Fprintf(os.Stderr, "Error: unknown command '%s'\n", command) + printUsage() + os.Exit(1) + } +} -- cgit v1.2.3