use prometheus::{ CounterVec, Gauge, Histogram, HistogramVec, Opts, Registry, TextEncoder, Encoder, histogram_opts, }; use std::sync::OnceLock; use std::thread; pub const STATE_IDLE: f64 = 0.0; pub const STATE_ACTIVE: f64 = 1.0; pub const STATE_THROTTLED: f64 = 2.0; pub static METRICS: OnceLock = OnceLock::new(); pub fn get() -> &'static Metrics { METRICS.get().unwrap() } pub struct Metrics { pub state: Gauge, pub images_checked: CounterVec, pub images_updated: CounterVec, pub http_request: CounterVec, pub http_request_duration: HistogramVec, pub run_duration: Gauge, pub git_operations: CounterVec, pub db_query_duration: Histogram, registry: Registry, } impl Metrics { pub fn new() -> Self { let registry = Registry::new(); let state = Gauge::with_opts( Opts::new("vbump_state", "Current state: 0=idle, 1=active, 2=throttled"), ).unwrap(); registry.register(Box::new(state.clone())).unwrap(); let images_checked = CounterVec::new( Opts::new("vbump_images_checked_total", "Total images checked"), &["registry"], ).unwrap(); registry.register(Box::new(images_checked.clone())).unwrap(); let images_updated = CounterVec::new( Opts::new("vbump_images_updated_total", "Total images with newer versions found"), &["registry"], ).unwrap(); registry.register(Box::new(images_updated.clone())).unwrap(); let http_request = CounterVec::new( Opts::new("vbump_http_request_total", "Total HTTP requests to registries"), &["registry"], ).unwrap(); registry.register(Box::new(http_request.clone())).unwrap(); let http_request_duration = HistogramVec::new( histogram_opts!( "vbump_http_request_duration_seconds", "Request latency distribution", vec![0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] ), &["registry"], ).unwrap(); registry.register(Box::new(http_request_duration.clone())).unwrap(); let run_duration = Gauge::with_opts( Opts::new("vbump_run_duration_seconds", "Duration of last complete scan"), ).unwrap(); registry.register(Box::new(run_duration.clone())).unwrap(); let git_operations = CounterVec::new( Opts::new("vbump_git_operations_total", "Git commits/pushes (success/failure)"), &["operation", "outcome"], ).unwrap(); registry.register(Box::new(git_operations.clone())).unwrap(); let db_query_duration = Histogram::with_opts( histogram_opts!( "vbump_query_duration_seconds", "Database query latency distribution", vec![0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5] ), ).unwrap(); registry.register(Box::new(db_query_duration.clone())).unwrap(); return Self { state, images_checked, images_updated, http_request, http_request_duration, run_duration, git_operations, db_query_duration, registry, }; } } pub fn init() { METRICS.get_or_init(Metrics::new); } pub fn serve(port: u16) { let registry = get().registry.clone(); thread::spawn(move || { let addr = format!("0.0.0.0:{}", port); let server = match tiny_http::Server::http(&addr) { Ok(s) => s, Err(e) => { eprintln!("Failed to start metrics server on {}: {}", addr, e); return; } }; println!("Metrics server listening on {}", addr); for request in server.incoming_requests() { let response = if request.url() == "/metrics" { let encoder = TextEncoder::new(); let metric_families = registry.gather(); let mut buffer = Vec::new(); encoder.encode(&metric_families, &mut buffer).unwrap(); tiny_http::Response::from_data(buffer) .with_header( tiny_http::Header::from_bytes( &b"Content-Type"[..], &b"text/plain; version=0.0.4; charset=utf-8"[..], ).unwrap() ) } else { tiny_http::Response::from_string("Not Found") .with_status_code(404) }; let _ = request.respond(response); } }); }