crabbox/internal/cli/usage.go
2026-05-01 01:53:13 +01:00

103 lines
3.2 KiB
Go

package cli
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
)
func (a App) usage(ctx context.Context, args []string) error {
fs := newFlagSet("usage", a.Stderr)
scope := fs.String("scope", "user", "scope: user, org, or all")
owner := fs.String("user", "", "user email")
org := fs.String("org", "", "org name")
month := fs.String("month", time.Now().UTC().Format("2006-01"), "month: YYYY-MM")
jsonOut := fs.Bool("json", false, "print JSON")
if err := parseFlags(fs, args); err != nil {
return err
}
if *scope != "user" && *scope != "org" && *scope != "all" {
return exit(2, "usage scope must be user, org, or all")
}
cfg, err := loadConfig()
if err != nil {
return err
}
coord, ok, err := newCoordinatorClient(cfg)
if err != nil {
return err
}
if !ok {
return exit(2, "usage requires a configured coordinator")
}
res, err := coord.Usage(ctx, *scope, *owner, *org, *month)
if err != nil {
return err
}
if *jsonOut {
return json.NewEncoder(a.Stdout).Encode(res)
}
printUsageSummary(a.Stdout, res.Usage)
printCostLimits(a.Stdout, res.Limits)
return nil
}
func printUsageSummary(out interface{ Write([]byte) (int, error) }, usage CoordinatorUsageSummary) {
scopeParts := []string{"scope=" + usage.Scope}
if usage.Owner != "" {
scopeParts = append(scopeParts, "user="+usage.Owner)
}
if usage.Org != "" {
scopeParts = append(scopeParts, "org="+usage.Org)
}
fmt.Fprintf(out, "usage month=%s %s\n", usage.Month, strings.Join(scopeParts, " "))
fmt.Fprintf(out, "total leases=%d active=%d runtime=%s estimated=$%.2f reserved=$%.2f\n",
usage.Leases, usage.ActiveLeases, formatDurationSeconds(usage.RuntimeSeconds), usage.EstimatedUSD, usage.ReservedUSD)
printUsageGroups(out, "owners", usage.ByOwner)
printUsageGroups(out, "orgs", usage.ByOrg)
printUsageGroups(out, "providers", usage.ByProvider)
printUsageGroups(out, "server_types", usage.ByServerType)
}
func printUsageGroups(out interface{ Write([]byte) (int, error) }, title string, groups []CoordinatorUsageGroup) {
if len(groups) == 0 {
return
}
fmt.Fprintf(out, "%s:\n", title)
for _, group := range groups {
fmt.Fprintf(out, " %-24s leases=%-3d active=%-3d runtime=%-9s estimated=$%-8.2f reserved=$%.2f\n",
group.Key, group.Leases, group.ActiveLeases, formatDurationSeconds(group.RuntimeSeconds), group.EstimatedUSD, group.ReservedUSD)
}
}
func printCostLimits(out interface{ Write([]byte) (int, error) }, limits CoordinatorCostLimits) {
fmt.Fprintln(out, "limits:")
fmt.Fprintf(out, " active leases: fleet=%s user=%s org=%s\n",
formatIntLimit(limits.MaxActiveLeases), formatIntLimit(limits.MaxActiveLeasesPerOwner), formatIntLimit(limits.MaxActiveLeasesPerOrg))
fmt.Fprintf(out, " monthly usd: fleet=%s user=%s org=%s\n",
formatUSDLimit(limits.MaxMonthlyUSD), formatUSDLimit(limits.MaxMonthlyUSDPerOwner), formatUSDLimit(limits.MaxMonthlyUSDPerOrg))
}
func formatDurationSeconds(seconds int64) string {
if seconds <= 0 {
return "0s"
}
return (time.Duration(seconds) * time.Second).Round(time.Second).String()
}
func formatIntLimit(value int) string {
if value <= 0 {
return "off"
}
return fmt.Sprint(value)
}
func formatUSDLimit(value float64) string {
if value <= 0 {
return "off"
}
return fmt.Sprintf("$%.2f", value)
}