diff --git a/cmd/cluster.go b/cmd/cluster.go index bee561d3..fb048e63 100644 --- a/cmd/cluster.go +++ b/cmd/cluster.go @@ -132,20 +132,17 @@ func clusterStatusBaseURL(listen string) (string, error) { } func printClusterStatus(w io.Writer, status *cluster.Status) { - leader, healthy := "not leader", "healthy" + // This node first, then its peers; the link columns are the peers' only + self, live := string(status.NodeID)+" (this node)", "yes" if status.Leader { - leader = "leader" + self = string(status.NodeID) + " (this node, leader)" } if !status.Healthy { - healthy = "NOT healthy (registry heartbeat stale)" - } - fmt.Fprintf(w, "Node %s: %s, %s, %s\n\n", status.NodeID, status.Role, leader, healthy) - if len(status.Peers) == 0 { - fmt.Fprintln(w, "No peers.") - return + live = "no" } tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) - fmt.Fprintln(tw, "PEER\tROLE\tLIVE\tHEARTBEAT\tSENT OK\tFAILED\tQUEUED\tDROPPED\tLAST SENT\tRECEIVED\tLAST RECEIVED") + fmt.Fprintln(tw, "NODE\tROLE\tLIVE\tHEARTBEAT\tSENT OK\tFAILED\tQUEUED\tDROPPED\tLAST SENT\tRECEIVED\tLAST RECEIVED") + fmt.Fprintf(tw, "%s\t%s\t%s\t-\t-\t-\t-\t-\t-\t-\t-\n", self, status.Role, live) for _, p := range status.Peers { live, heartbeat := "no", "last seen "+clusterStatusAge(status.Time, p.LastSeen) if p.Live { @@ -156,6 +153,9 @@ func printClusterStatus(w io.Writer, status *cluster.Status) { } tw.Flush() problems := clusterStatusProblems(status, "") + if !status.Healthy { + problems = append([]string{fmt.Sprintf("%s (this node): not healthy, its registry heartbeat is stale", status.NodeID)}, problems...) + } if len(problems) == 0 { return } diff --git a/cmd/cluster_test.go b/cmd/cluster_test.go index 5f3cd964..50495f07 100644 --- a/cmd/cluster_test.go +++ b/cmd/cluster_test.go @@ -84,6 +84,21 @@ func TestCLI_Cluster_Status(t *testing.T) { require.Contains(t, out, "nyc3-app-1: no batch accepted") // The warning that matters } +func TestCLI_Cluster_Status_ThisNodeInTheList(t *testing.T) { + // The node itself is the table's first row, not a line above it; a leader and an unhealthy node say so + status := &cluster.Status{NodeID: "nyc3-app-1", Role: cluster.RoleActive, Leader: true, Healthy: false, Time: 1792330000, Peers: []*cluster.PeerStatus{{ + NodeID: "nyc3-app-2", Role: cluster.RolePassive, Live: true, HeartbeatAge: 1, Out: cluster.PeerOutbound{BatchesOK: 7, LastOK: 1792329999}, + }}} + addr := newTestClusterStatusServerFor(t, func() *cluster.Status { return status }) + app, _, stdout, _ := newTestApp() + require.Nil(t, app.Run([]string{"ntfy", "cluster", "--experimental-cluster-listen", addr, "--experimental-cluster-secret", "s3cret", "status"})) + lines := strings.Split(strings.TrimSpace(stdout.String()), "\n") + require.True(t, strings.HasPrefix(lines[0], "NODE "), "the table comes first:\n%s", stdout.String()) + require.Regexp(t, `^nyc3-app-1 \(this node, leader\)\s+active\s+no\s`, lines[1]) + require.Regexp(t, `^nyc3-app-2\s+passive\s+yes\s+1s ago\s+7\s`, lines[2]) + require.Contains(t, stdout.String(), "nyc3-app-1 (this node): not healthy, its registry heartbeat is stale") +} + func TestCLI_Cluster_Status_JSON(t *testing.T) { addr := newTestClusterStatusServer(t) app, _, stdout, _ := newTestApp() diff --git a/docs/config.md b/docs/config.md index df8661fc..8acc17c4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -2376,15 +2376,14 @@ once. `/v1/health` only covers a node's own database heartbeat. A link between two nodes that works in one direction only, e.g. because a firewall drops the cluster port one way, keeps both nodes healthy while messages published on one of them reach the other only late, through the database. To see each node's view of its peers, run `ntfy cluster status` on the -node. It reads `experimental-cluster-listen` and `experimental-cluster-secret` from the server config, and shows per -peer the message batches sent there and received from there: +node. It reads `experimental-cluster-listen` and `experimental-cluster-secret` from the server config, and lists the +node itself (its role, and whether it leads), then per peer the message batches sent there and received from there: ``` $ ntfy cluster status -Node nyc3-app-2: passive, not leader, healthy - -PEER ROLE LIVE HEARTBEAT SENT OK FAILED QUEUED DROPPED LAST SENT RECEIVED LAST RECEIVED -nyc3-app-1 active yes 2s ago 0 219 0 0 never 5120 1s ago +NODE ROLE LIVE HEARTBEAT SENT OK FAILED QUEUED DROPPED LAST SENT RECEIVED LAST RECEIVED +nyc3-app-2 (this node) passive yes - - - - - - - - +nyc3-app-1 active yes 2s ago 0 219 0 0 never 5120 1s ago Problems: nyc3-app-1: no batch accepted, 219 failed; last error 10s ago: Post "http://10.108.0.2:2587/v1/cluster/message": context deadline exceeded diff --git a/docs/releases.md b/docs/releases.md index 7ac6d1ea..36a5ba2f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2356,6 +2356,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** * Do not send TCP keep-alive probes on the client listeners (`listen-http`, `listen-https`): Go sends one every 15 seconds on an idle connection by default, which, without a proxy in front of ntfy, woke phones holding a stream three times as often as ntfy's own keepalive event ([#2152](https://github.com/binwiederhier/ntfy/pull/2152)) +* `ntfy cluster status` lists the node itself as the first row of the table (marked "this node", and "leader" if it leads) instead of a line above it ([#2153](https://github.com/binwiederhier/ntfy/pull/2153)) ### ntfy iOS app v1.8.0 (UNRELEASED)