From cc022c3b7ba3b81ac11c89904095826bd3b5e5f3 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Sat, 19 Sep 2026 17:06:00 +0200 Subject: [PATCH] feature: restyle statusz after the classic Google status pages Modelled on Vitess's servenv status page and golang.org/x/net/trace, both written by Google engineers to mirror the internal pages: plain sans-serif HTML, default links and native form controls, dense bordered tables, and a full-width centred light blue (#eef) heading bar per section. Header shows "Status for ferrix_bgp" with start time, uptime, version, router ID and AS on the left and host, "View:" links and live-update state on the right. The dark theme is dropped on purpose. Behaviour is unchanged (live updates, owner-gated withdraw, per-session UPDATE bytes, looped-route highlight, peers polling). Additions: - a SwissIX large-community picker next to the field; presets with a peer AS ask for the number - event log lines include prepend and MED - /api/status gains hostname and started_at_unix (RouterInfo::new) Co-Authored-By: Claude Fable 5.1 --- bin/bgp/src/main.rs | 12 +- crates/integration-tests/src/test_server.rs | 7 +- crates/integration-tests/tests/http_api.rs | 4 +- crates/web/src/api.rs | 4 + crates/web/src/lib.rs | 32 +- crates/web/static/statusz.html | 389 ++++++++++---------- 6 files changed, 242 insertions(+), 206 deletions(-) diff --git a/bin/bgp/src/main.rs b/bin/bgp/src/main.rs index 9eb5c5f..14d96cb 100644 --- a/bin/bgp/src/main.rs +++ b/bin/bgp/src/main.rs @@ -1,5 +1,4 @@ use std::sync::Arc; -use std::time::Instant; use bgp_server::config::Config; use bgp_server::proto::config::BgpConfig; @@ -34,12 +33,11 @@ async fn main() -> Result<()> { if let Some(http_endpoint) = server.config().http_endpoint { let state = AppState { - info: Arc::new(RouterInfo { - router_id: server.config().router_id, - local_asn: server.config().local_asn, - version: env!("CARGO_PKG_VERSION").to_string(), - started: Instant::now(), - }), + info: Arc::new(RouterInfo::new( + server.config().router_id, + server.config().local_asn, + env!("CARGO_PKG_VERSION"), + )), registry: server.registry.clone(), local_rib: server.local_rib.clone(), adj_rib_in: server.adj_rib_in.clone(), diff --git a/crates/integration-tests/src/test_server.rs b/crates/integration-tests/src/test_server.rs index de48c1b..47382fe 100644 --- a/crates/integration-tests/src/test_server.rs +++ b/crates/integration-tests/src/test_server.rs @@ -87,12 +87,7 @@ impl TestServer { let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; let base_url = format!("http://{}", listener.local_addr()?); let state = AppState { - info: std::sync::Arc::new(RouterInfo { - router_id: Ipv4Addr::new(127, 0, 0, 1), - local_asn: self.local_asn, - version: "test".into(), - started: std::time::Instant::now(), - }), + info: std::sync::Arc::new(RouterInfo::new(Ipv4Addr::new(127, 0, 0, 1), self.local_asn, "test")), registry: self.registry.clone(), local_rib: self.local_rib.clone(), adj_rib_in: self.adj_rib_in.clone(), diff --git a/crates/integration-tests/tests/http_api.rs b/crates/integration-tests/tests/http_api.rs index 6eeb15d..4c18cee 100644 --- a/crates/integration-tests/tests/http_api.rs +++ b/crates/integration-tests/tests/http_api.rs @@ -49,6 +49,8 @@ async fn test_status_and_peers() -> Result<()> { assert_eq!(status["local_asn"], LOCAL_ASN); assert_eq!(status["router_id"], "127.0.0.1"); assert_eq!(status["sessions"], 0); + assert!(status["started_at_unix"].as_u64().unwrap() > 1_700_000_000); + assert!(!status["hostname"].as_str().unwrap().is_empty()); let _peer = established_peer(&server).await?; @@ -281,7 +283,7 @@ async fn test_statusz_page_is_served() -> Result<()> { assert_eq!(resp.status(), 200); assert!(resp.headers()["content-type"].to_str()?.starts_with("text/html")); let body = resp.text().await?; - assert!(body.contains("ferrix_bgp /statusz")); + assert!(body.contains("Status for ferrix_bgp")); assert!(body.contains("/api/events")); // The root redirects to the page. diff --git a/crates/web/src/api.rs b/crates/web/src/api.rs index e353730..6dc151e 100644 --- a/crates/web/src/api.rs +++ b/crates/web/src/api.rs @@ -69,6 +69,8 @@ pub struct StatusView { pub router_id: String, pub local_asn: u32, pub version: String, + pub hostname: String, + pub started_at_unix: u64, pub uptime_secs: u64, pub sessions: usize, pub local_routes: usize, @@ -335,6 +337,8 @@ pub async fn status(State(state): State) -> Json { router_id: state.info.router_id.to_string(), local_asn: state.info.local_asn, version: state.info.version.clone(), + hostname: state.info.hostname.clone(), + started_at_unix: unix_secs(state.info.started_at), uptime_secs: state.info.started.elapsed().as_secs(), sessions: state.registry.list().len(), local_routes: state.local_rib.snapshot().await.len(), diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs index 1e190db..7659077 100644 --- a/crates/web/src/lib.rs +++ b/crates/web/src/lib.rs @@ -12,7 +12,7 @@ mod metrics; use std::net::{Ipv4Addr, SocketAddr}; use std::sync::Arc; -use std::time::Instant; +use std::time::{Instant, SystemTime}; use axum::Router; use axum::http::header; @@ -36,6 +36,36 @@ pub struct RouterInfo { pub local_asn: u32, pub version: String, pub started: Instant, + /// Wall-clock start time, for display. + pub started_at: SystemTime, + /// Name of the machine we run on, for display. + pub hostname: String, +} + +impl RouterInfo { + pub fn new(router_id: Ipv4Addr, local_asn: u32, version: impl Into) -> Self { + Self { + router_id, + local_asn, + version: version.into(), + started: Instant::now(), + started_at: SystemTime::now(), + hostname: hostname(), + } + } +} + +/// Best-effort host name without pulling in a dependency: ask the `hostname` +/// utility, which exists on every platform we run on. +fn hostname() -> String { + std::process::Command::new("hostname") + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "unknown host".into()) } #[derive(Clone)] diff --git a/crates/web/static/statusz.html b/crates/web/static/statusz.html index e98be1d..e84b2fa 100644 --- a/crates/web/static/statusz.html +++ b/crates/web/static/statusz.html @@ -1,155 +1,143 @@ - + -ferrix_bgp /statusz - +Status for ferrix_bgp + + -
-

