diff --git a/core/observability.go b/core/observability.go index 63971df2..98e99440 100644 --- a/core/observability.go +++ b/core/observability.go @@ -44,6 +44,7 @@ func (n *Nylon) startObservability() error { mux.HandleFunc("/readyz", n.handleReady) mux.HandleFunc("/metrics", n.handleMetrics) mux.HandleFunc("/discovery", n.handleDiscovery) + mux.HandleFunc("/status", n.handleStatusJSON) obs := &observabilityServer{ listener: listener, @@ -131,6 +132,23 @@ func (n *Nylon) handleMetrics(w http.ResponseWriter, r *http.Request) { writePrometheusMetrics(w, status) } +func (n *Nylon) handleStatusJSON(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), observabilityTimeout) + defer cancel() + status, err := n.statusSnapshot(ctx) + if err != nil { + http.Error(w, "status unavailable", http.StatusServiceUnavailable) + return + } + data, err := pjMarshal.Marshal(status) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(data) +} + func (n *Nylon) handleDiscovery(w http.ResponseWriter, _ *http.Request) { _, port, err := net.SplitHostPort(n.LocalCfg.ObservabilityAddr) if err != nil { diff --git a/core/observability_test.go b/core/observability_test.go index 9e017e99..41e5a031 100644 --- a/core/observability_test.go +++ b/core/observability_test.go @@ -84,3 +84,14 @@ func TestPrometheusMetrics(t *testing.T) { require.NotContains(t, output, `nylon_wireguard_peer_latest_handshake_seconds{peer="bob"} 1.786`) require.Equal(t, 1, strings.Count(output, "# HELP nylon_wireguard_peer_transmit_bytes_total ")) } + +func TestObservabilityStatusUnavailable(t *testing.T) { + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(nil) + // nothing runs the dispatch loop, so the snapshot times out + n := &Nylon{Context: ctx, DispatchChannel: make(chan func() error)} + + rec := httptest.NewRecorder() + n.handleStatusJSON(rec, httptest.NewRequest(http.MethodGet, "/status", nil)) + require.Equal(t, http.StatusServiceUnavailable, rec.Code) +} diff --git a/docs/guides/observability.mdx b/docs/guides/observability.mdx index aa94ad29..ce8bef81 100644 --- a/docs/guides/observability.mdx +++ b/docs/guides/observability.mdx @@ -21,6 +21,7 @@ Restart nylon after changing the node configuration. You can then check the loca curl http://127.0.0.1:9090/healthz curl http://127.0.0.1:9090/readyz curl http://127.0.0.1:9090/metrics +curl http://127.0.0.1:9090/status ``` :::caution @@ -34,6 +35,7 @@ The observability server does not provide authentication or TLS. Bind it to a tr | `/healthz` | Returns `200 OK` while the daemon is running and `503 Service Unavailable` while it is shutting down. | | `/readyz` | Returns `200 OK` when nylon's internal event loop responds, or `503 Service Unavailable` if it cannot respond within one second. It does not check whether every peer is reachable. | | `/metrics` | Returns metrics in the Prometheus text exposition format. Returns `503 Service Unavailable` if nylon cannot collect a status snapshot within one second. | +| `/status` | Returns the node's status as JSON, the same object `nylon status --json` prints under `status`. Returns `503 Service Unavailable` if nylon cannot collect a snapshot within one second. | | `/discovery` | Returns Prometheus HTTP service-discovery targets for the addresses in `central.yaml`. | ## Configure Prometheus diff --git a/docs/reference/config.mdx b/docs/reference/config.mdx index afc1e4bb..388d36b4 100644 --- a/docs/reference/config.mdx +++ b/docs/reference/config.mdx @@ -22,7 +22,7 @@ no_net_configure: false # if true, nylon won't touch system routes or interfaces log_path: "" # write logs to this file (empty = stderr only) interface_name: "" # override the interface name (default: "nylon", or utunX on macOS) dns_resolvers: [] # DNS servers for nylon's own lookups, e.g. ["1.1.1.1:53"] -observability_addr: "" # e.g. "0.0.0.0:9090"; enables /metrics, /healthz, /readyz, and /discovery +observability_addr: "" # e.g. "0.0.0.0:9090"; enables /metrics, /healthz, /readyz, /discovery, and /status # Bootstrap: fetch central.yaml from a remote bundle on first start dist: