use {axum::{Router,
extract::State,
response::IntoResponse,
routing::get},
metrics::gauge,
metrics_exporter_prometheus::{Matcher,
PrometheusBuilder,
PrometheusHandle},
std::sync::Arc,
tracing::{error,
info}};
#[derive(Debug, Clone)]
pub struct MetricsConfig {
pub enabled: bool,
pub http_buckets: Vec<f64>,
pub custom_buckets: Vec<(String, Vec<f64>)>,
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
enabled: true,
http_buckets: vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
custom_buckets: Vec::new(),
}
}
}
pub struct MetricsGuard {
handle: PrometheusHandle,
}
impl MetricsGuard {
pub fn handle(&self) -> &PrometheusHandle {
&self.handle
}
pub fn render(&self) -> String {
self.handle.render()
}
}
pub fn init_metrics(config: &MetricsConfig) -> Option<MetricsGuard> {
if !config.enabled {
info!("Metrics collection disabled");
return None;
}
let mut builder = PrometheusBuilder::new()
.set_buckets_for_metric(
Matcher::Suffix("http_request_duration_seconds".to_string()),
&config.http_buckets,
)
.ok()?;
for (suffix, buckets) in &config.custom_buckets {
builder = builder
.set_buckets_for_metric(Matcher::Suffix(suffix.clone()), buckets)
.ok()?;
}
match builder.install_recorder() {
| Ok(handle) => {
info!("Metrics exporter initialized");
Some(MetricsGuard { handle })
}
| Err(e) => {
error!(error = %e, "Failed to install Prometheus recorder");
None
}
}
}
pub fn metrics_router(guard: Arc<MetricsGuard>, path: &str) -> Router {
Router::new().route(path, get(metrics_handler)).with_state(guard)
}
async fn metrics_handler(State(guard): State<Arc<MetricsGuard>>) -> impl IntoResponse {
(
[("content-type", "text/plain; version=0.0.4; charset=utf-8")],
guard.render(),
)
}
pub fn record_app_info(service: &'static str, version: &'static str) {
gauge!("app_info", "service" => service, "version" => version).set(1.0);
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = MetricsConfig::default();
assert!(config.enabled);
assert!(!config.http_buckets.is_empty());
assert!(config.custom_buckets.is_empty());
}
#[test]
fn test_disabled_metrics() {
let config = MetricsConfig {
enabled: false,
..Default::default()
};
let guard = init_metrics(&config);
assert!(guard.is_none());
}
}