Log e env_logger: Logging Idiomático em Rust

Guia completo da crate log e env_logger em Rust. Aprenda a usar a facade de logging padrão com info!, warn!, error!, debug!, trace!, configuração com RUST_LOG, formatação personalizada e filtros por módulo.

O logging é uma das práticas mais fundamentais no desenvolvimento de software. Em Rust, a crate log estabelece a facade de logging padrão do ecossistema — uma interface unificada que separa a emissão de logs (quem produz) da implementação de logging (quem consome e exibe). A crate env_logger é a implementação mais popular dessa facade, permitindo configurar o nível e o filtro de logs através de variáveis de ambiente.

Juntas, log e env_logger formam a solução de logging mais simples e amplamente compatível do ecossistema Rust. Praticamente toda crate que emite logs usa a facade log, o que significa que ao configurar env_logger na sua aplicação, você automaticamente captura logs de todas as suas dependências.

Instalação

Adicione ambas as crates ao seu Cargo.toml:

[dependencies]
log = "0.4"
env_logger = "0.11"

A crate log fornece as macros de logging (info!, warn!, etc.), enquanto env_logger fornece a implementação que efetivamente exibe os logs no terminal.

Se você quiser apenas emitir logs em uma biblioteca (sem decidir como exibi-los), basta adicionar log:

[dependencies]
log = "0.4"

Uso Básico

Inicializando o Logger

O primeiro passo é inicializar o env_logger no início da sua aplicação:

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

fn main() {
    // Inicializa o env_logger. Deve ser chamado uma única vez,
    // preferencialmente no início de main().
    env_logger::init();

    info!("Aplicação iniciada");
    debug!("Modo debug ativado");
    warn!("Atenção: configuração padrão em uso");
    error!("Falha ao conectar ao banco de dados");
    trace!("Detalhes internos de execução");
}

Níveis de Log

A crate log define cinco níveis de severidade, do mais severo ao mais detalhado:

NívelMacroUso típico
Errorerror!()Erros que impedem operação normal
Warnwarn!()Situações inesperadas, mas recuperáveis
Infoinfo!()Informações operacionais relevantes
Debugdebug!()Informações úteis para debugging
Tracetrace!()Detalhes granulares de execução interna

Controlando com RUST_LOG

A variável de ambiente RUST_LOG controla quais logs são exibidos:

# Mostra todos os logs de nível info ou superior
RUST_LOG=info cargo run

# Mostra tudo (incluindo trace)
RUST_LOG=trace cargo run

# Apenas erros
RUST_LOG=error cargo run

# Debug para o seu crate, info para o resto
RUST_LOG=meu_app=debug,info cargo run

Se RUST_LOG não estiver definida, nenhum log será exibido por padrão.

Logging com Valores Formatados

As macros de log suportam formatação idêntica ao println!:

use log::{info, warn, error};

fn processar_pedido(id: u64, valor: f64) {
    info!("Processando pedido #{} no valor de R${:.2}", id, valor);

    if valor > 10_000.0 {
        warn!("Pedido #{} excede o limite padrão: R${:.2}", id, valor);
    }
}

fn conectar_banco(url: &str) -> Result<(), String> {
    info!("Conectando ao banco: {}", url);
    // Simulando erro
    let resultado: Result<(), String> = Err("Conexão recusada".to_string());

    match &resultado {
        Ok(()) => info!("Conexão estabelecida com sucesso"),
        Err(e) => error!("Falha na conexão com {}: {}", url, e),
    }

    resultado
}

Logging com key=value (Estruturado)

A partir de versões recentes, log suporta pares chave-valor:

use log::info;

fn registrar_requisicao(metodo: &str, caminho: &str, status: u16, duracao_ms: u64) {
    info!(
        metodo = metodo,
        caminho = caminho,
        status = status,
        duracao_ms = duracao_ms;
        "Requisição HTTP processada"
    );
}

Recursos Avançados

Filtragem por Módulo

Uma das funcionalidades mais poderosas do RUST_LOG é a filtragem por caminho de módulo:

# Apenas logs do módulo 'servidor::http'
RUST_LOG=servidor::http=debug cargo run

# Múltiplos filtros
RUST_LOG=servidor::http=debug,servidor::db=info,warn cargo run

# Filtro por regex (requer feature 'regex' no env_logger)
RUST_LOG="servidor::http::handler.*=trace" cargo run

