use std::sync::atomic::{AtomicU64, Ordering};
static HTTP_REQUESTS: AtomicU64 = AtomicU64::new(0);
static HTTP_CLIENT_ERRORS: AtomicU64 = AtomicU64::new(0);
static HTTP_SERVER_ERRORS: AtomicU64 = AtomicU64::new(0);
static FILTER_DENIED: AtomicU64 = AtomicU64::new(0);
static KWT_AUTH_FAILURES: AtomicU64 = AtomicU64::new(0);
static KWT_AUTH_SUCCESSES: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn metrics_enabled(raw: Option<&str>) -> bool {
!matches!(raw, Some("0") | Some("false") | Some("no"))
}
#[must_use]
pub fn enabled_from_env() -> bool {
metrics_enabled(std::env::var("NAUTALID_METRICS").ok().as_deref())
}
pub fn record_http_status(status: u16) {
HTTP_REQUESTS.fetch_add(1, Ordering::Relaxed);
if (400..500).contains(&status) {
HTTP_CLIENT_ERRORS.fetch_add(1, Ordering::Relaxed);
} else if status >= 500 {
HTTP_SERVER_ERRORS.fetch_add(1, Ordering::Relaxed);
}
}
pub fn record_filter_denied() {
FILTER_DENIED.fetch_add(1, Ordering::Relaxed);
}
pub fn record_kwt_auth_failure() {
KWT_AUTH_FAILURES.fetch_add(1, Ordering::Relaxed);
}
pub fn record_kwt_auth_success() {
KWT_AUTH_SUCCESSES.fetch_add(1, Ordering::Relaxed);
}
fn counter(name: &str, help: &str, value: u64) -> String {
format!("# HELP {name} {help}\n# TYPE {name} counter\n{name} {value}\n")
}
fn gauge(name: &str, help: &str, value: u64) -> String {
format!("# HELP {name} {help}\n# TYPE {name} gauge\n{name} {value}\n")
}
pub fn render_prometheus(
bus_ready: bool,
kwt_replay_entries: Option<usize>,
kwt_replay_backend: Option<&str>,
detailed: bool,
) -> String {
let mut out = String::new();
out.push_str(&counter(
"nautalid_http_requests_total",
"Total HTTP responses served",
HTTP_REQUESTS.load(Ordering::Relaxed),
));
out.push_str(&counter(
"nautalid_http_client_errors_total",
"HTTP 4xx responses",
HTTP_CLIENT_ERRORS.load(Ordering::Relaxed),
));
out.push_str(&counter(
"nautalid_http_server_errors_total",
"HTTP 5xx responses",
HTTP_SERVER_ERRORS.load(Ordering::Relaxed),
));
out.push_str(&counter(
"nautalid_filter_denied_total",
"Requests rejected by the filter layer",
FILTER_DENIED.load(Ordering::Relaxed),
));
out.push_str(&counter(
"nautalid_kwt_auth_failures_total",
"Failed KWT validations on protected routes",
KWT_AUTH_FAILURES.load(Ordering::Relaxed),
));
out.push_str(&counter(
"nautalid_kwt_auth_successes_total",
"Successful KWT validations on protected routes",
KWT_AUTH_SUCCESSES.load(Ordering::Relaxed),
));
out.push_str(&gauge(
"nautalid_bus_ready",
"Control-plane bus bound and accepting (1=yes, 0=no)",
u64::from(bus_ready),
));
if detailed {
if let Some(entries) = kwt_replay_entries {
out.push_str(&gauge(
"nautalid_kwt_replay_entries",
"In-memory JTI replay cache size",
entries as u64,
));
}
out.push_str(&format!(
"# HELP nautalid_build_info Static build metadata\n\
# TYPE nautalid_build_info gauge\n\
nautalid_build_info{{version=\"{}\",kwt_replay_backend=\"{}\"}} 1\n",
env!("CARGO_PKG_VERSION"),
kwt_replay_backend.unwrap_or("none")
));
} else {
out.push_str(&format!(
"# HELP nautalid_build_info Static build metadata\n\
# TYPE nautalid_build_info gauge\n\
nautalid_build_info{{version=\"{}\"}} 1\n",
env!("CARGO_PKG_VERSION"),
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_includes_version_and_counters() {
let body = render_prometheus(true, Some(3), Some("memory"), true);
assert!(body.contains("nautalid_build_info"));
assert!(body.contains(env!("CARGO_PKG_VERSION")));
assert!(body.contains("nautalid_kwt_replay_entries 3"));
assert!(body.contains("nautalid_bus_ready 1"));
}
#[test]
fn render_redacts_sensitive_when_not_detailed() {
let body = render_prometheus(true, Some(3), Some("memory"), false);
assert!(!body.contains("kwt_replay_backend"));
assert!(!body.contains("nautalid_kwt_replay_entries"));
assert!(body.contains("nautalid_build_info"));
}
#[test]
fn metrics_disabled_by_env_zero() {
assert!(!metrics_enabled(Some("0")));
assert!(!metrics_enabled(Some("false")));
assert!(metrics_enabled(None));
assert!(metrics_enabled(Some("1")));
}
}