use std::time::Duration;
use prometheus::{Encoder, HistogramVec, IntCounter, IntCounterVec, Opts, Registry, TextEncoder};
use crate::error::{KubeGenericError, Result};
#[derive(Clone, Debug)]
pub struct Metrics {
reconciliations: IntCounter,
errors: IntCounterVec,
reconcile_duration: HistogramVec,
}
impl Metrics {
pub fn new() -> Self {
let reconciliations = IntCounter::new(
"koprs_reconciliations_total",
"Total number of reconciles completed",
)
.expect("static metric options are valid");
let errors = IntCounterVec::new(
Opts::new(
"koprs_reconcile_errors_total",
"Total number of failed reconciles",
),
&["kind", "error"],
)
.expect("static metric options are valid");
let reconcile_duration = HistogramVec::new(
prometheus::HistogramOpts::new(
"koprs_reconcile_duration_seconds",
"Reconcile latency in seconds",
)
.buckets(vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 15.0, 60.0]),
&["kind"],
)
.expect("static metric options are valid");
Self {
reconciliations,
errors,
reconcile_duration,
}
}
pub fn register(self, registry: &Registry) -> Result<Self> {
registry
.register(Box::new(self.reconciliations.clone()))
.map_err(|e| KubeGenericError::Internal(format!("failed to register metrics: {e}")))?;
registry
.register(Box::new(self.errors.clone()))
.map_err(|e| KubeGenericError::Internal(format!("failed to register metrics: {e}")))?;
registry
.register(Box::new(self.reconcile_duration.clone()))
.map_err(|e| KubeGenericError::Internal(format!("failed to register metrics: {e}")))?;
Ok(self)
}
pub fn new_registered(registry: &Registry) -> Result<Self> {
Self::new().register(registry)
}
pub fn record_success(&self, kind: &str, duration: Duration) {
self.reconciliations.inc();
self.reconcile_duration
.with_label_values(&[kind])
.observe(duration.as_secs_f64());
}
pub fn record_failure(&self, kind: &str, error: &str, duration: Duration) {
self.reconciliations.inc();
self.errors.with_label_values(&[kind, error]).inc();
self.reconcile_duration
.with_label_values(&[kind])
.observe(duration.as_secs_f64());
}
}
impl Default for Metrics {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn render(registry: &Registry) -> Result<String> {
let metric_families = registry.gather();
let mut buffer = Vec::new();
TextEncoder::new()
.encode(&metric_families, &mut buffer)
.map_err(|e| KubeGenericError::Internal(format!("failed to encode metrics: {e}")))?;
String::from_utf8(buffer)
.map_err(|e| KubeGenericError::Internal(format!("metrics output was not valid UTF-8: {e}")))
}
pub(crate) async fn serve_metrics(listener: tokio::net::TcpListener, registry: Registry) {
use axum::Router;
use axum::extract::State;
use axum::http::StatusCode;
use axum::routing::get;
async fn metrics_handler(State(registry): State<Registry>) -> (StatusCode, String) {
match render(®istry) {
Ok(body) => (StatusCode::OK, body),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
}
}
let app = Router::new()
.route("/metrics", get(metrics_handler))
.with_state(registry);
let _ = axum::serve(listener, app).await;
}