Tracing: Observabilidade Estruturada para Rust

Guia completo do Tracing em Rust: spans, events, subscribers, macro #[instrument], tracing-subscriber, filtragem com EnvFilter, JSON logging para produção, integração com OpenTelemetry e exemplos práticos.

Introdução

O Tracing é o framework de observabilidade padrão do ecossistema Rust. Diferente de sistemas de logging tradicionais que registram mensagens de texto isoladas, o Tracing trabalha com spans (intervalos de tempo) e events (pontos no tempo), permitindo rastrear o fluxo de execução de forma estruturada e hierárquica.

Isso é especialmente poderoso em aplicações assíncronas, onde uma requisição pode saltar entre tasks e threads. O Tracing mantém o contexto da execução mesmo nesses cenários, permitindo que você correlacione logs, métricas e traces de forma coerente.

O ecossistema Tracing é composto por várias crates complementares:

  • tracing: a API de instrumentação (spans, events, macros)
  • tracing-subscriber: implementações de subscribers (formatadores, filtros)
  • tracing-appender: escrita em arquivos com rotação
  • tracing-opentelemetry: exportação para OpenTelemetry

Por que usar o Tracing?

  • Estruturado: dados são campos tipados, não apenas texto
  • Hierárquico: spans aninhados mostram o fluxo de execução
  • Async-aware: funciona corretamente com async/await
  • Performático: overhead mínimo quando desabilitado
  • Extensível: subscribers customizáveis para qualquer destino
  • Ecossistema rico: JSON, OpenTelemetry, Jaeger, Datadog, etc.
  • Padrão da indústria: usado por Tokio, Axum, Hyper, Tonic e praticamente todo o ecossistema

Instalação

Adicione o Tracing ao seu Cargo.toml:

