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
-
- | name | peer | ASN | state | opened by | local address (next hop) | established | uptime | 4-byte ASN | detail |
-
-
-
+Status for ferrix_bgp
-
+
+
+Started: ... (up ...)
+Version ...
+Router ID ..., local AS ...
+
+
+
-
- Local routes
-
- | prefix | owner | origin | AS path | MED | communities | large communities | age | |
-
-
-
+Peers (...)
+
+| name | peer | AS | state | opened by | local address (next hop) | established | uptime | 4-byte AS | detail |
+
+
-
- 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.
-
- | prefix | from peer | AS path | next hop | origin | MED | communities | large communities | age |
-
-
-
+Announce a prefix
+
-
+Local routes (...)
+
+| prefix | owner | origin | AS path | MED | communities | large communities | age | actions |
+
+
-
-
+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.
+
+| prefix | from peer | AS path | next hop | origin | MED | communities | large communities | age |
+
+
+
+Event log
+
+
+ferrix_bgp · /statusz /metricz /api/events · tables refresh on every event