Tracing Rust: Observabilidade, Logs e OpenTelemetry em 2026

Guia completo de Tracing em Rust: spans, events, logs estruturados, tracing-subscriber, Axum, Tower, OpenTelemetry, produção e carreira backend.

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

Tracing Rust em 2026: onde ele encaixa

Quem pesquisa Tracing Rust normalmente já passou do “hello world” e quer responder uma pergunta operacional: como descobrir o que um serviço Rust está fazendo quando uma requisição fica lenta, um worker falha ou uma dependência externa começa a oscilar? A resposta moderna começa com spans e events bem modelados, não com println! espalhado pelo código.

Em aplicações backend, Tracing fica no centro da stack. Tokio preserva contexto em tarefas assíncronas, Axum e Tower usam camadas como TraceLayer para instrumentar HTTP, Tonic leva a mesma mentalidade para gRPC, e SQLx ou workers de mensageria precisam registrar latência, erro e cardinalidade de campos com cuidado. O artigo Rust com OpenTelemetry em produção aprofunda a etapa seguinte: exportar esses sinais para coletores, traces distribuídos e dashboards.

Para carreira, Tracing é um diferencial porque aproxima Rust do trabalho real de produção. Muitas vagas Rust não escrevem “tracing” no título, mas pedem backend, plataforma, infraestrutura, cloud, observabilidade, SRE, fintech ou sistemas distribuídos. Saber explicar request IDs, campos estáveis, logs JSON, sampling, OpenTelemetry Collector e investigação de incidentes mostra maturidade para empresas que usam Rust além da linguagem em si.

Use esta página como referência técnica e conecte com guias práticos de produção: serviços resilientes com Tower, Axum e Tokio, Rust para mensageria com Kafka, RabbitMQ e NATS, deploy Rust em VPS e Rust e eBPF para observabilidade. O objetivo não é ter telemetria bonita em demo; é conseguir responder rapidamente o que quebrou, desde quando, quem foi afetado e qual ação reduz o impacto.

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: "[email protected]".to_string(),
        },
        Usuario {
            id: 2,
            nome: "João Santos".to_string(),
            email: "[email protected]".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.

Tracing para produção e portfólio

Um projeto de portfólio convincente não precisa ser enorme. Uma API Axum com SQLx, PostgreSQL, um worker Tokio e Tracing bem configurado já demonstra muito: cada requisição recebe um request_id, cada operação de banco registra duração e classe de erro, cada worker cria um span com event_id e resultado, e logs JSON podem ser enviados para o ambiente de produção sem mudar o código de negócio.

Se o projeto usa middleware, conecte Tracing com Tower e tower-http::trace::TraceLayer. Se usa gRPC, aplique a mesma disciplina em Tonic. Se usa filas, leia também Rust para mensageria para modelar idempotência e dead letters com campos rastreáveis. Em todos os casos, evite registrar dados sensíveis; prefira IDs sintéticos, rota normalizada, classe de erro, versão do serviço e dependência lógica.

O próximo passo natural é OpenTelemetry em Rust. Tracing continua sendo a base dentro da aplicação; OpenTelemetry entra como camada de exportação e correlação entre serviços. Para quem mira backend Rust, DevOps e infraestrutura com Rust, vagas Rust ou empresas que usam Rust, essa combinação comunica que você sabe construir e operar software, não apenas compilar exemplos.

Próximos passos

  • Configure tracing-opentelemetry para exportar traces para Jaeger, Grafana Tempo ou OpenTelemetry Collector
  • 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