[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

Para uma configuração mais completa:

[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [
    "env-filter",    # Filtragem via RUST_LOG
    "json",          # Output em JSON
    "fmt",           # Formatação de texto
    "time",          # Timestamps
] }
tracing-appender = "0.2"     # Escrita em arquivos
tracing-opentelemetry = "0.27" # OpenTelemetry
opentelemetry = "0.27"
opentelemetry-otlp = "0.27"

Uso Básico

Configuração mínima

use tracing::{info, warn, error, debug, trace};

fn main() {
    // Inicializar o subscriber padrão
    tracing_subscriber::fmt::init();

    info!("Aplicação iniciada");
    debug!("Modo debug ativo");
    warn!("Este é um aviso");
    error!("Este é um erro");

    // Eventos com campos estruturados
    info!(
        usuario = "maria",
        acao = "login",
        "Usuário fez login"
    );

    // Campos numéricos
    info!(
        latencia_ms = 42,
        status = 200,
        metodo = "GET",
        caminho = "/api/usuarios",
        "Requisição processada"
    );
}

// Output:
// 2026-02-27T10:30:00.000Z  INFO app: Aplicação iniciada
// 2026-02-27T10:30:00.001Z  INFO app: Usuário fez login usuario="maria" acao="login"

Spans: rastreando intervalos de tempo

use tracing::{info, info_span, warn};

fn main() {
    tracing_subscriber::fmt::init();

    // Criar um span manualmente
    let span = info_span!("processamento", tarefa_id = 42);
    let _guard = span.enter();

    info!("Iniciando processamento");
    processar_dados();
    info!("Processamento concluído");
}

fn processar_dados() {
    // Span aninhado
    let span = info_span!("validacao", etapa = "dados");
    let _guard = span.enter();

    info!("Validando dados de entrada");

    // Outro nível de aninhamento
    let span = info_span!("transformacao");
    let _guard = span.enter();

    info!("Transformando dados");
    warn!("Campo opcional ausente, usando valor padrão");
}

// Output mostra a hierarquia:
// INFO processamento{tarefa_id=42}: Iniciando processamento
// INFO processamento{tarefa_id=42}:validacao{etapa="dados"}: Validando dados de entrada
// INFO processamento{tarefa_id=42}:validacao{etapa="dados"}:transformacao: Transformando dados
// WARN processamento{tarefa_id=42}:validacao{etapa="dados"}:transformacao: Campo opcional ausente

A macro #[instrument]

A forma mais ergonômica de instrumentar funções:

use tracing::{info, instrument, warn};

#[instrument]
fn processar_pedido(pedido_id: u64, cliente: &str) {
    info!("Processando pedido");
    validar_estoque(pedido_id);
    calcular_frete(pedido_id, "SP");
    info!("Pedido processado com sucesso");
}

#[instrument(skip(pedido_id))] // Não incluir pedido_id nos campos do span
fn validar_estoque(pedido_id: u64) {
    info!("Verificando estoque");
}

#[instrument(fields(regiao = %destino))] // Campo customizado
fn calcular_frete(pedido_id: u64, destino: &str) {
    info!(valor = 15.90, "Frete calculado");
}

fn main() {
    tracing_subscriber::fmt::init();

    processar_pedido(12345, "Maria");
}

// Output:
// INFO processar_pedido{pedido_id=12345 cliente="Maria"}: Processando pedido
// INFO processar_pedido{pedido_id=12345 cliente="Maria"}:validar_estoque: Verificando estoque
// INFO processar_pedido{pedido_id=12345 cliente="Maria"}:calcular_frete{regiao="SP"}: Frete calculado valor=15.9

Instrument com funções async

use tracing::{info, instrument};

#[instrument]
async fn buscar_usuario(id: u64) -> Result<String, String> {
    info!("Buscando no banco de dados");
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
    Ok(format!("Usuário {}", id))
}

#[instrument]
async fn processar_requisicao(usuario_id: u64) {
    info!("Iniciando processamento");

    match buscar_usuario(usuario_id).await {
        Ok(usuario) => info!(usuario = %usuario, "Usuário encontrado"),
        Err(e) => tracing::error!(erro = %e, "Falha ao buscar usuário"),
    }
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    processar_requisicao(42).await;
}

Recursos Avançados

Configuração detalhada do subscriber

use tracing_subscriber::{fmt, prelude::*, EnvFilter};

fn configurar_tracing_desenvolvimento() {
    tracing_subscriber::fmt()
        // Formato legível para desenvolvimento
        .pretty()
        // Mostrar nível do evento
        .with_level(true)
        // Mostrar nome do target (módulo)
        .with_target(true)
        // Mostrar timestamps
        .with_timer(fmt::time::ChronoLocal::new("%H:%M:%S%.3f".to_string()))
        // Mostrar número da linha
        .with_line_number(true)
        // Mostrar nome do arquivo
        .with_file(true)
        // Filtro padrão
        .with_env_filter(EnvFilter::new("debug"))
        .init();
}

fn configurar_tracing_producao() {
    tracing_subscriber::fmt()
        // JSON para produção (fácil de parsear por ferramentas)
        .json()
        // Timestamps ISO 8601
        .with_timer(fmt::time::ChronoUtc::rfc_3339())
        // Incluir spans atuais
        .with_current_span(true)
        // Incluir hierarquia de spans
        .with_span_list(true)
        // Filtro via variável RUST_LOG
        .with_env_filter(
            EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| EnvFilter::new("info")),
        )
        .init();
}

fn main() {
    let ambiente = std::env::var("APP_ENV").unwrap_or_else(|_| "dev".to_string());

    if ambiente == "producao" {
        configurar_tracing_producao();
    } else {
        configurar_tracing_desenvolvimento();
    }

    tracing::info!("Aplicação iniciada no ambiente: {}", ambiente);
}

Filtragem com EnvFilter

use tracing_subscriber::EnvFilter;

fn exemplos_filtros() {
    // Filtro simples por nível
    let _ = EnvFilter::new("info");

    // Filtro por módulo
    let _ = EnvFilter::new("minha_app=debug,hyper=warn,tower=info");

    // Filtro complexo
    let _ = EnvFilter::new(
        "info,minha_app=trace,minha_app::db=debug,sqlx=warn,hyper=error"
    );

    // Filtro com spans específicos
    let _ = EnvFilter::new("info,[requisicao]=debug");

    // Via variável de ambiente RUST_LOG
    let _ = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info"));
}

// Uso na linha de comando:
// RUST_LOG=debug cargo run
// RUST_LOG=minha_app=trace,sqlx=warn cargo run
// RUST_LOG="info,[requisicao{id=42}]=trace" cargo run

JSON logging para produção

use tracing::{info, instrument};
use tracing_subscriber::fmt;

