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ível | Macro | Uso típico |
|---|---|---|
Error | error!() | Erros que impedem operação normal |
Warn | warn!() | Situações inesperadas, mas recuperáveis |
Info | info!() | Informações operacionais relevantes |
Debug | debug!() | Informações úteis para debugging |
Trace | trace!() | 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ística | log + env_logger | tracing |
|---|---|---|
| Complexidade | Simples | Mais complexo |
| Dados estruturados | Básico | Nativo (spans e fields) |
| Contexto entre funções | Manual | Automático via spans |
| Async | Funcional | Projetado para async |
| Performance | Muito bom | Excelente |
| Ecossistema | Amplo (facade padrão) | Crescendo rapidamente |
| Caso de uso ideal | Aplicações simples | Sistemas 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ção | Características |
|---|---|
env_logger | Simples, configuração via variável ambiente |
pretty_env_logger | Como env_logger, mas com saída formatada |
simplelog | Múltiplos destinos (arquivo, terminal) |
fern | Altamente configurável, múltiplos outputs |
log4rs | Inspirado 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