// // Copyright (c) 2025 murilo ijanc' // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // use std::{ future::ready, time::{Duration, Instant}, }; use axum::{ Router, extract::{MatchedPath, Request}, middleware::Next, response::IntoResponse, routing::get, }; use metrics_exporter_prometheus::{ Matcher, PrometheusBuilder, PrometheusHandle, }; use crate::helpers; pub(crate) async fn start_metrics_server() -> anyhow::Result<()> { let app = metrics_app(); let listener = tokio::net::TcpListener::bind("127.0.0.1:3001").await?; tracing::info!("metrics listening on {}", listener.local_addr()?); axum::serve(listener, app) .with_graceful_shutdown(helpers::shutdown_signal()) .await?; Ok(()) } fn metrics_app() -> Router { let recorder_handle = setup_metrics_recorder(); Router::new() .route("/metrics", get(move || ready(recorder_handle.render()))) } fn setup_metrics_recorder() -> PrometheusHandle { const EXPONENTIAL_SECONDS: &[f64] = &[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]; let recorder_handle = PrometheusBuilder::new() .set_buckets_for_metric( Matcher::Full("http_requests_duration_seconds".to_string()), EXPONENTIAL_SECONDS, ) .unwrap() .install_recorder() .unwrap(); let upkeep_handle = recorder_handle.clone(); tokio::spawn(async move { loop { tokio::time::sleep(Duration::from_secs(5)).await; upkeep_handle.run_upkeep(); } }); recorder_handle } pub(crate) async fn track_metrics( req: Request, next: Next, ) -> impl IntoResponse { let start = Instant::now(); let path = if let Some(matched_path) = req.extensions().get::() { matched_path.as_str().to_owned() } else { req.uri().path().to_owned() }; let method = req.method().clone(); let response = next.run(req).await; let latency = start.elapsed().as_secs_f64(); let status = response.status().as_u16().to_string(); let labels = [("method", method.to_string()), ("path", path), ("status", status)]; metrics::counter!("http_requests_total", &labels).increment(1); metrics::histogram!("http_requests_duration_seconds", &labels) .record(latency); response }