fn configurar_json_logging() {
    tracing_subscriber::fmt()
        .json()
        // Flatten campos de eventos e spans
        .flatten_event(true)
        // Incluir span hierárquico
        .with_current_span(true)
        .with_span_list(true)
        // Timestamp
        .with_timer(fmt::time::ChronoUtc::rfc_3339())
        .init();
}

#[instrument(fields(request_id = %uuid::Uuid::new_v4()))]
async fn handler_exemplo(metodo: &str, caminho: &str) {
    info!(
        metodo = metodo,
        caminho = caminho,
        "Requisição recebida"
    );

    // Simular processamento
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;

    info!(
        status = 200,
        latencia_ms = 10,
        "Resposta enviada"
    );
}

#[tokio::main]
async fn main() {
    configurar_json_logging();

    handler_exemplo("GET", "/api/usuarios").await;
}

// Output JSON (uma linha por evento):
// {"timestamp":"2026-02-27T10:30:00.000Z","level":"INFO","message":"Requisição recebida","metodo":"GET","caminho":"/api/usuarios","request_id":"a1b2c3d4-...","target":"app"}
// {"timestamp":"2026-02-27T10:30:00.010Z","level":"INFO","message":"Resposta enviada","status":200,"latencia_ms":10,"request_id":"a1b2c3d4-...","target":"app"}

Logging em arquivo com rotação

use tracing_appender::rolling;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

fn configurar_log_arquivo() {
    // Rotação diária de arquivos de log
    let file_appender = rolling::daily("logs", "app.log");
    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

    // Combinar stdout e arquivo
    let stdout_layer = fmt::layer()
        .pretty()
        .with_filter(EnvFilter::new("info"));

    let file_layer = fmt::layer()
        .json()
        .with_writer(non_blocking)
        .with_filter(EnvFilter::new("debug"));

    tracing_subscriber::registry()
        .with(stdout_layer)
        .with(file_layer)
        .init();

    // IMPORTANTE: _guard deve ser mantido vivo durante toda a execução
    // Se _guard for dropado, os logs pendentes podem ser perdidos
}

fn configurar_log_arquivo_com_guard() -> tracing_appender::non_blocking::WorkerGuard {
    let file_appender = rolling::daily("logs", "app.log");
    let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);

    tracing_subscriber::fmt()
        .json()
        .with_writer(non_blocking)
        .with_env_filter(EnvFilter::new("info"))
        .init();

    guard // Retornar o guard para mantê-lo vivo
}

fn main() {
    let _guard = configurar_log_arquivo_com_guard();

    tracing::info!("Aplicação iniciada");
    // ... resto da aplicação

    // O guard é dropado aqui, garantindo que todos os logs são escritos
}

Múltiplos subscribers (layers)

use tracing_subscriber::{fmt, prelude::*, EnvFilter, Layer};

fn configurar_multiplos_layers() {
    // Layer para console (texto formatado)
    let console_layer = fmt::layer()
        .pretty()
        .with_filter(EnvFilter::new("info"));

    // Layer para arquivo (JSON)
    let file_appender = tracing_appender::rolling::daily("logs", "app.log");
    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);

    let file_layer = fmt::layer()
        .json()
        .with_writer(non_blocking)
        .with_filter(EnvFilter::new("debug"));

    // Layer para erros (arquivo separado)
    let error_appender = tracing_appender::rolling::daily("logs", "errors.log");
    let (error_non_blocking, _error_guard) = tracing_appender::non_blocking(error_appender);

    let error_layer = fmt::layer()
        .json()
        .with_writer(error_non_blocking)
        .with_filter(EnvFilter::new("error"));

    // Combinar todos os layers
    tracing_subscriber::registry()
        .with(console_layer)
        .with(file_layer)
        .with(error_layer)
        .init();
}

Integração com OpenTelemetry

use opentelemetry::trace::TracerProvider;
use opentelemetry_otlp::WithExportConfig;
use tracing_subscriber::{prelude::*, EnvFilter, fmt};