Isso é extremamente útil para depurar um módulo específico sem ser inundado por logs de outras partes do sistema:

// src/db/conexao.rs
mod conexao {
    use log::{debug, info, trace};

    pub fn conectar(url: &str) {
        // Visível com RUST_LOG=meu_app::db::conexao=debug
        debug!("Tentando conexão com: {}", url);
        trace!("Parâmetros de conexão: timeout=30s, pool=5");
        info!("Conexão estabelecida");
    }
}

// src/http/servidor.rs
mod servidor {
    use log::{info, debug};

    pub fn iniciar(porta: u16) {
        // Visível com RUST_LOG=meu_app::http::servidor=debug
        debug!("Configurando servidor na porta {}", porta);
        info!("Servidor HTTP escutando na porta {}", porta);
    }
}

Formatação Personalizada

O env_logger permite personalizar completamente o formato dos logs:

use env_logger::Builder;
use log::{info, warn, LevelFilter};
use std::io::Write;

fn main() {
    Builder::new()
        .format(|buf, record| {
            writeln!(
                buf,
                "[{} {} {}:{}] {}",
                chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
                record.level(),
                record.file().unwrap_or("desconhecido"),
                record.line().unwrap_or(0),
                record.args()
            )
        })
        .filter(None, LevelFilter::Info)
        .init();

    info!("Servidor iniciado");
    warn!("Memória acima de 80%");
}
// Saída: [2026-02-27 14:30:00 INFO src/main.rs:18] Servidor iniciado
// Saída: [2026-02-27 14:30:00 WARN src/main.rs:19] Memória acima de 80%

Formatação com Cores

O env_logger suporta cores no terminal por padrão:

use env_logger::Builder;
use log::{info, warn, error, debug, LevelFilter};
use std::io::Write;

fn main() {
    Builder::new()
        .format(|buf, record| {
            let level_style = buf.default_level_style(record.level());
            writeln!(
                buf,
                "{level_style}[{:<5}]{level_style:#} {} - {}",
                record.level(),
                record.target(),
                record.args()
            )
        })
        .filter(None, LevelFilter::Debug)
        .init();

    info!("Tudo certo");
    warn!("Cuidado");
    error!("Problema!");
    debug!("Detalhes");
}

Configuração Programática com Fallback

Você pode definir um nível padrão e ainda permitir override via RUST_LOG:

use env_logger::Builder;
use log::LevelFilter;
use std::env;

fn main() {
    let mut builder = Builder::new();

    // Define um padrão
    builder.filter(None, LevelFilter::Info);

    // Permite override via RUST_LOG
    if let Ok(rust_log) = env::var("RUST_LOG") {
        builder.parse_filters(&rust_log);
    }

    builder.init();
}

Direcionando Logs para Arquivo

Embora env_logger escreva para stderr por padrão, você pode redirecionar:

use env_logger::Builder;
use log::{info, LevelFilter};
use std::fs::File;
use std::io::Write;

fn main() {
    let arquivo = File::create("app.log").expect("Falha ao criar arquivo de log");
    let arquivo = std::sync::Mutex::new(arquivo);

    Builder::new()
        .format(move |_buf, record| {
            let mut arquivo = arquivo.lock().unwrap();
            writeln!(
                arquivo,
                "[{}] {} - {}",
                record.level(),
                record.target(),
                record.args()
            )
        })
        .filter(None, LevelFilter::Info)
        .init();

    info!("Este log vai para o arquivo");
}

Usando log em Bibliotecas

Bibliotecas devem usar apenas a crate log, sem inicializar nenhum logger:

// Arquivo: src/lib.rs de uma biblioteca
use log::{debug, info, warn};

pub struct Cache {
    dados: std::collections::HashMap<String, String>,
    capacidade: usize,
}

impl Cache {
    pub fn novo(capacidade: usize) -> Self {
        info!("Cache criado com capacidade {}", capacidade);
        Cache {
            dados: std::collections::HashMap::new(),
            capacidade,
        }
    }

    pub fn inserir(&mut self, chave: String, valor: String) {
        if self.dados.len() >= self.capacidade {
            warn!("Cache cheio ({}/{}), removendo entrada mais antiga",
                  self.dados.len(), self.capacidade);
            if let Some(primeira_chave) = self.dados.keys().next().cloned() {
                self.dados.remove(&primeira_chave);
            }
        }
        debug!("Inserindo chave '{}' no cache", chave);
        self.dados.insert(chave, valor);
    }

