Logging e Observabilidade em Rust: Guia | Rust Brasil

Guia de logging e observabilidade em Rust: tracing, log, métricas, OpenTelemetry e structured logging.

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

PilarFerramenta RustPara Que Serve
Logstracing + tracing-subscriberEventos discretos com contexto
Tracestracing + tracing-opentelemetryFluxo de execução entre serviços
Métricasmetrics + metrics-exporter-prometheusAgregações numéricas ao longo do tempo

Veja Também