Por qué la observabilidad es una superpotencia para el desarrollador de Rust
Imagine: su servicio en Rust funciona en producción, pero de repente la latencia se dispara. Los registros están en silencio, las métricas muestran 200 OK y los usuarios se quejan. ¿Le suena familiar? Esto es la clásica «caja negra». Sin observabilidad en Rust, está ciego.
Observabilidad no es solo «registros + métricas». Es la capacidad de hacer cualquier pregunta al sistema sin necesidad de escribir nuevo código. En Rust, con su énfasis en rendimiento y confiabilidad, la observabilidad se convierte en un ingrediente obligatorio para aplicaciones de grado de producción.
En este artículo, analizaremos los tres pilares de la observabilidad en Rust: trazado, métricas y registros. Obtendrá ejemplos de código listos para usar con tracing, OpenTelemetry y Prometheus que puede implementar de inmediato en su proyecto.
Trazado en Rust: el viaje de una solicitud a través del sistema
Trazado (tracing) es la historia de vida de una sola solicitud. En lugar de registros dispersos, obtiene una ruta vinculada: desde la entrada a la API hasta la consulta a la base de datos y la respuesta al cliente. Para Rust, la mejor herramienta es el crate tracing de la comunidad Tokio.
Configuración de tracing en una aplicación Rust
Comencemos con la configuración básica. Agregue a Cargo.toml:
[dependencies]tracing = "0.1"tracing-subscriber = "0.3"tracing-opentelemetry = "0.23"opentelemetry = "0.22"opentelemetry-otlp = "0.15"Ahora inicialicemos el trazador en main.rs:
use tracing_subscriber::{fmt, prelude::*, EnvFilter};use opentelemetry::{global, KeyValue};use opentelemetry_otlp::WithExportConfig;
fn init_tracer() -> Result<(), Box<dyn std::error::Error>> { global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new()); let tracer = opentelemetry_otlp::new_pipeline() .tracing() .with_exporter( opentelemetry_otlp::new_exporter() .tonic() .with_endpoint("http://localhost:4317") ) .with_trace_config( opentelemetry::sdk::trace::config() .with_resource(opentelemetry::sdk::Resource::new(vec![ KeyValue::new("service.name", "rust-observability-demo") ])) ) .install_simple()?;
// Vinculamos tracing con OpenTelemetry let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); let subscriber = fmt::Subscriber::builder() .with_env_filter(EnvFilter::from_default_env()) .finish() .with(telemetry);
tracing::subscriber::set_global_default(subscriber)?; Ok(())}Creación de spans y traces
Ahora agreguemos trazado a la lógica de negocio:
use tracing::{span, Span, Level, instrument};
#[instrument]async fn handle_request(user_id: u64) -> Result<String, Box<dyn std::error::Error>> { let user_data = get_user_data(user_id).await?; let processed = process_data(&user_data).await?; Ok(processed)}
#[instrument(skip(db_pool))]async fn get_user_data(user_id: u64) -> Result<User, Box<dyn std::error::Error>> { // Simulación de consulta a BD let span = Span::current(); span.record("db.query", "SELECT * FROM users WHERE id = $1"); tokio::time::sleep(std::time::Duration::from_millis(50)).await; Ok(User { id: user_id, name: "Alice".into() })}
#[instrument]async fn process_data(user: &User) -> Result<String, Box<dyn std::error::Error>> { let result = format!("Processed user: {}", user.name); tracing::info!(user_id = user.id, "Data processed successfully"); Ok(result)}El atributo #[instrument] crea automáticamente un span para la función y registra todos los argumentos. En Jaege