    pub fn obter(&self, chave: &str) -> Option<&String> {
        let resultado = self.dados.get(chave);
        match resultado {
            Some(_) => debug!("Cache hit para '{}'", chave),
            None => debug!("Cache miss para '{}'", chave),
        }
        resultado
    }
}

Compilação Condicional de Logs

A crate log suporta features para eliminar logs em tempo de compilação:

[dependencies]
log = { version = "0.4", features = ["max_level_info", "release_max_level_warn"] }

Isso remove completamente as chamadas debug! e trace! do binário em modo release, resultando em zero overhead.

Boas Práticas

1. Inicialize o Logger Apenas Uma Vez

fn main() {
    // Correto: inicializar no início de main
    env_logger::init();

    // Todo o resto da aplicação...
    executar_app();
}

// ERRADO: não inicialize em bibliotecas ou funções chamadas múltiplas vezes
// fn processar() {
//     env_logger::init(); // Panic! se chamado mais de uma vez
// }

2. Use try_init para Testes

#[cfg(test)]
mod tests {
    use log::info;

    fn inicializar_logger() {
        // try_init não causa panic se já inicializado
        let _ = env_logger::builder()
            .is_test(true)
            .try_init();
    }

    #[test]
    fn teste_com_logs() {
        inicializar_logger();
        info!("Executando teste");
        assert_eq!(2 + 2, 4);
    }

    #[test]
    fn outro_teste_com_logs() {
        inicializar_logger();
        info!("Outro teste");
        assert!(true);
    }
}

3. Escolha o Nível Correto

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

fn processar_transacao(id: u64, valor: f64) -> Result<(), String> {
    // TRACE: detalhes internos, fluxo de execução
    trace!("Entrando em processar_transacao(id={}, valor={})", id, valor);

    // DEBUG: informações úteis para desenvolvimento
    debug!("Validando transação #{}", id);

    // INFO: eventos operacionais importantes
    info!("Transação #{} processada: R${:.2}", id, valor);

    // WARN: algo inesperado, mas o sistema continua funcionando
    if valor > 50_000.0 {
        warn!("Transação #{} com valor alto: R${:.2} — requer auditoria", id, valor);
    }

    // ERROR: algo deu errado
    if valor < 0.0 {
        error!("Transação #{} com valor negativo: R${:.2}", id, valor);
        return Err("Valor inválido".to_string());
    }

    trace!("Saindo de processar_transacao");
    Ok(())
}

4. Inclua Contexto Suficiente

use log::{error, info};

// RUIM: sem contexto
fn processar_ruim(dados: &[u8]) {
    info!("Processando...");
    if dados.is_empty() {
        error!("Erro!");
    }
}

// BOM: com contexto útil
fn processar_bom(id: &str, dados: &[u8]) {
    info!("Processando requisição id={} ({} bytes)", id, dados.len());
    if dados.is_empty() {
        error!("Requisição id={} contém payload vazio", id);
    }
}

5. Evite Logging em Loops Quentes

use log::{debug, info};

fn processar_lote(itens: &[u32]) {
    info!("Processando lote com {} itens", itens.len());

    // RUIM: log dentro de loop com milhares de iterações
    // for item in itens {
    //     debug!("Processando item {}", item);
    // }

    // BOM: log apenas em marcos importantes
    for (i, item) in itens.iter().enumerate() {
        // Log a cada 1000 itens
        if i % 1000 == 0 && i > 0 {
            debug!("Progresso: {}/{} itens processados", i, itens.len());
        }
        let _ = item; // processar item
    }

    info!("Lote processado: {} itens", itens.len());
}

Exemplos Práticos

Exemplo Completo: Servidor de Aplicação com Logging

use log::{debug, error, info, trace, warn, LevelFilter};
use env_logger::Builder;
use std::io::Write;
use std::time::Instant;

// --- Módulo de Configuração ---

mod config {
    use log::{debug, info, warn};

    pub struct AppConfig {
        pub porta: u16,
        pub max_conexoes: usize,
        pub timeout_ms: u64,
    }

