//! 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, Option) { 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::().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}" ); } }