From c2687b44edb4da3b258f4e28c6add8713c50bcec Mon Sep 17 00:00:00 2001 From: Jesper Jensen Date: Fri, 6 Feb 2026 19:34:39 +0100 Subject: Add metrics --- src/metrics.rs | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/metrics.rs (limited to 'src/metrics.rs') diff --git a/src/metrics.rs b/src/metrics.rs new file mode 100644 index 0000000..09617c4 --- /dev/null +++ b/src/metrics.rs @@ -0,0 +1,147 @@ +use prometheus::{ + CounterVec, Gauge, 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 rate_limits: CounterVec, + pub auth_attempts: CounterVec, + pub run_duration: Gauge, + pub git_operations: CounterVec, + 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", "outcome"], + ).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 rate_limits = CounterVec::new( + Opts::new("vbump_rate_limits_total", "Rate limit (429) responses received"), + &["registry"], + ).unwrap(); + registry.register(Box::new(rate_limits.clone())).unwrap(); + + let auth_attempts = CounterVec::new( + Opts::new("vbump_auth_attempts_total", "Auth attempts (success/failure)"), + &["registry", "outcome"], + ).unwrap(); + registry.register(Box::new(auth_attempts.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(); + + Self { + state, + images_checked, + images_updated, + http_request, + http_request_duration, + rate_limits, + auth_attempts, + run_duration, + git_operations, + 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); + } + }); +} -- cgit v1.2.3