    impl AppConfig {
        pub fn carregar() -> Self {
            info!("Carregando configuração da aplicação");

            let porta = std::env::var("APP_PORTA")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or_else(|| {
                    warn!("APP_PORTA não definida, usando padrão 8080");
                    8080
                });

            let max_conexoes = std::env::var("APP_MAX_CONN")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or_else(|| {
                    debug!("APP_MAX_CONN não definida, usando padrão 100");
                    100
                });

            let timeout_ms = std::env::var("APP_TIMEOUT")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(5000);

            info!(
                "Configuração carregada: porta={}, max_conexoes={}, timeout={}ms",
                porta, max_conexoes, timeout_ms
            );

            AppConfig {
                porta,
                max_conexoes,
                timeout_ms,
            }
        }
    }
}

// --- Módulo de Banco de Dados ---

mod db {
    use log::{debug, error, info, trace};
    use std::collections::HashMap;

    pub struct Database {
        dados: HashMap<String, String>,
    }

    impl Database {
        pub fn conectar(url: &str) -> Result<Self, String> {
            info!("Conectando ao banco de dados: {}", url);
            debug!("Parâmetros: pool_size=5, timeout=30s");

            // Simulação de conexão
            if url.is_empty() {
                error!("URL do banco de dados vazia");
                return Err("URL inválida".to_string());
            }

            info!("Conexão com banco de dados estabelecida");
            Ok(Database {
                dados: HashMap::new(),
            })
        }

        pub fn inserir(&mut self, chave: &str, valor: &str) -> Result<(), String> {
            trace!("DB INSERT: chave='{}', valor='{}'", chave, valor);
            self.dados.insert(chave.to_string(), valor.to_string());
            debug!("Registro inserido: chave='{}'", chave);
            Ok(())
        }

        pub fn buscar(&self, chave: &str) -> Option<String> {
            trace!("DB SELECT: chave='{}'", chave);
            let resultado = self.dados.get(chave).cloned();
            match &resultado {
                Some(v) => debug!("Registro encontrado: chave='{}', tamanho={}", chave, v.len()),
                None => debug!("Registro não encontrado: chave='{}'", chave),
            }
            resultado
        }
    }
}

// --- Módulo de Serviço ---

mod servico {
    use log::{debug, error, info, warn};
    use std::time::Instant;

    pub struct Requisicao {
        pub metodo: String,
        pub caminho: String,
        pub corpo: Option<String>,
    }

    pub struct Resposta {
        pub status: u16,
        pub corpo: String,
    }

    pub fn processar_requisicao(req: &Requisicao) -> Resposta {
        let inicio = Instant::now();
        info!("{} {}", req.metodo, req.caminho);

        let resposta = match req.caminho.as_str() {
            "/saude" => {
                debug!("Health check solicitado");
                Resposta {
                    status: 200,
                    corpo: r#"{"status": "ok"}"#.to_string(),
                }
            }
            "/api/usuarios" => {
                debug!("Listando usuários");
                Resposta {
                    status: 200,
                    corpo: r#"[{"id": 1, "nome": "Maria"}]"#.to_string(),
                }
            }
            _ => {
                warn!("Rota não encontrada: {}", req.caminho);
                Resposta {
                    status: 404,
                    corpo: r#"{"erro": "Não encontrado"}"#.to_string(),
                }
            }
        };

        let duracao = inicio.elapsed();
        if duracao.as_millis() > 100 {
            warn!(
                "{} {} - {} (LENTO: {:?})",
                req.metodo, req.caminho, resposta.status, duracao
            );
        } else {
            info!(
                "{} {} - {} ({:?})",
                req.metodo, req.caminho, resposta.status, duracao
            );
        }

        if resposta.status >= 500 {
            error!(
                "Erro interno: {} {} retornou {}",
                req.metodo, req.caminho, resposta.status
            );
        }

        resposta
    }
}

fn inicializar_logger() {
    Builder::new()
        .format(|buf, record| {
            let nivel = record.level();
            let estilo = buf.default_level_style(nivel);
            writeln!(
                buf,
                "{estilo}[{:<5}]{estilo:#} [{}] {} ({}:{})",
                nivel,
                buf.timestamp_millis(),
                record.args(),
                record.file().unwrap_or("?"),
                record.line().unwrap_or(0),
            )
        })
        .filter(None, LevelFilter::Info)
        .parse_default_env()
        .init();
}

