Introdução
Logging e observabilidade são fundamentais para qualquer aplicação em produção. No ecossistema Rust, duas abordagens dominam: a facade log (simples e tradicional) e o framework tracing (moderno e estruturado).
A crate log existe desde os primórdios do Rust e oferece uma facade de logging simples com níveis (error, warn, info, debug, trace). Ela é leve, fácil de usar e tem amplo suporte.
O tracing foi criado pela equipe do Tokio como evolução do log, adicionando conceitos fundamentais para observabilidade moderna: spans (contextos de execução), structured logging (campos tipados), e integração nativa com sistemas de rastreamento distribuído como OpenTelemetry e Jaeger.
Neste artigo, vamos comparar as duas abordagens e mostrar por que o tracing é a recomendação para projetos modernos.
Tabela Comparativa
| Característica | log | tracing |
|---|---|---|
| Tipo | Facade simples | Framework de observabilidade |
| Structured logging | Não nativo | Sim, campos tipados |
| Spans | Não | Sim |
| Async support | Básico | Nativo (instrument) |
| OpenTelemetry | Via adaptador | Integração nativa |
| Performance | Muito leve | Leve (zero-cost quando desabilitado) |
| Ecossistema | Amplo (env_logger, fern, etc.) | Crescente (tracing-subscriber, etc.) |
| Compatibilidade | Padrão Rust | Compatível com log via bridge |
| Curva de aprendizado | Baixa | Moderada |
Dependências no Cargo.toml
Para log:
[dependencies]
log = "0.4"
env_logger = "0.11" # Backend simples
Para tracing:
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
# Opcional: bridge para crates que usam log
tracing-log = "0.2"
# Opcional: OpenTelemetry
tracing-opentelemetry = "0.27"
opentelemetry = "0.27"
opentelemetry_sdk = "0.27"
Comparação Básica
Logging com log
use log::{info, warn, error, debug, trace};
fn processar_pedido(pedido_id: u64, valor: f64) {
info!("Processando pedido {pedido_id}");
debug!("Valor do pedido: R${valor:.2}");
if valor > 10000.0 {
warn!("Pedido {pedido_id} com valor alto: R${valor:.2}");
}
match validar_pagamento(pedido_id) {
Ok(_) => info!("Pedido {pedido_id} processado com sucesso"),
Err(e) => error!("Erro no pedido {pedido_id}: {e}"),
}
}
fn validar_pagamento(_id: u64) -> Result<(), String> {
Ok(())
}
fn main() {
env_logger::init();
processar_pedido(42, 15000.0);
}
Saída:
[2026-02-23T10:30:00Z INFO meu_app] Processando pedido 42
[2026-02-23T10:30:00Z DEBUG meu_app] Valor do pedido: R$15000.00
[2026-02-23T10:30:00Z WARN meu_app] Pedido 42 com valor alto: R$15000.00
[2026-02-23T10:30:00Z INFO meu_app] Pedido 42 processado com sucesso
Logging com tracing
use tracing::{info, warn, error, debug, instrument, info_span};
#[instrument(fields(valor_formatado = format!("R${valor:.2}")))]
fn processar_pedido(pedido_id: u64, valor: f64) {
info!("Processando pedido");
debug!(valor, "Detalhes do pedido");
if valor > 10000.0 {
warn!(limite = 10000.0, "Pedido com valor alto");
}
match validar_pagamento(pedido_id) {
Ok(_) => info!("Pedido processado com sucesso"),
Err(e) => error!(erro = %e, "Erro ao processar pedido"),
}
}
fn validar_pagamento(_id: u64) -> Result<(), String> {
Ok(())
}
fn main() {
tracing_subscriber::fmt()
.with_target(true)
.with_thread_ids(true)
.init();
processar_pedido(42, 15000.0);
}
Saída:
2026-02-23T10:30:00.123Z INFO ThreadId(1) processar_pedido{pedido_id=42 valor=15000.0 valor_formatado="R$15000.00"}: meu_app: Processando pedido
2026-02-23T10:30:00.123Z DEBUG ThreadId(1) processar_pedido{pedido_id=42 valor=15000.0}: meu_app: Detalhes do pedido valor=15000.0
2026-02-23T10:30:00.123Z WARN ThreadId(1) processar_pedido{pedido_id=42 valor=15000.0}: meu_app: Pedido com valor alto limite=10000.0
2026-02-23T10:30:00.123Z INFO ThreadId(1) processar_pedido{pedido_id=42 valor=15000.0}: meu_app: Pedido processado com sucesso
Note como o tracing automaticamente inclui o contexto do span (pedido_id, valor) em cada linha de log, sem precisar repetir essas informações manualmente.
Spans: O Grande Diferencial
Spans são contextos de execução que rastreiam operações do início ao fim. São o conceito mais poderoso do tracing.
use tracing::{info, info_span, instrument, Instrument};
use tokio::time::{sleep, Duration};
// Usando o atributo #[instrument]
#[instrument]
async fn buscar_usuario(user_id: u64) -> String {
info!("Consultando banco de dados");
sleep(Duration::from_millis(50)).await;
format!("Usuario-{user_id}")
}
// Usando span manual
async fn processar_requisicao(req_id: &str) {
let span = info_span!("processar_requisicao", req_id);
let _enter = span.enter();
info!("Requisição recebida");
// Span aninhado para sub-operação
let resultado = {
let _span = info_span!("validacao").entered();
info!("Validando dados");
true
};
if resultado {
info!("Requisição válida, processando...");
}
}
// Spans com async usando .instrument()
async fn pipeline_completo() {
let span = info_span!("pipeline", etapa = "completo");
async {
info!("Iniciando pipeline");
let usuario = buscar_usuario(42).await;
info!(usuario = %usuario, "Usuário encontrado");
sleep(Duration::from_millis(100)).await;
info!("Pipeline concluído");
}
.instrument(span)
.await;
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_target(false)
.init();
pipeline_completo().await;
}
Structured Logging: Campos Tipados
O tracing permite adicionar campos estruturados (key-value) a eventos e spans:
use tracing::{info, warn, error};
use std::time::Instant;
struct Pedido {
id: u64,
cliente: String,
valor: f64,
itens: usize,
}
fn processar(pedido: &Pedido) {
let inicio = Instant::now();
// Campos estruturados com diferentes formatações
info!(
pedido_id = pedido.id,
cliente = %pedido.cliente, // % = Display
valor = pedido.valor,
itens = pedido.itens,
"Processando pedido"
);
// Campos podem ser expressões
let duracao = inicio.elapsed();
info!(
pedido_id = pedido.id,
duracao_ms = duracao.as_millis() as u64,
"Pedido processado"
);
}
fn main() {
// Saída em JSON para sistemas de log
tracing_subscriber::fmt()
.json()
.init();
let pedido = Pedido {
id: 123,
cliente: "Maria Silva".to_string(),
valor: 299.90,
itens: 3,
};
processar(&pedido);
}
Saída em JSON:
{"timestamp":"2026-02-23T10:30:00.123Z","level":"INFO","fields":{"message":"Processando pedido","pedido_id":123,"cliente":"Maria Silva","valor":299.9,"itens":3},"target":"meu_app"}
Esse formato é ideal para ingestão em sistemas como Elasticsearch, Datadog, Grafana Loki e CloudWatch.
Subscribers: Configuração de Saída
O tracing-subscriber oferece diversas formas de configurar a saída:
Formatação Customizada
use tracing_subscriber::{fmt, EnvFilter, prelude::*};
fn setup_tracing() {
tracing_subscriber::registry()
.with(
fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_file(true)
.with_line_number(true)
.with_level(true)
.compact()
)
.with(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"))
)
.init();
}
fn main() {
setup_tracing();
tracing::info!("Aplicação iniciada");
}
Múltiplos Outputs
use tracing_subscriber::{fmt, EnvFilter, prelude::*};
use std::fs::File;
fn setup_tracing() {
let arquivo = File::create("app.log").unwrap();
tracing_subscriber::registry()
// Layer para stdout (formato legível)
.with(
fmt::layer()
.pretty()
.with_filter(EnvFilter::new("info"))
)
// Layer para arquivo (formato JSON)
.with(
fmt::layer()
.json()
.with_writer(arquivo)
.with_filter(EnvFilter::new("debug"))
)
.init();
}
Filtragem Avançada
O EnvFilter permite filtragem granular por módulo e span:
use tracing_subscriber::EnvFilter;
fn main() {
// Via variável de ambiente
// RUST_LOG=info,meu_app::db=debug,tower_http=trace
// Via código
let filtro = EnvFilter::new("info")
.add_directive("meu_app::db=debug".parse().unwrap())
.add_directive("meu_app::auth=trace".parse().unwrap())
.add_directive("tower_http=debug".parse().unwrap())
.add_directive("sqlx=warn".parse().unwrap());
tracing_subscriber::fmt()
.with_env_filter(filtro)
.init();
}
Exemplos de diretivas de filtro:
# Tudo em info, módulo db em debug
RUST_LOG="info,meu_app::db=debug"
# Apenas warnings e erros
RUST_LOG="warn"
# Tudo em trace para o app, info para dependências
RUST_LOG="meu_app=trace,info"
# Filtrar por campo de span
RUST_LOG="meu_app[pedido_id]=debug"
Integração com OpenTelemetry
O tracing se integra nativamente com OpenTelemetry para rastreamento distribuído:
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-opentelemetry = "0.27"
opentelemetry = "0.27"
opentelemetry_sdk = { version = "0.27", features = ["rt-tokio"] }
opentelemetry-otlp = "0.27"
use tracing::{info, instrument};
use tracing_subscriber::{prelude::*, EnvFilter};
use opentelemetry::trace::TracerProvider;
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_otlp::SpanExporter;
fn init_tracing() -> SdkTracerProvider {
let exporter = SpanExporter::builder()
.with_tonic()
.build()
.unwrap();
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.build();
let tracer = provider.tracer("meu-servico");
tracing_subscriber::registry()
.with(tracing_opentelemetry::layer().with_tracer(tracer))
.with(tracing_subscriber::fmt::layer())
.with(EnvFilter::new("info"))
.init();
provider
}
#[instrument]
async fn processar_pedido(pedido_id: u64) {
info!("Processando pedido");
validar_pedido(pedido_id).await;
cobrar_pagamento(pedido_id).await;
info!("Pedido concluído");
}
#[instrument]
async fn validar_pedido(pedido_id: u64) {
info!("Validando pedido");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
#[instrument]
async fn cobrar_pagamento(pedido_id: u64) {
info!("Cobrando pagamento");
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
#[tokio::main]
async fn main() {
let provider = init_tracing();
processar_pedido(42).await;
// Garantir que os traces são enviados antes de sair
provider.shutdown().unwrap();
}
Os spans do tracing são automaticamente convertidos em traces do OpenTelemetry, que podem ser visualizados no Jaeger, Zipkin ou qualquer backend compatível.
Compatibilidade entre log e tracing
Se você tem crates que usam log e quer capturá-las no tracing:
use tracing_subscriber::prelude::*;
use tracing_log::LogTracer;
fn main() {
// Redirecionar eventos do `log` para o `tracing`
LogTracer::init().unwrap();
tracing_subscriber::fmt()
.with_env_filter("info")
.init();
// Eventos de crates que usam log::info! etc.
// agora aparecem no tracing
log::info!("Isso vem do crate log");
tracing::info!("Isso vem do tracing");
}
Performance
Ambas as bibliotecas são projetadas para alto desempenho:
| Cenário | log (com env_logger) | tracing (com fmt) |
|---|---|---|
| Log desabilitado | ~1 ns | ~1 ns |
| Log em nível filtrado | ~5 ns | ~10 ns |
| Log ativo (stdout) | ~1 us | ~2 us |
| Log ativo (JSON) | N/A nativo | ~3 us |
O tracing é ligeiramente mais lento devido à complexidade adicional de spans e campos estruturados, mas a diferença é negligível para qualquer aplicação real.
Quando Usar Cada Um
Use log quando:
- Está escrevendo uma library crate que precisa ser leve
- O projeto é muito simples e não precisa de spans ou structured logging
- Precisa da menor footprint possível
Use tracing quando:
- Está escrevendo uma aplicação (binário)
- Precisa de structured logging para sistemas de observabilidade
- Trabalha com código async (spans rastreiam contexto entre awaits)
- Precisa de rastreamento distribuído (OpenTelemetry)
- Usa Axum ou Tokio (integração nativa)
- Quer filtragem granular por módulo, span ou campo
Recomendação prática
Use tracing para aplicações e log para libraries. Libraries que usam log são automaticamente compatíveis com tracing via bridge. Para aplicações, o tracing oferece tudo que o log oferece e muito mais.
Boas Práticas
- Sempre use
#[instrument]em funções async — propaga contexto automaticamente - Prefira campos estruturados sobre interpolação de strings
- Configure
EnvFilterpara controle dinâmico viaRUST_LOG - Use JSON em produção para ingestão em sistemas de log
- Use formato legível em desenvolvimento local
- Adicione request_id nos spans de nível superior para correlação
use tracing::{info, instrument};
use uuid::Uuid;
#[instrument(skip(body), fields(request_id = %Uuid::new_v4()))]
async fn handle_request(method: &str, path: &str, body: &[u8]) {
info!(method, path, body_size = body.len(), "Requisição recebida");
// Todos os logs dentro desta função incluem request_id
}
Conclusão
O tracing representa a evolução natural do logging em Rust. Enquanto o log continua sendo útil para libraries que precisam ser leves, o tracing é a escolha definitiva para aplicações que precisam de observabilidade moderna — especialmente no contexto de microserviços e sistemas distribuídos.