//! Live network status, scraped from the ircd's Prometheus/OpenMetrics endpoint //! (`metrics_bind` on the daemon). A background task GETs it every minute over //! plain HTTP on localhost — no IRC client, so it never shows up as a connecting //! user. If the endpoint is unreachable the state simply reads "offline". use std::sync::{Arc, RwLock}; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::{sleep, timeout}; #[derive(Clone, Default)] pub struct NetStats { pub online: bool, pub users: Option, pub channels: Option, } pub type Shared = Arc>; /// Spawn the background poller. Refreshes every 60s. pub fn spawn_updater(shared: Shared) { tokio::spawn(async move { loop { let next = match timeout(Duration::from_secs(6), probe()).await { Ok(Some(s)) => s, _ => NetStats::default(), }; if let Ok(mut g) = shared.write() { *g = next; } sleep(Duration::from_secs(60)).await; } }); } async fn probe() -> Option { let addr = std::env::var("ECHO_METRICS_ADDR").unwrap_or_else(|_| "127.0.0.1:9109".into()); let mut stream = TcpStream::connect(&addr).await.ok()?; stream .write_all(b"GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n") .await .ok()?; let mut buf = Vec::with_capacity(4096); let mut chunk = [0u8; 4096]; loop { let n = stream.read(&mut chunk).await.ok()?; if n == 0 || buf.len() > 65536 { break; } buf.extend_from_slice(&chunk[..n]); } let text = String::from_utf8_lossy(&buf); let body = text.split("\r\n\r\n").nth(1).unwrap_or(&text); let (mut users, mut channels) = (None, None); for line in body.lines() { if line.starts_with('#') { continue; } let mut it = line.split_whitespace(); match (it.next(), it.next()) { (Some("echoircd_users"), Some(v)) => users = v.parse().ok(), (Some("echoircd_channels"), Some(v)) => channels = v.parse().ok(), _ => {} } } Some(NetStats { online: true, users, channels }) }