async fn configurar_opentelemetry() -> Result<(), Box<dyn std::error::Error>> {
    // Configurar exporter OTLP
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint("http://localhost:4317")
        .build()?;

    let provider = opentelemetry_sdk::trace::TracerProvider::builder()
        .with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio)
        .with_resource(opentelemetry_sdk::Resource::new(vec![
            opentelemetry::KeyValue::new("service.name", "minha-app"),
            opentelemetry::KeyValue::new("service.version", "1.0.0"),
        ]))
        .build();

    let tracer = provider.tracer("minha-app");

    // Layer do OpenTelemetry
    let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);

    // Layer do console
    let fmt_layer = fmt::layer()
        .pretty()
        .with_filter(EnvFilter::new("info"));

    // Combinar
    tracing_subscriber::registry()
        .with(otel_layer)
        .with(fmt_layer)
        .init();

    Ok(())
}

use tracing::{info, instrument};

#[instrument]
async fn processar_pedido(pedido_id: u64) {
    info!("Processando pedido");

    validar_pagamento(pedido_id).await;
    enviar_notificacao(pedido_id).await;

    info!("Pedido concluído");
}

#[instrument]
async fn validar_pagamento(pedido_id: u64) {
    info!("Validando pagamento");
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    info!("Pagamento validado");
}

#[instrument]
async fn enviar_notificacao(pedido_id: u64) {
    info!("Enviando notificação");
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
    info!("Notificação enviada");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    configurar_opentelemetry().await?;

    processar_pedido(12345).await;

    // Dar tempo para o exporter enviar os dados
    opentelemetry::global::shutdown_tracer_provider();

    Ok(())
}

Campos dinâmicos em spans

use tracing::{info, info_span, Span};

fn exemplo_campos_dinamicos() {
    let span = info_span!("requisicao", request_id = tracing::field::Empty, user_id = tracing::field::Empty);
    let _guard = span.enter();

    // Preencher campos depois de criar o span
    Span::current().record("request_id", "req-abc-123");

    info!("Requisição recebida");

    // Após autenticação, adicionar user_id
    Span::current().record("user_id", 42);

    info!("Usuário autenticado");
}

Boas Práticas

1. Use campos estruturados, não interpolação de strings

use tracing::info;

fn exemplo_boas_praticas() {
    // RUIM: dados interpolados na string
    let usuario_id = 42;
    let acao = "login";
    info!("Usuário {} realizou {}", usuario_id, acao);

    // BOM: campos estruturados (parseáveis por ferramentas)
    info!(
        usuario_id = usuario_id,
        acao = acao,
        "Ação do usuário"
    );
}

2. Use #[instrument] em todas as funções importantes

use tracing::instrument;

#[instrument(skip(senha))] // Nunca logar senhas!
async fn autenticar(email: &str, senha: &str) -> Result<u64, String> {
    // ...
    Ok(42)
}

#[instrument(err)] // Logar automaticamente se retornar Err
async fn buscar_dados(id: u64) -> Result<String, std::io::Error> {
    // ...
    Ok("dados".to_string())
}

#[instrument(ret)] // Logar o valor de retorno
fn calcular(a: i32, b: i32) -> i32 {
    a + b
}

#[instrument(
    name = "processar_pagamento", // Nome customizado do span
    skip(cartao),                 // Pular dados sensíveis
    fields(
        valor_brl = %valor,       // Campo customizado
        moeda = "BRL",            // Campo estático
    )
)]
async fn processar(pedido_id: u64, valor: f64, cartao: &str) -> Result<(), String> {
    // ...
    Ok(())
}

3. Configure filtros por ambiente

use tracing_subscriber::EnvFilter;

fn filtro_para_ambiente(ambiente: &str) -> EnvFilter {
    match ambiente {
        "dev" => EnvFilter::new("debug,hyper=info,h2=info"),
        "staging" => EnvFilter::new("info,minha_app=debug"),
        "producao" => EnvFilter::new("warn,minha_app=info"),
        _ => EnvFilter::new("info"),
    }
}

4. Nunca logue dados sensíveis

use tracing::{info, instrument};

// BOM: skip de campos sensíveis
#[instrument(skip(token, senha))]
async fn criar_conta(email: &str, senha: &str, token: &str) -> Result<u64, String> {
    info!(email = email, "Criando conta");
    Ok(1)
}