ferrix_bgp

- router-id … - AS … - version … - uptime … - sessions … - local … - received … - connecting -
- -
-
-

Peers

- - - -
namepeerASNstateopened bylocal address (next hop)establisheduptime4-byte ASNdetail
-
+

Status for ferrix_bgp

-
-

Announce a prefix

-
- - - - - - - -
-
-
-
+
+
+Started: ... (up ...)
+Version ...
+Router ID ..., local AS ...
+
+
+Running on ...
+View: variables, status json, peers json, local routes json, received routes json
+Live updates: connecting
+
+
-
-

Local routes

- - - -
prefixowneroriginAS pathMEDcommunitieslarge communitiesage
-
+

Peers (...)

+ + + +
namepeerASstateopened bylocal address (next hop)establisheduptime4-byte ASdetail
-
-

Received routes (Adj-RIB-In)

-

Rows highlighted in yellow carry a prefix that is also announced locally: a route that has travelled through the lab and come back.

- - - -
prefixfrom peerAS pathnext hoporiginMEDcommunitieslarge communitiesage
-
+

Announce a prefix

+
+ + + + + +
+  
+ + + + +
+ + comma separated, ASN:VALUE or a name
+ + GLOBAL:LOCAL1:LOCAL2
+  
+
-
-

Event log

-
-
+

Local routes (...)

+ + + +
prefixowneroriginAS pathMEDcommunitieslarge communitiesageactions
-
-
ferrix_bgp · Adj-RIB-In and local routes are live via /api/events · tables refresh on every event.
+

Received routes, Adj-RIB-In (...)

+
A yellow row carries a prefix that is also announced locally: it has travelled through other routers and come back.
+ + + +
prefixfrom peerAS pathnext hoporiginMEDcommunitieslarge communitiesage
+ +

Event log

+
+ +
ferrix_bgp · /statusz /metricz /api/events · tables refresh on every event