use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::Arc;
use std::time::Instant;
use kevy_config::Config;
use crate::ops::stats;
pub fn spawn_if_enabled(cfg: &Arc<Config>) {
let port = cfg.metrics.listen_port;
if port == 0 {
return;
}
let cfg_clone: Arc<Config> = Arc::clone(cfg);
std::thread::Builder::new()
.name("kevy-metrics".into())
.spawn(move || run_listener(port, cfg_clone))
.expect("spawn kevy-metrics thread");
}
fn run_listener(port: u16, cfg: Arc<Config>) {
let addr = format!("127.0.0.1:{port}");
let listener = match TcpListener::bind(&addr) {
Ok(l) => l,
Err(e) => {
eprintln!("kevy: metrics endpoint failed to bind {addr}: {e}");
return;
}
};
eprintln!("kevy: metrics endpoint listening on http://{addr}/metrics");
let start = Instant::now();
loop {
let (mut conn, _peer) = match listener.accept() {
Ok(t) => t,
Err(_) => continue,
};
let _ = conn.set_read_timeout(Some(std::time::Duration::from_secs(2)));
let _ = conn.set_write_timeout(Some(std::time::Duration::from_secs(2)));
let mut buf = [0u8; 1024];
let n = match conn.read(&mut buf) {
Ok(n) => n,
Err(_) => continue,
};
let raw = &buf[..n];
let is_metrics = raw.starts_with(b"GET /metrics");
let body = if is_metrics {
render_metrics(&cfg, start.elapsed().as_secs())
} else {
String::new()
};
let resp = if is_metrics {
format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: text/plain; version=0.0.4\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n{}",
body.len(),
body,
)
} else {
"HTTP/1.1 404 Not Found\r\n\
Content-Length: 0\r\n\
Connection: close\r\n\
\r\n"
.to_string()
};
let _ = conn.write_all(resp.as_bytes());
}
}
fn render_metrics(cfg: &Arc<Config>, uptime_seconds: u64) -> String {
let mut out = String::with_capacity(4 * 1024);
let totals = stats::aggregate();
push_help(&mut out, "kevy_uptime_seconds", "Seconds since kevy started");
push_type(&mut out, "kevy_uptime_seconds", "counter");
push_value(&mut out, "kevy_uptime_seconds", uptime_seconds);
push_help(&mut out, "kevy_maxclients", "Configured max client connections");
push_type(&mut out, "kevy_maxclients", "gauge");
push_value(&mut out, "kevy_maxclients", cfg.server.max_clients as u64);
push_help(&mut out, "kevy_used_memory_bytes", "Resident keyspace memory");
push_type(&mut out, "kevy_used_memory_bytes", "gauge");
push_value(&mut out, "kevy_used_memory_bytes", totals.used_memory);
push_help(&mut out, "kevy_used_memory_peak_bytes", "Peak resident keyspace memory");
push_type(&mut out, "kevy_used_memory_peak_bytes", "gauge");
push_value(&mut out, "kevy_used_memory_peak_bytes", totals.used_memory_peak);
push_help(&mut out, "kevy_maxmemory_bytes", "Configured maxmemory ceiling (0 = unlimited)");
push_type(&mut out, "kevy_maxmemory_bytes", "gauge");
push_value(&mut out, "kevy_maxmemory_bytes", cfg.memory.maxmemory);
push_help(&mut out, "kevy_evicted_keys_total", "Keys evicted due to maxmemory");
push_type(&mut out, "kevy_evicted_keys_total", "counter");
push_value(&mut out, "kevy_evicted_keys_total", totals.evicted_keys);
push_help(&mut out, "kevy_expired_keys_total", "Keys expired due to TTL");
push_type(&mut out, "kevy_expired_keys_total", "counter");
push_value(&mut out, "kevy_expired_keys_total", totals.expired_keys);
push_help(&mut out, "kevy_keys_total", "Number of keys across all shards");
push_type(&mut out, "kevy_keys_total", "gauge");
push_value(&mut out, "kevy_keys_total", totals.keys);
push_help(&mut out, "kevy_expires_total", "Number of keys with TTL");
push_type(&mut out, "kevy_expires_total", "gauge");
push_value(&mut out, "kevy_expires_total", totals.expires);
push_help(&mut out, "kevy_build_info", "kevy version (always 1)");
push_type(&mut out, "kevy_build_info", "gauge");
out.push_str(&format!(
"kevy_build_info{{version=\"{}\"}} 1\n",
env!("CARGO_PKG_VERSION")
));
out
}
fn push_help(out: &mut String, name: &str, help: &str) {
out.push_str("# HELP ");
out.push_str(name);
out.push(' ');
out.push_str(help);
out.push('\n');
}
fn push_type(out: &mut String, name: &str, ty: &str) {
out.push_str("# TYPE ");
out.push_str(name);
out.push(' ');
out.push_str(ty);
out.push('\n');
}
fn push_value(out: &mut String, name: &str, value: u64) {
out.push_str(name);
out.push(' ');
out.push_str(&value.to_string());
out.push('\n');
}