70 lines
2.2 KiB
Rust
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 })
|
|
}
|