The code was formatted by hand, and `cargo fmt --check` differed in 866 places. rustfmt.toml keeps the house's long lines (120) and short expressions on one line, which halves what changes; this commit is only what rustfmt does with it, and is named in .git-blame-ignore-revs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
3.6 KiB
Rust
99 lines
3.6 KiB
Rust
//! Memory, for ipx and the directory both (#224), and the directory's own /metrics.
|
|
//!
|
|
//! mimalloc is the allocator of every binary that builds this crate in, ipx included: with
|
|
//! glibc's, ipx kept the 500-850 MB a night of scans had needed until it restarted, and the
|
|
//! directory sat at 1.25 GiB doing nothing, each thread's arena holding what it had freed.
|
|
//! mimalloc gives freed pages back, so resident memory follows what is in use: a spike that
|
|
//! does not come down again is a leak. Not jemalloc, whose upstream was archived in 2025.
|
|
|
|
use std::sync::atomic::Ordering::Relaxed;
|
|
|
|
#[global_allocator]
|
|
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
|
|
/// The process's resident memory in bytes and its open files, from /proc; None elsewhere.
|
|
pub fn process() -> (Option<f64>, Option<f64>) {
|
|
let rss = std::fs::read_to_string("/proc/self/status").ok().and_then(|s| {
|
|
let kb =
|
|
s.lines().find_map(|l| l.strip_prefix("VmRSS:"))?.trim().strip_suffix("kB")?.trim().parse::<f64>().ok()?;
|
|
Some(kb * 1024.0)
|
|
});
|
|
let fds = std::fs::read_dir("/proc/self/fd").ok().map(|d| d.count() as f64);
|
|
(rss, fds)
|
|
}
|
|
|
|
/// The directory's /metrics, in Prometheus's text format: memory, open files, and the feeds it
|
|
/// has fetched. Nothing from the database, so a scrape costs nothing and answers while it is down.
|
|
fn render(started: std::time::Instant) -> String {
|
|
let mut out = String::new();
|
|
let mut gauge = |name: &str, kind: &str, help: &str, labels: &str, v: f64| {
|
|
out.push_str(&format!("# HELP {name} {help}\n# TYPE {name} {kind}\n{name}{labels} {v}\n"));
|
|
};
|
|
let (rss, fds) = process();
|
|
if let Some(v) = rss {
|
|
gauge("directory_process_resident_bytes", "gauge", "Memory the process holds.", "", v);
|
|
}
|
|
if let Some(v) = fds {
|
|
gauge("directory_process_open_fds", "gauge", "Files and sockets the process has open.", "", v);
|
|
}
|
|
gauge(
|
|
"directory_feed_fetches_total",
|
|
"counter",
|
|
"Feeds fetched, answered 304 included.",
|
|
"",
|
|
ipx_feed::FETCHES.load(Relaxed) as f64,
|
|
);
|
|
gauge(
|
|
"directory_feed_bytes_total",
|
|
"counter",
|
|
"Bytes of feed bodies fetched.",
|
|
"",
|
|
ipx_feed::FETCHED_BYTES.load(Relaxed) as f64,
|
|
);
|
|
gauge(
|
|
"directory_uptime_seconds",
|
|
"gauge",
|
|
"Seconds since the directory started.",
|
|
"",
|
|
started.elapsed().as_secs() as f64,
|
|
);
|
|
gauge(
|
|
"directory_build_info",
|
|
"gauge",
|
|
"The version running, as a label.",
|
|
&format!("{{version=\"{}\"}}", env!("CARGO_PKG_VERSION")),
|
|
1.0,
|
|
);
|
|
out
|
|
}
|
|
|
|
/// Serves /metrics on a listener of its own: the public port is behind the tunnel with no
|
|
/// Access in front, and these are for the LAN.
|
|
pub async fn serve(listener: tokio::net::TcpListener) {
|
|
let started = std::time::Instant::now();
|
|
let app = axum::Router::new()
|
|
.route(
|
|
"/metrics",
|
|
axum::routing::get(move || async move {
|
|
([(axum::http::header::CONTENT_TYPE, "text/plain; version=0.0.4")], render(started))
|
|
}),
|
|
)
|
|
// Prometheus asks for gzip (#253).
|
|
.layer(tower_http::compression::CompressionLayer::new().gzip(true));
|
|
if let Err(e) = axum::serve(listener, app).await {
|
|
tracing::error!(error = %e, "metrics stopped");
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn metrics_say_memory_and_feeds() {
|
|
let text = super::render(std::time::Instant::now());
|
|
assert!(
|
|
text.contains("directory_process_resident_bytes ") && text.contains("directory_feed_bytes_total "),
|
|
"{text}"
|
|
);
|
|
}
|
|
}
|