website/src/stats.rs

70 lines
2.2 KiB
Rust

//! 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<u64>,
pub channels: Option<u64>,
}
pub type Shared = Arc<RwLock<NetStats>>;
/// 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<NetStats> {
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 })
}