small pixel drawing of a pufferfish cascade

internal/cli/status.go

package cli

import (
	"bytes"
	"flag"
	"fmt"
	"os"
	"sort"
	"strings"
	"text/tabwriter"
	"time"

	"j3s.sh/cascade/api"
)

type statusCommand struct {
	flagAPIAddr string
}

func (c statusCommand) Usage() {
	fmt.Printf(`usage: cascade status [flags]
    high-level overview of the cascade cluster: member counts,
    status breakdown, and agent reachability.

flags:
  -api
    address of the cascade http api to target (default = 127.0.0.1:8500)
`)
}

func (c *statusCommand) Init(args []string) {
	flags := flag.NewFlagSet("", flag.ContinueOnError)
	flags.Usage = c.Usage
	flags.StringVar(&c.flagAPIAddr, "api", "", "")
	if err := flags.Parse(args); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func RunStatus(args []string) {
	c := statusCommand{}
	c.Init(args)

	cfg := api.DefaultConfig()
	if c.flagAPIAddr != "" {
		cfg.Address = c.flagAPIAddr
	}
	client, err := api.NewClient(cfg)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	agent := client.Agent()

	start := time.Now()
	nodes, err := agent.Members()
	apiRTT := time.Since(start)
	if err != nil {
		fmt.Printf("error fetching nodes from %s: %s\n", cfg.Address, err)
		os.Exit(1)
	}

	self, err := agent.SelfTyped()
	nodeName := "<unknown>"
	var dnsAddr, httpAddr, serfAddr, dnsDomain string
	if err == nil {
		nodeName = self.Config.NodeName
		dnsAddr = self.Config.DNSBindAddr
		httpAddr = self.Config.HTTPBindAddr
		serfAddr = self.Config.SerfBindAddr
		dnsDomain = self.Config.DNSDomain
	}

	statusCounts := map[string]int{}
	tagKeyCounts := map[string]int{}
	for _, n := range nodes {
		statusCounts[n.StatusPretty()]++
		for k := range n.Tags {
			tagKeyCounts[k]++
		}
	}

	localServices, _ := agent.Services()
	catalogServices, _ := client.Catalog().Services()

	total := len(nodes)
	alive := statusCounts["alive"]
	healthPct := 0.0
	if total > 0 {
		healthPct = 100 * float64(alive) / float64(total)
	}

	var b bytes.Buffer
	tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)

	fmt.Fprintln(tw, "cluster")
	fmt.Fprintf(tw, "  agent\t%s\n", nodeName)
	fmt.Fprintf(tw, "  api\t%s\n", cfg.Address)
	fmt.Fprintf(tw, "  api rtt\t%s\n", apiRTT.Round(time.Microsecond))
	fmt.Fprintf(tw, "  nodes\t%d\n", total)
	fmt.Fprintf(tw, "  health\t%.1f%% alive (%d/%d)\n", healthPct, alive, total)
	fmt.Fprintln(tw)

	if dnsAddr != "" || httpAddr != "" || serfAddr != "" {
		fmt.Fprintln(tw, "bind addrs")
		fmt.Fprintf(tw, "  dns\t%s\n", dnsAddr)
		fmt.Fprintf(tw, "  http\t%s\n", httpAddr)
		fmt.Fprintf(tw, "  serf\t%s\n", serfAddr)
		if dnsDomain != "" {
			fmt.Fprintf(tw, "  domain\t.%s\n", dnsDomain)
		}
		fmt.Fprintln(tw)
	}

	fmt.Fprintln(tw, "services")
	fmt.Fprintf(tw, "  local agent\t%s\n", summarizeLocalServices(localServices))
	fmt.Fprintf(tw, "  cluster\t%s\n", summarizeCatalog(catalogServices))
	fmt.Fprintln(tw)

	fmt.Fprintln(tw, "status")
	for _, s := range sortedKeys(statusCounts) {
		fmt.Fprintf(tw, "  %s\t%d\n", s, statusCounts[s])
	}

	if len(tagKeyCounts) > 0 {
		fmt.Fprintln(tw)
		fmt.Fprintln(tw, "tags")
		for _, k := range sortedKeys(tagKeyCounts) {
			fmt.Fprintf(tw, "  %s\t%d nodes\n", k, tagKeyCounts[k])
		}
	}

	if err := tw.Flush(); err != nil {
		fmt.Printf("error flushing tabwriter: %s", err)
		os.Exit(1)
	}
	fmt.Print(b.String())

	if alive < total {
		os.Exit(1)
	}
}

func sortedKeys(m map[string]int) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	sort.Strings(out)
	return out
}

func summarizeLocalServices(services map[string]*api.AgentService) string {
	if len(services) == 0 {
		return "none"
	}
	names := make([]string, 0, len(services))
	for _, s := range services {
		names = append(names, s.Service)
	}
	sort.Strings(names)
	return fmt.Sprintf("%d (%s)", len(names), strings.Join(names, ", "))
}

func summarizeCatalog(services map[string][]string) string {
	if len(services) == 0 {
		return "none"
	}
	return fmt.Sprintf("%d unique services", len(services))
}