fn main() {
    inicializar_logger();

    info!("=== Iniciando aplicação ===");
    let inicio = Instant::now();

    // Carregar configuração
    let config = config::AppConfig::carregar();

    // Conectar ao banco
    let mut db = match db::Database::conectar("postgres://localhost/meu_app") {
        Ok(db) => db,
        Err(e) => {
            error!("Falha fatal ao conectar ao banco: {}", e);
            std::process::exit(1);
        }
    };

    // Inserir dados de exemplo
    let _ = db.inserir("usuario:1", r#"{"nome": "Maria", "email": "maria@ex.com"}"#);
    let _ = db.inserir("usuario:2", r#"{"nome": "João", "email": "joao@ex.com"}"#);

    // Simular requisições
    let requisicoes = vec![
        servico::Requisicao {
            metodo: "GET".to_string(),
            caminho: "/saude".to_string(),
            corpo: None,
        },
        servico::Requisicao {
            metodo: "GET".to_string(),
            caminho: "/api/usuarios".to_string(),
            corpo: None,
        },
        servico::Requisicao {
            metodo: "GET".to_string(),
            caminho: "/api/inexistente".to_string(),
            corpo: None,
        },
    ];

    for req in &requisicoes {
        let resp = servico::processar_requisicao(req);
        debug!("Corpo da resposta: {}", resp.corpo);
    }

    // Buscar dados
    match db.buscar("usuario:1") {
        Some(dados) => info!("Usuário encontrado: {}", dados),
        None => warn!("Usuário não encontrado no banco"),
    }

    let duracao_total = inicio.elapsed();
    info!(
        "=== Aplicação finalizada em {:?} (porta configurada: {}) ===",
        duracao_total, config.porta
    );
}

Execute com diferentes níveis de log:

# Apenas informações operacionais
RUST_LOG=info cargo run

# Debug completo
RUST_LOG=debug cargo run

# Debug apenas do módulo de banco
RUST_LOG=meu_app::db=debug,info cargo run

# Trace no serviço, info no resto
RUST_LOG=meu_app::servico=trace,info cargo run

Comparação com Alternativas

log + env_logger vs tracing

Característicalog + env_loggertracing
ComplexidadeSimplesMais complexo
Dados estruturadosBásicoNativo (spans e fields)
Contexto entre funçõesManualAutomático via spans
AsyncFuncionalProjetado para async
PerformanceMuito bomExcelente
EcossistemaAmplo (facade padrão)Crescendo rapidamente
Caso de uso idealAplicações simplesSistemas distribuídos/async

Quando usar log + env_logger:

  • Aplicações de linha de comando (CLIs)
  • Bibliotecas que querem compatibilidade máxima
  • Projetos simples sem necessidade de spans
  • Quando simplicidade é prioridade

Quando usar tracing:

  • Sistemas assíncronos com Tokio
  • Microsserviços e sistemas distribuídos
  • Quando você precisa de contexto automático entre chamadas async
  • Integração com OpenTelemetry, Jaeger, etc.

Note que tracing é compatível com log — se você usar tracing-log, logs emitidos via log são capturados pelo tracing.

Outras Implementações da Facade log

Além de env_logger, existem outras implementações populares:

ImplementaçãoCaracterísticas
env_loggerSimples, configuração via variável ambiente
pretty_env_loggerComo env_logger, mas com saída formatada
simplelogMúltiplos destinos (arquivo, terminal)
fernAltamente configurável, múltiplos outputs
log4rsInspirado no Log4j, configuração via arquivo

Conclusão

A dupla log + env_logger é a porta de entrada para logging em Rust. A separação entre facade (log) e implementação (env_logger) é um padrão elegante que permite que bibliotecas emitam logs sem impor decisões sobre como esses logs serão tratados.

Para a maioria das aplicações, env_logger oferece tudo que você precisa: filtragem por nível e módulo, formatação personalizável e configuração simples via RUST_LOG. Para cenários mais avançados — como logging estruturado em sistemas distribuídos — considere migrar para tracing, que mantém compatibilidade com o ecossistema log.

Próximos passos:

  • Explore a crate tracing para observabilidade estruturada
  • Veja anyhow e thiserror para combinar logging com tratamento de erros robusto
  • Aprenda a configurar logging em aplicações web com Axum ou Actix Web