implement status
Jes Olson j3s@c3f.net
Fri, 22 May 2026 22:31:46 -0500
5 files changed,
194 insertions(+),
21 deletions(-)
M
api/agent.go
→
api/agent.go
@@ -58,6 +58,34 @@ func (c *Client) Agent() *Agent {
return &Agent{c: c} } +// AgentSelf describes the agent the client is talking to. +type AgentSelf struct { + Config struct { + NodeName string + Version string + BuildDate string + DNSBindAddr string + HTTPBindAddr string + SerfBindAddr string + } +} + +// SelfTyped queries the local agent for information about itself. +func (a *Agent) SelfTyped() (*AgentSelf, error) { + r := a.c.newRequest("GET", "/v1/agent/self") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + defer closeResponseBody(resp) + + var out AgentSelf + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return &out, nil +} + // Self is used to query the agent we are speaking to for // information about itself func (a *Agent) Self() (map[string]map[string]interface{}, error) {
M
internal/agent/agent_endpoint.go
→
internal/agent/agent_endpoint.go
@@ -17,31 +17,33 @@ Stats map[string]map[string]string
Meta map[string]string } -func (s *HTTPHandlers) agentSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - var err error - if err = s.agent.GetCoordinate(); err != nil { - return nil, err - } - +func (s *HTTPHandlers) agentSelf(w http.ResponseWriter, r *http.Request) { config := struct { - NodeName string - Version string - BuildDate string + NodeName string + Version string + BuildDate string + DNSBindAddr string + HTTPBindAddr string + SerfBindAddr string }{ - NodeName: s.agent.Config.NodeName, - // We expect the ent version to be part - // of the reported version string, and that's - // now part of the metadata, not the actual version. - Version: s.agent.Config.VersionWithMetadata(), + NodeName: s.agent.Config.NodeName, + Version: s.agent.Config.VersionWithMetadata(), + DNSBindAddr: s.agent.Config.DNSBindAddr.String(), + HTTPBindAddr: s.agent.Config.HTTPBindAddr.String(), + SerfBindAddr: s.agent.Config.SerfBindAddr.String(), } - return Self{ + self := Self{ Config: config, - // Coord: cs[s.agent.Config.SegmentName], - // Member: s.agent.AgentLocalMember(), - // Stats: s.agent.Stats(), - // Meta: s.agent.State.Metadata(), - }, nil + } + + json, err := s.marshalJSON(r, self) + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + fmt.Fprintf(w, string(json)) } // return s.agent.Members()
M
internal/agent/http.go
→
internal/agent/http.go
@@ -111,6 +111,7 @@
mux.HandleFunc("/", s.Index) endpoints := map[string]func(resp http.ResponseWriter, req *http.Request){ "/v1/agent/members": s.agentMembers, + "/v1/agent/self": s.agentSelf, } for pattern, fn := range endpoints {
A
internal/cli/status.go
@@ -0,0 +1,140 @@
+package cli + +import ( + "bytes" + "flag" + "fmt" + "os" + "sort" + "text/tabwriter" + "time" + + "git.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() + members, err := agent.Members() + apiRTT := time.Since(start) + if err != nil { + fmt.Printf("error fetching members from %s: %s\n", cfg.Address, err) + os.Exit(1) + } + + self, err := agent.SelfTyped() + nodeName := "<unknown>" + var dnsAddr, httpAddr, serfAddr string + if err == nil { + nodeName = self.Config.NodeName + dnsAddr = self.Config.DNSBindAddr + httpAddr = self.Config.HTTPBindAddr + serfAddr = self.Config.SerfBindAddr + } + + statusCounts := map[string]int{} + tagKeyCounts := map[string]int{} + for _, m := range members { + statusCounts[m.StatusPretty()]++ + for k := range m.Tags { + tagKeyCounts[k]++ + } + } + + total := len(members) + 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, " members\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) + 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 +}
M
main.go
→
main.go
@@ -28,8 +28,10 @@ args := os.Args[2:]
switch os.Args[1] { case "agent": cli.RunAgent(args) - case "members": + case "members", "list": cli.RunMembers(args) + case "status": + cli.RunStatus(args) default: fmt.Fprintf(os.Stderr, "'%s' is not a valid command\n\n%s", command, usage) os.Exit(1)