stats: scrape the ircd metrics endpoint over http (no irc client, no snote spam); add channels
This commit is contained in:
parent
fc129a1f1b
commit
149ab69afe
3 changed files with 37 additions and 64 deletions
|
|
@ -20,15 +20,18 @@ use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||||
struct Status {
|
struct Status {
|
||||||
online: bool,
|
online: bool,
|
||||||
users: String,
|
users: String,
|
||||||
|
channels: String,
|
||||||
version: String,
|
version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(shared: &stats::Shared) -> Status {
|
fn view(shared: &stats::Shared) -> Status {
|
||||||
let s = shared.read().ok().map(|g| g.clone()).unwrap_or_default();
|
let s = shared.read().ok().map(|g| g.clone()).unwrap_or_default();
|
||||||
|
let fmt = |n: Option<u64>| n.map(|v| v.to_string()).unwrap_or_else(|| "—".into());
|
||||||
Status {
|
Status {
|
||||||
online: s.online,
|
online: s.online,
|
||||||
users: s.users.map(|n| n.to_string()).unwrap_or_else(|| "—".into()),
|
users: fmt(s.users),
|
||||||
version: s.version.unwrap_or_else(|| "5.0.0".into()),
|
channels: fmt(s.channels),
|
||||||
|
version: "5.0.0".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
91
src/stats.rs
91
src/stats.rs
|
|
@ -1,11 +1,11 @@
|
||||||
//! Live network status, polled from the running ircd. A background task connects
|
//! Live network status, scraped from the ircd's Prometheus/OpenMetrics endpoint
|
||||||
//! to the local daemon as an ordinary client every minute, runs LUSERS/VERSION,
|
//! (`metrics_bind` on the daemon). A background task GETs it every minute over
|
||||||
//! parses the reply, and stores it. If the daemon is unreachable the state simply
|
//! plain HTTP on localhost — no IRC client, so it never shows up as a connecting
|
||||||
//! reads "offline" — it never blocks or breaks page rendering.
|
//! user. If the endpoint is unreachable the state simply reads "offline".
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::{sleep, timeout};
|
use tokio::time::{sleep, timeout};
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ use tokio::time::{sleep, timeout};
|
||||||
pub struct NetStats {
|
pub struct NetStats {
|
||||||
pub online: bool,
|
pub online: bool,
|
||||||
pub users: Option<u64>,
|
pub users: Option<u64>,
|
||||||
pub version: Option<String>,
|
pub channels: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Shared = Arc<RwLock<NetStats>>;
|
pub type Shared = Arc<RwLock<NetStats>>;
|
||||||
|
|
@ -22,7 +22,7 @@ pub type Shared = Arc<RwLock<NetStats>>;
|
||||||
pub fn spawn_updater(shared: Shared) {
|
pub fn spawn_updater(shared: Shared) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let next = match timeout(Duration::from_secs(7), probe()).await {
|
let next = match timeout(Duration::from_secs(6), probe()).await {
|
||||||
Ok(Some(s)) => s,
|
Ok(Some(s)) => s,
|
||||||
_ => NetStats::default(),
|
_ => NetStats::default(),
|
||||||
};
|
};
|
||||||
|
|
@ -35,67 +35,36 @@ pub fn spawn_updater(shared: Shared) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn probe() -> Option<NetStats> {
|
async fn probe() -> Option<NetStats> {
|
||||||
let addr = std::env::var("ECHO_IRCD_ADDR").unwrap_or_else(|_| "127.0.0.1:6667".into());
|
let addr = std::env::var("ECHO_METRICS_ADDR").unwrap_or_else(|_| "127.0.0.1:9109".into());
|
||||||
let stream = TcpStream::connect(&addr).await.ok()?;
|
let mut stream = TcpStream::connect(&addr).await.ok()?;
|
||||||
let (rd, mut wr) = stream.into_split();
|
stream
|
||||||
wr.write_all(b"NICK echostat\r\nUSER echostat 0 * :echo website status\r\n")
|
.write_all(b"GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||||
.await
|
.await
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let mut lines = BufReader::new(rd).lines();
|
let mut buf = Vec::with_capacity(4096);
|
||||||
let (mut users, mut version, mut asked) = (None, None, false);
|
let mut chunk = [0u8; 4096];
|
||||||
|
loop {
|
||||||
for _ in 0..100 {
|
let n = stream.read(&mut chunk).await.ok()?;
|
||||||
let line = match lines.next_line().await {
|
if n == 0 || buf.len() > 65536 {
|
||||||
Ok(Some(l)) => l,
|
|
||||||
_ => break,
|
|
||||||
};
|
|
||||||
let line = line.trim_end_matches('\r');
|
|
||||||
if let Some(rest) = line.strip_prefix("PING ") {
|
|
||||||
let _ = wr.write_all(format!("PONG {rest}\r\n").as_bytes()).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match numeric(line) {
|
|
||||||
Some("001") if !asked => {
|
|
||||||
let _ = wr.write_all(b"LUSERS\r\nVERSION\r\n").await;
|
|
||||||
asked = true;
|
|
||||||
}
|
|
||||||
Some("251") => users = users.or_else(|| parse_users(line)),
|
|
||||||
Some("351") => version = version.or_else(|| parse_version(line)),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if users.is_some() && version.is_some() {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
buf.extend_from_slice(&chunk[..n]);
|
||||||
}
|
}
|
||||||
let _ = wr.write_all(b"QUIT :bye\r\n").await;
|
let text = String::from_utf8_lossy(&buf);
|
||||||
Some(NetStats { online: true, users, version })
|
let body = text.split("\r\n\r\n").nth(1).unwrap_or(&text);
|
||||||
}
|
|
||||||
|
|
||||||
/// The three-digit numeric of an IRC line (`:src 251 nick …`), if any.
|
let (mut users, mut channels) = (None, None);
|
||||||
fn numeric(line: &str) -> Option<&str> {
|
for line in body.lines() {
|
||||||
let mut it = line.split(' ');
|
if line.starts_with('#') {
|
||||||
let first = it.next()?;
|
continue;
|
||||||
let code = if first.starts_with(':') { it.next()? } else { first };
|
|
||||||
(code.len() == 3 && code.bytes().all(|b| b.is_ascii_digit())).then_some(code)
|
|
||||||
}
|
}
|
||||||
|
let mut it = line.split_whitespace();
|
||||||
/// RPL_LUSERCLIENT trailing: "There are 5 users on 1 server" → first number.
|
match (it.next(), it.next()) {
|
||||||
fn parse_users(line: &str) -> Option<u64> {
|
(Some("echoircd_users"), Some(v)) => users = v.parse().ok(),
|
||||||
line.splitn(2, " :")
|
(Some("echoircd_channels"), Some(v)) => channels = v.parse().ok(),
|
||||||
.nth(1)?
|
_ => {}
|
||||||
.split_whitespace()
|
|
||||||
.find_map(|w| w.parse::<u64>().ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RPL_VERSION trailing: "echoircd 5.0.0 · …" → "5.0.0".
|
|
||||||
fn parse_version(line: &str) -> Option<String> {
|
|
||||||
let tail = line.splitn(2, " :").nth(1)?;
|
|
||||||
let mut it = tail.split_whitespace();
|
|
||||||
let a = it.next()?;
|
|
||||||
if a.eq_ignore_ascii_case("echoircd") {
|
|
||||||
it.next().map(|v| v.to_string())
|
|
||||||
} else {
|
|
||||||
Some(a.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(NetStats { online: true, users, channels })
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
<dl class="sc-rows">
|
<dl class="sc-rows">
|
||||||
<div><dt>state</dt><dd>{% if status.online %}<b class="live">online</b>{% else %}<b class="down">offline</b>{% endif %}</dd></div>
|
<div><dt>state</dt><dd>{% if status.online %}<b class="live">online</b>{% else %}<b class="down">offline</b>{% endif %}</dd></div>
|
||||||
<div><dt>users</dt><dd>{{ status.users }}</dd></div>
|
<div><dt>users</dt><dd>{{ status.users }}</dd></div>
|
||||||
|
<div><dt>channels</dt><dd>{{ status.channels }}</dd></div>
|
||||||
<div><dt>network</dt><dd>echoiRCd</dd></div>
|
<div><dt>network</dt><dd>echoiRCd</dd></div>
|
||||||
<div><dt>version</dt><dd>echoIRCd {{ status.version }}</dd></div>
|
<div><dt>version</dt><dd>echoIRCd {{ status.version }}</dd></div>
|
||||||
<div><dt>tls</dt><dd>1.3 · X25519MLKEM768</dd></div>
|
<div><dt>tls</dt><dd>1.3 · X25519MLKEM768</dd></div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue