Introdução
Ter um programa que compila e passa nos testes é apenas o começo. Quando sua aplicação está em produção atendendo milhares de requisições, a pergunta muda de “funciona?” para “como está funcionando?”. Logging e observabilidade são os olhos e ouvidos da sua aplicação em produção.
Rust possui um ecossistema maduro de observabilidade, liderado pela crate tracing, que vai muito além de simples println!. Neste artigo, vamos explorar desde logging básico até distributed tracing com OpenTelemetry, métricas com Prometheus e como integrar tudo em um sistema de observabilidade profissional.
O Problema: Debugging em Produção às Cegas
Não Faça Isso: println! como Logging
// ERRADO: println! em produção
fn processar_pedido(id: u64, valor: f64) -> Result<(), String> {
println!("Processando pedido {} com valor {}", id, valor);
if valor > 10000.0 {
println!("AVISO: Pedido de alto valor detectado!");
}
// Simula processamento
println!("Pedido {} processado com sucesso", id);
Ok(())
}
fn main() {
processar_pedido(42, 15000.0).unwrap();
// Problemas:
// - Sem timestamps
// - Sem níveis de log (info, warn, error)
// - Sem contexto estruturado
// - Sem filtro — tudo vai para stdout
// - Sem correlação entre eventos relacionados
}
Não Faça Isso: Logging Sem Estrutura
// ERRADO: Logs como texto livre — impossível de parsear automaticamente
fn processar_pagamento(usuario_id: u64, valor: f64) {
eprintln!(
"[2026-02-23 10:30:45] INFO - Usuário {} fez pagamento de R${:.2}",
usuario_id, valor
);
// Cada desenvolvedor formata de um jeito diferente
// Ferramentas de análise não conseguem extrair campos
}
A Solução: tracing para Observabilidade Completa
Passo 1: Configuração Básica
# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
use tracing::{info, warn, error, debug, instrument};
use tracing_subscriber::{fmt, EnvFilter};
fn configurar_logging() {
let filtro = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(filtro)
.with_target(true) // Mostra o módulo de origem
.with_thread_ids(true) // Mostra o ID da thread
.with_file(true) // Mostra arquivo e linha
.with_line_number(true)
.init();
}
fn main() {
configurar_logging();
info!("Aplicação iniciada");
debug!("Modo debug ativado");
processar_pedido(42, 1500.0);
}
fn processar_pedido(id: u64, valor: f64) {
info!(pedido_id = id, valor = valor, "Processando pedido");
if valor > 10000.0 {
warn!(pedido_id = id, valor = valor, "Pedido de alto valor detectado");
}
info!(pedido_id = id, "Pedido processado com sucesso");
}
Controle o nível de log via variável de ambiente:
# Todos os logs de info para cima
RUST_LOG=info cargo run
# Debug apenas para seu crate
RUST_LOG=meu_app=debug,info cargo run
# Trace para um módulo específico
RUST_LOG=meu_app::pagamento=trace cargo run
Passo 2: Structured Logging com JSON
Para produção, use output JSON que ferramentas como ELK e Datadog conseguem parsear:
use tracing::info;
use tracing_subscriber::{fmt, EnvFilter};
fn configurar_logging_producao() {
let filtro = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(filtro)
.json() // Output em JSON
.with_current_span(true) // Inclui span atual no JSON
.flatten_event(true) // Achata campos do evento
.init();
}
fn main() {
configurar_logging_producao();
info!(
usuario_id = 42,
acao = "login",
ip = "192.168.1.1",
"Usuário autenticado"
);
// Output:
// {"timestamp":"2026-02-23T10:30:45Z","level":"INFO","fields":{"message":"Usuário autenticado","usuario_id":42,"acao":"login","ip":"192.168.1.1"},"target":"meu_app"}
}
Passo 3: Spans para Contexto Hierárquico
Spans agrupam eventos relacionados e medem duração:
use tracing::{info, warn, error, instrument, Span};
#[derive(Debug)]
struct Pedido {
id: u64,
usuario_id: u64,
itens: Vec<String>,
valor_total: f64,
}
#[instrument(skip(pedido), fields(pedido_id = pedido.id, usuario = pedido.usuario_id))]
fn processar_pedido(pedido: &Pedido) -> Result<(), String> {
info!("Iniciando processamento");
validar_estoque(pedido)?;
processar_pagamento(pedido)?;
info!(valor = pedido.valor_total, "Pedido concluído");
Ok(())
}
#[instrument(skip(pedido))]
fn validar_estoque(pedido: &Pedido) -> Result<(), String> {
info!(itens = ?pedido.itens, "Verificando estoque");
// Simula verificação
if pedido.itens.is_empty() {
error!("Pedido sem itens");
return Err("Pedido vazio".into());
}
info!("Estoque validado");
Ok(())
}
#[instrument(skip(pedido), fields(valor = pedido.valor_total))]
fn processar_pagamento(pedido: &Pedido) -> Result<(), String> {
info!("Processando pagamento");
if pedido.valor_total > 50000.0 {
warn!("Pagamento de alto valor — verificação extra necessária");
}
info!("Pagamento aprovado");
Ok(())
}
O macro #[instrument] automaticamente:
- Cria um span com o nome da função
- Registra argumentos como campos (exceto os listados em
skip) - Mede a duração da execução
- Mantém o contexto hierárquico (span pai → span filho)
Passo 4: Logging em Aplicações Async (Tokio)
# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tokio = { version = "1", features = ["full"] }
axum = "0.8"
use axum::{extract::Path, routing::get, Router};
use tracing::{info, instrument};
#[instrument]
async fn buscar_usuario(Path(id): Path<u64>) -> String {
info!("Buscando usuário no banco");
// Simula busca assíncrona
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
info!("Usuário encontrado");
format!("Usuário {id}")
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter("info")
.init();
info!("Servidor iniciando");
let app = Router::new().route("/usuarios/{id}", get(buscar_usuario));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
info!("Escutando em 0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}
Passo 5: OpenTelemetry para Distributed Tracing
# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-opentelemetry = "0.28"
opentelemetry = "0.27"
opentelemetry_sdk = { version = "0.27", features = ["rt-tokio"] }
opentelemetry-otlp = "0.27"
tokio = { version = "1", features = ["full"] }
use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::runtime;
use tracing::info;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
fn init_telemetria() -> Result<(), Box<dyn std::error::Error>> {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint("http://localhost:4317")
.build()?;
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter, runtime::Tokio)
.build();
let tracer = provider.tracer("meu-servico");
let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(EnvFilter::new("info"))
.with(tracing_subscriber::fmt::layer())
.with(telemetry_layer)
.init();
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
init_telemetria()?;
info!("Serviço iniciado com OpenTelemetry");
// Seus spans agora são exportados para Jaeger/Zipkin/etc.
Ok(())
}
Métricas com Prometheus
# Cargo.toml
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
metrics = "0.24"
metrics-exporter-prometheus = "0.16"
use axum::{routing::get, Router};
use metrics::{counter, histogram, gauge};
use metrics_exporter_prometheus::PrometheusBuilder;
use std::time::Instant;
fn configurar_metricas() -> metrics_exporter_prometheus::PrometheusHandle {
PrometheusBuilder::new()
.install_recorder()
.expect("Falha ao instalar recorder de métricas")
}
async fn processar_requisicao() -> &'static str {
let inicio = Instant::now();
// Incrementa contador de requisições
counter!("http_requests_total", "endpoint" => "/api", "method" => "GET")
.increment(1);
// Simula processamento
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
// Registra duração
let duracao = inicio.elapsed().as_secs_f64();
histogram!("http_request_duration_seconds", "endpoint" => "/api")
.record(duracao);
"OK"
}
async fn metricas_handler(
handle: axum::extract::State<metrics_exporter_prometheus::PrometheusHandle>,
) -> String {
handle.render()
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().with_env_filter("info").init();
let handle = configurar_metricas();
// Gauge para conexões ativas (exemplo)
gauge!("conexoes_ativas").set(0.0);
let app = Router::new()
.route("/api", get(processar_requisicao))
.route("/metrics", get(metricas_handler))
.with_state(handle);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
tracing::info!("Servidor com métricas em http://localhost:3000/metrics");
axum::serve(listener, app).await.unwrap();
}
Acesse /metrics para ver a saída no formato Prometheus:
# HELP http_requests_total Total de requisições HTTP
# TYPE http_requests_total counter
http_requests_total{endpoint="/api",method="GET"} 42
# HELP http_request_duration_seconds Duração das requisições HTTP
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{endpoint="/api",le="0.005"} 10
Armadilhas Comuns
1. Logging de Dados Sensíveis
use tracing::info;
// ERRADO: Logando dados sensíveis
fn autenticar(usuario: &str, senha: &str) {
info!(usuario = usuario, senha = senha, "Tentativa de login");
// A senha vai parar nos logs!
}
// CORRETO: Nunca logue dados sensíveis
fn autenticar_seguro(usuario: &str, _senha: &str) {
info!(usuario = usuario, "Tentativa de login");
}
2. Nível de Log Incorreto
use tracing::{debug, info, warn, error};
// Use cada nível corretamente:
fn exemplo_niveis() {
error!("Falha crítica que impede operação"); // Requer ação imediata
warn!("Situação anormal, mas tratável"); // Atenção necessária
info!("Eventos importantes do fluxo normal"); // Operações normais
debug!("Detalhes para debugging"); // Desenvolvimento
// trace! para detalhes muito granulares
}
// ERRADO: Tudo como info
fn logs_ruins() {
info!("Entrando na função X"); // Deveria ser debug ou trace
info!("Conexão ao banco falhou!"); // Deveria ser error
info!("Valor incomum detectado"); // Deveria ser warn
}
3. Logging em Hot Path
use tracing::{debug, info};
// ERRADO: Log em cada iteração de um loop apertado
fn processar_itens(itens: &[u64]) -> u64 {
let mut total = 0;
for &item in itens {
info!(item = item, "Processando item"); // Milhares de logs por segundo!
total += item;
}
total
}
// CORRETO: Log apenas do resultado agregado
fn processar_itens_melhor(itens: &[u64]) -> u64 {
debug!(total_itens = itens.len(), "Iniciando processamento em lote");
let total: u64 = itens.iter().sum();
info!(total_itens = itens.len(), resultado = total, "Lote processado");
total
}
4. Spans Sem Fechamento Adequado
use tracing::{info_span, info, Instrument};
// CORRETO: Span em código assíncrono
async fn operacao_async() {
let span = info_span!("operacao_longa");
async {
info!("Dentro do span");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
info!("Operação concluída");
}
.instrument(span)
.await;
}
Exemplo do Mundo Real: Stack de Observabilidade Completa
# Cargo.toml
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.6", features = ["trace"] }
use axum::{
extract::Path,
routing::get,
Json, Router,
};
use serde::Serialize;
use tower_http::trace::TraceLayer;
use tracing::{info, instrument};
#[derive(Serialize)]
struct Produto {
id: u64,
nome: String,
preco: f64,
}
#[instrument]
async fn listar_produtos() -> Json<Vec<Produto>> {
info!("Listando todos os produtos");
let produtos = vec![
Produto { id: 1, nome: "Teclado".into(), preco: 250.0 },
Produto { id: 2, nome: "Mouse".into(), preco: 150.0 },
];
info!(total = produtos.len(), "Produtos listados");
Json(produtos)
}
#[instrument]
async fn buscar_produto(Path(id): Path<u64>) -> Json<Produto> {
info!("Buscando produto");
Json(Produto {
id,
nome: "Teclado Mecânico".into(),
preco: 450.0,
})
}
#[tokio::main]
async fn main() {
// Configuração: JSON em produção, formatado em desenvolvimento
let is_prod = std::env::var("RUST_ENV")
.map(|v| v == "production")
.unwrap_or(false);
if is_prod {
tracing_subscriber::fmt()
.json()
.with_env_filter("info")
.init();
} else {
tracing_subscriber::fmt()
.pretty()
.with_env_filter("debug")
.init();
}
let app = Router::new()
.route("/produtos", get(listar_produtos))
.route("/produtos/{id}", get(buscar_produto))
.layer(TraceLayer::new_for_http()); // Log automático de requisições HTTP
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
info!("Servidor pronto em http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
Resumo: Os Três Pilares da Observabilidade
| Pilar | Ferramenta Rust | Para Que Serve |
|---|---|---|
| Logs | tracing + tracing-subscriber | Eventos discretos com contexto |
| Traces | tracing + tracing-opentelemetry | Fluxo de execução entre serviços |
| Métricas | metrics + metrics-exporter-prometheus | Agregações numéricas ao longo do tempo |
Veja Também
- Receita: Variáveis de Ambiente — Configure níveis de log via ambiente
- Boas Práticas de Error Handling — Logue erros corretamente
- Segurança em Rust — Evite vazar dados sensíveis nos logs
- CI/CD para Projetos Rust — Configure logging no pipeline de deploy
- Otimização de Performance — Use métricas para identificar gargalos