// BOM: mascarar dados parcialmente
fn mascarar_email(email: &str) -> String {
    let partes: Vec<&str> = email.split('@').collect();
    if partes.len() == 2 {
        let usuario = partes[0];
        let dominio = partes[1];
        let visivel = if usuario.len() > 2 { 2 } else { 1 };
        format!("{}***@{}", &usuario[..visivel], dominio)
    } else {
        "***".to_string()
    }
}

5. Adicione contexto com spans

use tracing::{info, info_span, Instrument};

async fn processar_requisicao(request_id: &str, usuario_id: u64) {
    // Span de nível de requisição
    let span = info_span!(
        "requisicao",
        request_id = request_id,
        usuario_id = usuario_id,
    );

    async {
        info!("Processando requisição");

        // Sub-operações herdam o contexto
        buscar_dados().await;
        salvar_resultado().await;

        info!("Requisição concluída");
    }
    .instrument(span) // Associar o span à future
    .await;
}

async fn buscar_dados() {
    let span = info_span!("buscar_dados");
    async {
        info!("Consultando banco");
    }
    .instrument(span)
    .await;
}

async fn salvar_resultado() {
    let span = info_span!("salvar_resultado");
    async {
        info!("Salvando no banco");
    }
    .instrument(span)
    .await;
}

Exemplos Práticos

Serviço web observável com Axum

use axum::{
    extract::{Path, State},
    http::{Request, StatusCode},
    middleware::{self, Next},
    response::{IntoResponse, Response},
    routing::get,
    Json, Router,
};
use serde::Serialize;
use std::time::{Duration, Instant};
use tracing::{error, info, info_span, instrument, warn, Instrument};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use uuid::Uuid;

// === Configuração de tracing ===

fn configurar_tracing() {
    let ambiente = std::env::var("APP_ENV").unwrap_or_else(|_| "dev".to_string());

    if ambiente == "producao" {
        // JSON para produção
        tracing_subscriber::fmt()
            .json()
            .flatten_event(true)
            .with_current_span(true)
            .with_span_list(true)
            .with_env_filter(
                EnvFilter::try_from_default_env()
                    .unwrap_or_else(|_| EnvFilter::new("info,tower_http=debug")),
            )
            .init();
    } else {
        // Texto formatado para desenvolvimento
        tracing_subscriber::fmt()
            .pretty()
            .with_env_filter(EnvFilter::new(
                "debug,hyper=info,h2=info,tower=info",
            ))
            .init();
    }
}

// === Middleware de tracing ===

async fn middleware_tracing(request: Request<axum::body::Body>, next: Next) -> Response {
    let request_id = Uuid::new_v4().to_string();
    let metodo = request.method().clone();
    let uri = request.uri().clone();
    let versao = request.version();

    let span = info_span!(
        "requisicao",
        request_id = %request_id,
        metodo = %metodo,
        uri = %uri,
        versao = ?versao,
        status = tracing::field::Empty,
        latencia_ms = tracing::field::Empty,
    );

    let inicio = Instant::now();

    let response = next.run(request).instrument(span.clone()).await;

    let latencia = inicio.elapsed();
    let status = response.status().as_u16();

    span.record("status", status);
    span.record("latencia_ms", latencia.as_millis() as u64);

    let _guard = span.enter();

    if status >= 500 {
        error!(
            status = status,
            latencia_ms = latencia.as_millis() as u64,
            "Erro interno do servidor"
        );
    } else if status >= 400 {
        warn!(
            status = status,
            latencia_ms = latencia.as_millis() as u64,
            "Erro do cliente"
        );
    } else {
        info!(
            status = status,
            latencia_ms = latencia.as_millis() as u64,
            "Requisição concluída"
        );
    }

    response
}

// === Handlers ===

#[derive(Serialize)]
struct Usuario {
    id: u64,
    nome: String,
    email: String,
}

#[instrument(skip(state))]
async fn listar_usuarios(State(state): State<AppState>) -> Json<Vec<Usuario>> {
    info!("Listando usuários");

    let usuarios = buscar_usuarios_do_banco(&state).await;

    info!(total = usuarios.len(), "Usuários carregados");
    Json(usuarios)
}

#[instrument(skip(state))]
async fn obter_usuario(
    State(state): State<AppState>,
    Path(id): Path<u64>,
) -> Result<Json<Usuario>, StatusCode> {
    info!("Buscando usuário");

    match buscar_usuario_por_id(&state, id).await {
        Some(usuario) => {
            info!(nome = %usuario.nome, "Usuário encontrado");
            Ok(Json(usuario))
        }
        None => {
            warn!("Usuário não encontrado");
            Err(StatusCode::NOT_FOUND)
        }
    }
}

