Introducción: Por qué el monitoreo de aplicaciones Rust es un desafío y una oportunidad
Rust es conocido por su rendimiento y seguridad de memoria. Sin embargo, cuando tu aplicación opera al límite del hardware (por ejemplo, un servidor WebSocket de alta carga o un analizador de datos), las métricas estándar de CPU y RAM a menudo resultan insuficientes. A nivel avanzado, el monitoreo de aplicaciones Rust no solo requiere recopilar datos, sino crear un sistema de alertas preciso que prediga una falla antes de que ocurra.
En este artículo, veremos cómo construir un sistema de monitoreo listo para producción para Rust: desde métricas personalizadas usando el crate prometheus hasta la configuración de alertas a través de Alertmanager y Grafana.
1. Métricas avanzadas: no solo solicitudes HTTP
El monitoreo básico (número de solicitudes, latencias) es el mínimo. A nivel avanzado, recopilamos métricas que reflejan el estado interno de la aplicación: tamaño de colas, número de tareas activas en Tokio, estado del pool de conexiones.
1.1. Métricas personalizadas con tipo Histogram y Gauge
Usamos el crate prometheus. Veamos un ejemplo de recopilación de métricas para un pool de conexiones a la base de datos:
use prometheus::{HistogramOpts, HistogramVec, Gauge, register_histogram_vec, register_gauge};use lazy_static::lazy_static;
lazy_static! { static ref DB_CONNECTION_POOL: Gauge = register_gauge!( "db_connection_pool_size", "Número actual de conexiones activas en el pool" ).unwrap();
static ref DB_QUERY_DURATION: HistogramVec = register_histogram_vec!( HistogramOpts::new("db_query_duration_seconds", "Tiempo de ejecución de consulta a la BD"), &["query_type", "status"] ).unwrap();}
// Uso en el códigofn execute_query(query_type: &str) -> Result<(), String> { DB_CONNECTION_POOL.inc(); let start = std::time::Instant::now(); // ... ejecución de la consulta ... let duration = start.elapsed().as_secs_f64(); DB_QUERY_DURATION.with_label_values(&[query_type, "success"]).observe(duration); DB_CONNECTION_POOL.dec(); Ok(())}Importante: Usa HistogramVec para categorizar las consultas por tipo (SELECT, INSERT) y estado (success, error). Esto permitirá crear mapas de calor en Grafana.
1.2. Métricas desde el interior del runtime de Tokio
Para aplicaciones asíncronas, es crítico conocer la carga del planificador. Usamos tokio-metrics:
use tokio_metrics::RuntimeMetrics;use std::sync::Arc;
async fn monitor_runtime(metrics: Arc<RuntimeMetrics>) { loop { tokio::time::sleep(std::time::Duration::from_secs(5)).await; let num_alive_tasks = metrics.num_alive_tasks(); let blocking_threads = metrics.num_blocking_threads(); // Enviamos estos valores a Prometheus a través de Gauge println!("Tareas activas: {}, Hilos bloqueantes: {}", num_alive_tasks, blocking_threads); }}Estas métricas ayudan a detectar fugas de tareas (cuando num_alive_tasks crece constantemente) o el uso excesivo de block_in_place.
2. Arquitectura de recopilación de métricas: Push vs Pull
En el ecosistema Rust, se usa más el modelo Pull (recolector de Prometheus), pero para procesos de corta duración (utilidades CLI, analizadores) es conveniente el enfoque Push a través de Pushgateway.
2.1. Modelo Pull: Endpoint HTTP
La forma estándar es ejecutar un servidor HTTP separado en el puerto 9090 (u otro) dentro de tu aplicación:
use warp::Filter;use prometheus::{Encoder, TextEncoder};
async fn metrics_handler() -> Result<impl warp::Reply, warp::Rejection> { let metric_families = prometheus::gather(); let mut buffer = Vec::new(); let encoder = TextEncoder::new(); encoder.encode(&metric_families, &mut buffer).unwrap(); Ok(warp::reply::with_header(bu