summaryrefslogtreecommitdiff
path: root/src/metrics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/metrics.rs')
-rw-r--r--src/metrics.rs147
1 files changed, 147 insertions, 0 deletions
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<Metrics> = 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);
+ }
+ });
+}