#[instrument]
async fn saude() -> impl IntoResponse {
    info!("Health check");
    Json(serde_json::json!({"status": "ok"}))
}

// === Funções de negócio instrumentadas ===

#[instrument(skip(state))]
async fn buscar_usuarios_do_banco(state: &AppState) -> Vec<Usuario> {
    info!("Consultando banco de dados");

    // Simular latência do banco
    tokio::time::sleep(Duration::from_millis(20)).await;

    vec![
        Usuario {
            id: 1,
            nome: "Maria Silva".to_string(),
            email: "maria@email.com".to_string(),
        },
        Usuario {
            id: 2,
            nome: "João Santos".to_string(),
            email: "joao@email.com".to_string(),
        },
    ]
}

#[instrument(skip(state))]
async fn buscar_usuario_por_id(state: &AppState, id: u64) -> Option<Usuario> {
    info!("Consultando banco de dados por ID");

    tokio::time::sleep(Duration::from_millis(10)).await;

    if id <= 2 {
        Some(Usuario {
            id,
            nome: format!("Usuário {}", id),
            email: format!("usuario{}@email.com", id),
        })
    } else {
        None
    }
}

// === Estado e App ===

#[derive(Clone)]
struct AppState {
    db_url: String,
}

fn criar_app() -> Router {
    let state = AppState {
        db_url: "postgres://localhost/app".to_string(),
    };

    Router::new()
        .route("/saude", get(saude))
        .route("/usuarios", get(listar_usuarios))
        .route("/usuarios/{id}", get(obter_usuario))
        .layer(middleware::from_fn(middleware_tracing))
        .with_state(state)
}

#[tokio::main]
async fn main() {
    configurar_tracing();

    info!(
        versao = env!("CARGO_PKG_VERSION"),
        "Iniciando servidor"
    );

    let app = criar_app();

    let addr = "0.0.0.0:3000";
    info!(endereco = addr, "Servidor pronto");

    let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Comparação com Alternativas

CaracterísticaTracinglog + env_loggersloglog4rs
AbordagemSpans + eventsEvents apenasStructured loggingLogging framework
EstruturadoSim (nativo)NãoSimParcial
SpansSimNãoNãoNão
Async-awareSimNãoNãoNão
HierárquicoSim (spans)NãoParcialNão
OpenTelemetryNativoVia bridgeVia bridgeNão
JSON outputSimNãoSimSim
PerformanceExcelenteBoaBoaBoa
EcossistemaTokio, Axum, etc.AmploModeradoLimitado
  • Tracing vs log: log é mais simples e adequado para libs que não precisam de spans. Tracing é superior para aplicações, especialmente async. O Tracing implementa o facade do log, então libs usando log funcionam com subscribers do Tracing.
  • Tracing vs slog: slog foi pioneiro em structured logging no Rust, mas Tracing se tornou o padrão com melhor integração com o ecossistema async.
  • Quando usar log: em bibliotecas que querem dependência mínima e não precisam de spans.
  • Quando usar Tracing: em aplicações, especialmente as que usam async/await.

Conclusão

O Tracing transformou a observabilidade em Rust de simples “imprimir mensagens” para um sistema sofisticado de rastreamento estruturado. A combinação de spans hierárquicos, eventos estruturados e integração nativa com o ecossistema async torna-o indispensável para qualquer aplicação de produção.

O investimento em instrumentar seu código com Tracing paga dividendos enormes quando você precisa diagnosticar problemas em produção. A capacidade de correlacionar eventos dentro de spans, filtrar por contexto e exportar para sistemas como Jaeger, Datadog ou Grafana Tempo faz toda a diferença na operação de serviços em escala.

Próximos passos

  • Configure tracing-opentelemetry para exportar traces para Jaeger ou Grafana Tempo
  • Use tower-http TraceLayer para instrumentar automaticamente requisições Axum
  • Explore tracing-appender para rotação de arquivos de log
  • Combine com Config para configurar níveis de log por ambiente
  • Aprenda sobre métricas com opentelemetry-metrics para complementar os traces