leaderless gossip-based service discovery w/ consul compatibility
5

Configure Feed

Select the types of activity you want to include in your feed.

implement status

Jes Olson (May 22, 2026, 10:31 PM -0500) f0826821 978cce69

+194 -21
+28
api/agent.go
··· 58 58 return &Agent{c: c} 59 59 } 60 60 61 + // AgentSelf describes the agent the client is talking to. 62 + type AgentSelf struct { 63 + Config struct { 64 + NodeName string 65 + Version string 66 + BuildDate string 67 + DNSBindAddr string 68 + HTTPBindAddr string 69 + SerfBindAddr string 70 + } 71 + } 72 + 73 + // SelfTyped queries the local agent for information about itself. 74 + func (a *Agent) SelfTyped() (*AgentSelf, error) { 75 + r := a.c.newRequest("GET", "/v1/agent/self") 76 + _, resp, err := requireOK(a.c.doRequest(r)) 77 + if err != nil { 78 + return nil, err 79 + } 80 + defer closeResponseBody(resp) 81 + 82 + var out AgentSelf 83 + if err := decodeBody(resp, &out); err != nil { 84 + return nil, err 85 + } 86 + return &out, nil 87 + } 88 + 61 89 // Self is used to query the agent we are speaking to for 62 90 // information about itself 63 91 func (a *Agent) Self() (map[string]map[string]interface{}, error) {
+22 -20
internal/agent/agent_endpoint.go
··· 17 17 Meta map[string]string 18 18 } 19 19 20 - func (s *HTTPHandlers) agentSelf(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 21 - var err error 22 - if err = s.agent.GetCoordinate(); err != nil { 23 - return nil, err 24 - } 25 - 20 + func (s *HTTPHandlers) agentSelf(w http.ResponseWriter, r *http.Request) { 26 21 config := struct { 27 - NodeName string 28 - Version string 29 - BuildDate string 22 + NodeName string 23 + Version string 24 + BuildDate string 25 + DNSBindAddr string 26 + HTTPBindAddr string 27 + SerfBindAddr string 30 28 }{ 31 - NodeName: s.agent.Config.NodeName, 32 - // We expect the ent version to be part 33 - // of the reported version string, and that's 34 - // now part of the metadata, not the actual version. 35 - Version: s.agent.Config.VersionWithMetadata(), 29 + NodeName: s.agent.Config.NodeName, 30 + Version: s.agent.Config.VersionWithMetadata(), 31 + DNSBindAddr: s.agent.Config.DNSBindAddr.String(), 32 + HTTPBindAddr: s.agent.Config.HTTPBindAddr.String(), 33 + SerfBindAddr: s.agent.Config.SerfBindAddr.String(), 36 34 } 37 35 38 - return Self{ 36 + self := Self{ 39 37 Config: config, 40 - // Coord: cs[s.agent.Config.SegmentName], 41 - // Member: s.agent.AgentLocalMember(), 42 - // Stats: s.agent.Stats(), 43 - // Meta: s.agent.State.Metadata(), 44 - }, nil 38 + } 39 + 40 + json, err := s.marshalJSON(r, self) 41 + if err != nil { 42 + fmt.Println(err) 43 + w.WriteHeader(http.StatusInternalServerError) 44 + return 45 + } 46 + fmt.Fprintf(w, string(json)) 45 47 } 46 48 47 49 // return s.agent.Members()
+1
internal/agent/http.go
··· 111 111 mux.HandleFunc("/", s.Index) 112 112 endpoints := map[string]func(resp http.ResponseWriter, req *http.Request){ 113 113 "/v1/agent/members": s.agentMembers, 114 + "/v1/agent/self": s.agentSelf, 114 115 } 115 116 116 117 for pattern, fn := range endpoints {
+140
internal/cli/status.go
··· 1 + package cli 2 + 3 + import ( 4 + "bytes" 5 + "flag" 6 + "fmt" 7 + "os" 8 + "sort" 9 + "text/tabwriter" 10 + "time" 11 + 12 + "git.j3s.sh/cascade/api" 13 + ) 14 + 15 + type statusCommand struct { 16 + flagAPIAddr string 17 + } 18 + 19 + func (c statusCommand) Usage() { 20 + fmt.Printf(`usage: cascade status [flags] 21 + high-level overview of the cascade cluster: member counts, 22 + status breakdown, and agent reachability. 23 + 24 + flags: 25 + -api 26 + address of the cascade http api to target (default = 127.0.0.1:8500) 27 + `) 28 + } 29 + 30 + func (c *statusCommand) Init(args []string) { 31 + flags := flag.NewFlagSet("", flag.ContinueOnError) 32 + flags.Usage = c.Usage 33 + flags.StringVar(&c.flagAPIAddr, "api", "", "") 34 + if err := flags.Parse(args); err != nil { 35 + fmt.Println(err) 36 + os.Exit(1) 37 + } 38 + } 39 + 40 + func RunStatus(args []string) { 41 + c := statusCommand{} 42 + c.Init(args) 43 + 44 + cfg := api.DefaultConfig() 45 + if c.flagAPIAddr != "" { 46 + cfg.Address = c.flagAPIAddr 47 + } 48 + client, err := api.NewClient(cfg) 49 + if err != nil { 50 + fmt.Println(err) 51 + os.Exit(1) 52 + } 53 + 54 + agent := client.Agent() 55 + 56 + start := time.Now() 57 + members, err := agent.Members() 58 + apiRTT := time.Since(start) 59 + if err != nil { 60 + fmt.Printf("error fetching members from %s: %s\n", cfg.Address, err) 61 + os.Exit(1) 62 + } 63 + 64 + self, err := agent.SelfTyped() 65 + nodeName := "<unknown>" 66 + var dnsAddr, httpAddr, serfAddr string 67 + if err == nil { 68 + nodeName = self.Config.NodeName 69 + dnsAddr = self.Config.DNSBindAddr 70 + httpAddr = self.Config.HTTPBindAddr 71 + serfAddr = self.Config.SerfBindAddr 72 + } 73 + 74 + statusCounts := map[string]int{} 75 + tagKeyCounts := map[string]int{} 76 + for _, m := range members { 77 + statusCounts[m.StatusPretty()]++ 78 + for k := range m.Tags { 79 + tagKeyCounts[k]++ 80 + } 81 + } 82 + 83 + total := len(members) 84 + alive := statusCounts["alive"] 85 + healthPct := 0.0 86 + if total > 0 { 87 + healthPct = 100 * float64(alive) / float64(total) 88 + } 89 + 90 + var b bytes.Buffer 91 + tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) 92 + 93 + fmt.Fprintln(tw, "cluster") 94 + fmt.Fprintf(tw, " agent\t%s\n", nodeName) 95 + fmt.Fprintf(tw, " api\t%s\n", cfg.Address) 96 + fmt.Fprintf(tw, " api rtt\t%s\n", apiRTT.Round(time.Microsecond)) 97 + fmt.Fprintf(tw, " members\t%d\n", total) 98 + fmt.Fprintf(tw, " health\t%.1f%% alive (%d/%d)\n", healthPct, alive, total) 99 + fmt.Fprintln(tw) 100 + 101 + if dnsAddr != "" || httpAddr != "" || serfAddr != "" { 102 + fmt.Fprintln(tw, "bind addrs") 103 + fmt.Fprintf(tw, " dns\t%s\n", dnsAddr) 104 + fmt.Fprintf(tw, " http\t%s\n", httpAddr) 105 + fmt.Fprintf(tw, " serf\t%s\n", serfAddr) 106 + fmt.Fprintln(tw) 107 + } 108 + 109 + fmt.Fprintln(tw, "status") 110 + for _, s := range sortedKeys(statusCounts) { 111 + fmt.Fprintf(tw, " %s\t%d\n", s, statusCounts[s]) 112 + } 113 + 114 + if len(tagKeyCounts) > 0 { 115 + fmt.Fprintln(tw) 116 + fmt.Fprintln(tw, "tags") 117 + for _, k := range sortedKeys(tagKeyCounts) { 118 + fmt.Fprintf(tw, " %s\t%d nodes\n", k, tagKeyCounts[k]) 119 + } 120 + } 121 + 122 + if err := tw.Flush(); err != nil { 123 + fmt.Printf("error flushing tabwriter: %s", err) 124 + os.Exit(1) 125 + } 126 + fmt.Print(b.String()) 127 + 128 + if alive < total { 129 + os.Exit(1) 130 + } 131 + } 132 + 133 + func sortedKeys(m map[string]int) []string { 134 + out := make([]string, 0, len(m)) 135 + for k := range m { 136 + out = append(out, k) 137 + } 138 + sort.Strings(out) 139 + return out 140 + }
+3 -1
main.go
··· 28 28 switch os.Args[1] { 29 29 case "agent": 30 30 cli.RunAgent(args) 31 - case "members": 31 + case "members", "list": 32 32 cli.RunMembers(args) 33 + case "status": 34 + cli.RunStatus(args) 33 35 default: 34 36 fmt.Fprintf(os.Stderr, "'%s' is not a valid command\n\n%s", command, usage) 35 37 os.Exit(1)