Por que observabilidade é um superpoder para o desenvolvedor Rust
Imagine: seu serviço em Rust está rodando em produção, mas de repente a latência dispara para as alturas. Os logs silenciam, as métricas mostram 200 OK, e os usuários reclamam. Familiar? Este é o clássico "caixa preta". Sem observabilidade em Rust, você está cego.
Observabilidade não é apenas "logs + métricas". É a capacidade de fazer qualquer pergunta ao sistema sem precisar escrever novo código. Em Rust, com seu foco em desempenho e confiabilidade, a observabilidade se torna um ingrediente obrigatório para aplicações de nível de produção.
Neste artigo, vamos abordar os três pilares da observabilidade em Rust: tracing, métricas e logs. Você terá exemplos de código prontos com tracing, OpenTelemetry e Prometheus que podem ser implementados imediatamente no seu projeto.
Tracing em Rust: a jornada da requisição pelo sistema
Tracing é a história de vida de uma requisição. Em vez de logs isolados, você obtém um caminho conectado: desde a entrada na API até a consulta ao banco de dados e a resposta ao cliente. Para Rust, a melhor ferramenta é a crate tracing da comunidade Tokio.
Configuração do tracing em uma aplicação Rust
Vamos começar com a configuração básica. Adicione ao Cargo.toml:
[dependencies]tracing = "0.1"tracing-subscriber = "0.3"tracing-opentelemetry = "0.23"opentelemetry = "0.22"opentelemetry-otlp = "0.15"Agora inicializamos o tracer no 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()?;
// Vinculando tracing ao 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(())}Criação de spans e traces
Agora vamos adicionar tracing à lógica de negócio:
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>> { // Simulação de consulta ao 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)}O atributo #[instrument] cria automaticamente um span para a função e registra todos os argumentos. No Jaeger