Tracing vs Log Rust: Qual Usar em 2026? | Rust Brasil

Tracing vs log em Rust: structured logging, spans, subscribers e instrumentação. Qual escolher para observabilidade?

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ísticalogtracing
TipoFacade simplesFramework de observabilidade
Structured loggingNão nativoSim, campos tipados
SpansNãoSim
Async supportBásicoNativo (instrument)
OpenTelemetryVia adaptadorIntegração nativa
PerformanceMuito leveLeve (zero-cost quando desabilitado)
EcossistemaAmplo (env_logger, fern, etc.)Crescente (tracing-subscriber, etc.)
CompatibilidadePadrão RustCompatível com log via bridge
Curva de aprendizadoBaixaModerada

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áriolog (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

  1. Sempre use #[instrument] em funções async — propaga contexto automaticamente
  2. Prefira campos estruturados sobre interpolação de strings
  3. Configure EnvFilter para controle dinâmico via RUST_LOG
  4. Use JSON em produção para ingestão em sistemas de log
  5. Use formato legível em desenvolvimento local
  6. 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.

Veja Também