Add grafana

This commit is contained in:
Andras Schmelczer 2026-02-03 21:46:01 +00:00
parent 69de6d75af
commit ba79df5966
3 changed files with 435 additions and 13 deletions

99
server-rs/src/metrics.rs Normal file
View file

@ -0,0 +1,99 @@
use axum::body::Body;
use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use metrics::{counter, gauge, histogram};
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
use std::time::Instant;
/// Initialize the Prometheus metrics exporter and return a handle for rendering metrics.
pub fn init_metrics() -> PrometheusHandle {
PrometheusBuilder::new()
.install_recorder()
.expect("Failed to install Prometheus recorder")
}
/// Middleware to track HTTP request metrics.
pub async fn track_metrics(request: Request<Body>, next: Next) -> Response {
let path = request.uri().path().to_string();
let method = request.method().to_string();
// Skip metrics endpoint itself to avoid recursion
if path == "/metrics" {
return next.run(request).await;
}
let start = Instant::now();
let response = next.run(request).await;
let duration = start.elapsed();
let status = response.status().as_u16().to_string();
// Normalize path for metrics (avoid high cardinality from dynamic segments)
let normalized_path = normalize_path(&path);
// Record metrics
histogram!("http_request_duration_seconds", "method" => method.clone(), "path" => normalized_path.clone(), "status" => status.clone())
.record(duration.as_secs_f64());
counter!("http_requests_total", "method" => method, "path" => normalized_path, "status" => status)
.increment(1);
response
}
/// Normalize paths to avoid high cardinality from dynamic segments.
fn normalize_path(path: &str) -> String {
if path.starts_with("/api/tiles/") && !path.ends_with("style.json") {
return "/api/tiles/:z/:x/:y".to_string();
}
path.to_string()
}
/// Handler for the /metrics endpoint.
pub async fn metrics_handler(handle: PrometheusHandle) -> impl IntoResponse {
// Update process metrics before rendering
update_process_metrics();
match handle.render() {
output if !output.is_empty() => (StatusCode::OK, output),
_ => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to render metrics".to_string()),
}
}
/// Update process-level metrics (memory, etc.)
fn update_process_metrics() {
#[cfg(target_os = "linux")]
{
if let Ok(status) = std::fs::read_to_string("/proc/self/status") {
for line in status.lines() {
if let Some(value) = line.strip_prefix("VmRSS:") {
if let Some(kb) = parse_kb(value) {
gauge!("process_resident_memory_bytes").set((kb * 1024) as f64);
}
} else if let Some(value) = line.strip_prefix("VmSize:") {
if let Some(kb) = parse_kb(value) {
gauge!("process_virtual_memory_bytes").set((kb * 1024) as f64);
}
}
}
}
}
}
#[cfg(target_os = "linux")]
fn parse_kb(value: &str) -> Option<u64> {
value
.trim()
.strip_suffix(" kB")
.or_else(|| value.trim().strip_suffix("kB"))
.and_then(|num| num.trim().parse().ok())
}
/// Record a custom gauge for data loading (call at startup).
pub fn record_data_stats(property_count: usize, poi_count: usize, postcode_count: usize) {
gauge!("data_properties_loaded").set(property_count as f64);
gauge!("data_pois_loaded").set(poi_count as f64);
gauge!("data_postcodes_loaded").set(postcode_count as f64);
}