Logs de Aplicações Java: Motivações e Melhores Práticas de Utilização
Motivações e Melhores Práticas de Utilização
Introdução e Objetivos
Este artigo aborda o log sob a perspectiva de depuração e monitoramento de sistemas em ambientes de desenvolvimento e produção, distinguindo-se dos logs de auditoria. O objetivo principal da ferramenta de log é auxiliar no acompanhamento da execução do sistema, facilitando a identificação de erros e indicação de falhas críticas.
Motivação para Utilizar Log
A técnica de log consiste na geração de registros em diferentes destinos (arquivos, console, sistemas centralizados) para que desenvolvedores e analistas de suporte obtenham informações sobre processamentos importantes ou ocorrências de erros em produção. O monitoramento de logs é essencial para o diagnóstico de problemas.
Tentar descobrir um bug em produção sem logs é como andar no escuro. Apesar dos testes em homologação, não é possível esgotar todas as possibilidades — bugs podem passar despercebidos e chegar à produção. Muitos problemas não se manifestam em desenvolvimento ou homologação. Por isso, logs são fundamentais para verificar efeitos colaterais e falhas.
Em produção, o código é testado em todas as suas possibilidades reais. Quando ocorre um erro, um log bem definido auxilia na contextualização do problema, registrando o estado e a localização da falha. Devemos instrumentar a aplicação com logs para ter informações necessárias e suficientes quando algo der errado, praticando manutenção preventiva e garantindo qualidade ao usuário.
Testes unitários são valiosos para indicar o que está errado, mas logs fornecem contextos detalhados e onde o problema ocorreu. Log e testes não devem ser confundidos — são complementares.
A técnica de log é mais eficaz que debug para diagnosticar problemas em produção. O debug foca em verificar o estado do programa em um instante específico, consumindo tempo excessivo. Breakpoints não podem ser versionados para referência futura. Logs, quando usados com critério, tornam-se uma ferramenta essencial. Quando usados sem critérios, tornam-se sobrecarga com pouco valor agregado.
Arquitetura de Componentes de Log
Priority – Níveis de Logging
Os níveis de log são hierárquicos e decrescentes. Alterando para um nível mais baixo, as informações anteriores são mantidas e novas são adicionadas. As bibliotecas Java geralmente suportam os seguintes níveis (de acordo com SLF4J/Logback/Log4j2):
- OFF: nenhuma mensagem é gravada
- ERROR: erros que permitam que a aplicação continue executando
- WARN: situações potencialmente prejudiciais
- INFO: informações que descrevem operações do sistema
- DEBUG: informações detalhadas de depuração
- TRACE: informações muito detalhadas, mais granulares que DEBUG
- ALL: qualquer informação do sistema
Nota importante: Diferente de outras linguagens, Java não possui um nível FATAL padrão no SLF4J. Erros críticos devem ser logados como ERROR e o sistema deve decidir se deve parar ou continuar.
Destination (Appenders) – Destino dos Logs
O Appender é a parte do sistema de log responsável por enviar mensagens para algum destino. Ele responde à pergunta: “onde você deseja armazenar essas informações?”
Em ambientes distribuídos, podemos dividir em três preocupações principais:
- Gravação e formatação de mensagens: o que deve ir no log e como formatá-lo
- Destination (Appenders): para onde as mensagens vão e como chegam lá
- Análise de log: desde inspeção simples até ferramentas sofisticadas de visualização
Tipos comuns de Appenders:
- RollingFileAppender: Inicia registro em novo arquivo arquivando o antigo. Permite configurar gatilhos (tempo, tamanho) e compactação de backups.
- ConsoleAppender: Envia saídas ao console para revisão durante depuração.
- DatabaseAppender: Armazena logs em tabelas de banco de dados.
- AsyncAppender: Opera em thread de prioridade mais baixa, despachando mensagens para outros appenders.
- SMTPAppender: Envia logs via email para notificações.
- Appenders para sistemas centralizados: Elastic Stack, Graylog, Loki, DataDog, Splunk.
É possível criar appenders customizados (ex: fila de mensagens assíncrona).
Dependendo da biblioteca, é possível configurar múltiplos appenders para um nível. Exemplo: enviar mensagens ERROR para um sistema de tickets e simultaneamente enviar email.
Layout – Formatação de Mensagens
Layout define o formato da saída do log. As bibliotecas Java fornecem layouts para texto simples, JSON, XML, HTML e outros.
Exemplo de layout JSON:
{
"level": "INFO",
"timestamp": "2024-12-03T11:02:59.201-03:00",
"app": "service-send-email",
"version": "2.0.1",
"logger": "com.empresa.service.EmailService",
"thread": "http-nio-8080-exec-1",
"class": "com.empresa.util.StringUtils",
"method": "formatEmail",
"line": 45,
"message": "Email enviado com sucesso",
"customerId": "12345",
"emailType": "CONFIRMATION"
}
Exemplo de layout texto:
2024-12-03 22:20:02.165 INFO [http-nio-8080-exec-1] c.e.s.EmailService - Email enviado: customer=12345
Requisitos para Componentes de Log
- Alternar entre bibliotecas sem modificar código: Use facades como SLF4J.
- Sem dependência direta de bibliotecas específicas: Dependa apenas da interface (SLF4J).
- Instância global do logger: Alterar configuração em um só lugar.
- Alterar comportamento facilmente: Via arquivo de configuração, sem recompilação.
- Alteração dinâmica de nível de log: Quando o sistema estiver em execução.
Práticas Recomendadas
É muito mais fácil e barato adicionar logs durante o desenvolvimento. Neste momento, o programador tem conhecimento fresco sobre o sistema. Adicionar logs em aplicação pronta exige reaprendizado da lógica e consome muito mais tempo.
Prática 0 – Utilize SLF4J como Facade
SLF4J (Simple Logging Facade for Java) é a abstração padrão para logging em Java.
Motivações:
- Evita acoplamento com bibliotecas específicas
- Flexibiliza adoção de novas bibliotecas (Logback, Log4j2, etc.)
- Altera configurações sem recompilação
- Centraliza código de logging
Exemplo de código:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Em frameworks modernos (ex: Spring Boot), o logger pode ser injetado.
// Caso contrário, é inicializado estaticamente.
public class EmailService {
// 1. Inicialização Estática (Método Clássico)
private static final Logger logger = LoggerFactory.getLogger(EmailService.class);
// 2. Injeção (Método Moderno, ex: Spring)
// @Autowired
// private Logger logger;
public void sendEmail(String recipient, String message) {
// Uso de parâmetros posicionais {} para eficiência
logger.info("Iniciando envio de email para: {}", recipient);
try {
// ... lógica de envio
logger.debug("Conectando ao servidor SMTP: {}", "smtp.empresa.com");
// ...
logger.info("Email enviado com sucesso para: {}", recipient);
} catch (Exception e) {
// Exceção como ÚLTIMO parâmetro para logar o stack trace completo
logger.error("Erro ao enviar email para: {}", recipient, e);
}
}
}
Dependência Maven (SLF4J + Logback):
<dependencies>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<!-- Logback como implementação -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
</dependencies>
Prática 1 – Configure o Formato
A maioria das ferramentas de análise consegue se ajustar a um padrão. Escolha o formato que melhor atenda suas necessidades. JSON é recomendado para análise automatizada.
Exemplo de formato JSON (Logback):
{
"level": "INFO",
"timestamp": "2024-12-03T11:02:59.201-03:00",
"app": "service-send-email",
"version": "2.0.1",
"logger": "com.empresa.service.EmailService",
"thread": "http-nio-8080-exec-1",
"class": "com.empresa.service.EmailService",
"method": "sendEmail",
"line": 45,
"message": "Email enviado com sucesso",
"customerId": "12345",
"mdc": {
"traceId": "abc-123-def",
"userId": "user-456"
}
}
Campos recomendados:
- level: INFO, WARN, DEBUG, ERROR
- timestamp: ISO 8601 / RFC 3339
- logger: nome completo da classe (ex: com.empresa.module.Service)
- thread: nome da thread
- class/method/line: localização no código
- app: nome da aplicação
- version: versão semântica (semver.org)
- message: mensagem do log
- stacktrace: rastreamento de erros
- mdc: Mapped Diagnostic Context para dados contextuais
Configuração Logback (logback.xml):
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"service-email","version":"2.0.1"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
Prática 2 – Configuração para Ambiente de Produção
Em produção, configure o log para nível INFO ou superior para reduzir I/O e melhorar desempenho.
Recomendações:
- Utilize AsyncAppender para não comprometer performance
- Defina política de rotação de arquivos (por tamanho ou tempo)
- Configure compactação automática de logs antigos
- Estabeleça política de backup e retenção
- Configure alertas para padrões críticos
Exemplo de configuração para produção (logback-spring.xml):
<configuration>
<springProfile name="prod">
<!-- Appender assíncrono -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE"/>
</appender>
<!-- Arquivo com rotação -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC"/>
</root>
</springProfile>
</configuration>
Prática 3 – Não Utilize System.out.println
System.out.println() causa perda de performance e não pode ser configurado. Em produção, I/O desnecessário torna a aplicação lenta.
❌ ERRADO:
System.out.println("Processando pedido: " + orderId);
System.err.println("Erro: " + exception.getMessage());
✅ CORRETO:
logger.info("Processando pedido: {}", orderId);
logger.error("Erro ao processar pedido: {}", orderId, exception);
Prática 4 – Utilize os Níveis Corretamente
Escolha o nível apropriado para cada situação:
- ERROR: Erros que prejudicam operações mas permitem continuidade (acesso negado, parâmetros inválidos, falha em operação específica)
- WARN: Situações potencialmente prejudiciais mas que não impedem execução (uso de API deprecada, timeout de conexão secundária, dados inconsistentes que podem ser corrigidos)
- INFO: Operações importantes do sistema em produção (inicialização, requisições processadas, eventos de negócio)
- DEBUG: Informações de depuração (valores de variáveis, fluxo de execução detalhado)
- TRACE: Informações extremamente detalhadas (cada passo de um algoritmo)
Exemplos:
// ERROR - Operação falhou mas sistema continua
logger.error("Falha ao processar pagamento do pedido {}: {}",
orderId, exception.getMessage(), exception);
// WARN - Situação anormal mas não crítica
logger.warn("Tempo de resposta da API externa acima do esperado: {}ms", responseTime);
// INFO - Evento importante do negócio
logger.info("Pedido {} criado com sucesso. Cliente: {}, Valor: {}",
orderId, customerId, totalValue);
// DEBUG - Detalhes para depuração
logger.debug("Calculando desconto. Valor original: {}, Cupom: {}, Desconto aplicado: {}",
originalValue, couponCode, discount);
⚠️ Atenção: Nunca use logger.error() seguido de System.exit() ou similar em bibliotecas. Isso viola o princípio de responsabilidade única. Apenas a aplicação principal deve decidir sobre finalização.
Exemplos que refletem a finalidade de cada nível no ciclo de vida da aplicação.
| Nível | Objetivo | Exemplo de Uso |
|---|---|---|
| ERROR | Falha que exige intervenção, mas que permite a aplicação continuar. | logger.error("Falha na transação. Estorno solicitado. Pedido: {}", orderId, exception); |
| WARN | Evento inesperado ou situação de risco que deve ser monitorada. | logger.warn("Cache expirado. Recarregando dados da API externa, tempo de resposta alto: {}ms", responseTime); |
| INFO | Eventos de negócio importantes em produção (início/fim de operação). | logger.info("Pedido {} criado com sucesso. Cliente: {}", orderId, customerId); |
| DEBUG | Fluxo de execução detalhado, valores de variáveis intermediárias. | logger.debug("Calculando imposto. Base: {}, Alíquota: {}", valorBase, taxaImposto); |
| TRACE | Nível mais granular. Usado para logs internos de bibliotecas ou loops intensivos. | logger.trace("Item {}: processando byte {}/{} do stream.", itemId, currentByte, totalBytes); |
Prática 5 – Logue Apenas Informações Necessárias e Precisas
Logs contam a história do ciclo de vida da aplicação.
Diretrizes:
- Liste o fluxo da aplicação
- Logue cada passo importante do fluxo (no nível apropriado)
- Revise mensagens regularmente e remova o que não agrega valor
- Crie métodos
toString()bem formatados para objetos complexos - Use loggers parametrizados (evite concatenação de strings)
❌ ERRADO:
logger.error("Erro no banco de dados");
// Concatenação de strings (ineficiente)
logger.info("Processando cliente: " + customer.getName() + ", idade: " + customer.getAge());
✅ CORRETO:
logger.error("Erro ao conectar ao banco de dados. Usuário: {}, Host: {}, Erro: {}",
dbUser, dbHost, exception.getMessage(), exception);
// Parametrização eficiente
logger.info("Processando cliente: {}, idade: {}", customer.getName(), customer.getAge());
// Usando toString() bem implementado
logger.debug("Estado do pedido: {}", order);
✅ Por Que Esta é a Melhor Prática
O uso de placeholders ({}) oferece duas grandes vantagens em relação à concatenação de strings tradicional:
Performance e Eficiência
O SLF4J (e as implementações modernas como Logback e Log4j2) só realiza a custosa operação de formatação e concatenação de *strings* se o nível de log estiver ativo.
-
Exemplo: Se o seu sistema estiver configurado para o nível
WARN(Aviso), a chamadalogger.info(...)será ignorada. Como os argumentoscustomer.getName()ecustomer.getAge()são passados separadamente, a formatação da string só ocorre após o sistema verificar que o nívelINFOnão está ativo.
❌ Contraste com a Concatenação de Strings (Má Prática):
// Má prática: A concatenação de strings ocorre SEMPRE,
// mesmo se o log.info for descartado.
logger.info("Processando cliente: " + customer.getName() + ", idade: " + customer.getAge());
Tratamento de Exceções
O SLF4J tem um tratamento especial para o último parâmetro. Se o último parâmetro for um objeto Throwable (uma exceção), ele será automaticamente processado pela implementação para incluir o stack trace completo no log, sem a necessidade de um placeholder para ele.
Exemplo com Exceção:
try {
// ... alguma operação
} catch (IOException e) {
// A exceção 'e' é o último parâmetro e será logada com o stack trace completo.
logger.error("Falha ao ler o arquivo: {}", nomeArquivo, e);
}
Prática 6 – Logue Exceções Corretamente
Quando logar exceções, inclua contexto suficiente para diagnóstico.
✅ CORRETO:
try {
processPayment(order);
} catch (PaymentException e) {
logger.error("Falha ao processar pagamento. Pedido: {}, Cliente: {}, Valor: {}, Gateway: {}",
order.getId(), order.getCustomerId(), order.getTotalValue(),
paymentGateway, e);
}
Exemplo de saída JSON com stacktrace:
{
"level": "ERROR",
"timestamp": "2024-12-03T12:39:50.930-03:00",
"logger": "com.empresa.service.PaymentService",
"thread": "payment-processor-1",
"message": "Falha ao processar pagamento",
"orderId": "ORD-12345",
"customerId": "CUST-789",
"totalValue": 150.00,
"gateway": "STRIPE",
"exception": "com.empresa.exception.PaymentException",
"exceptionMessage": "Gateway timeout",
"stacktrace": "com.empresa.service.PaymentService.processPayment(PaymentService.java:145)n..."
}
Diretrizes para exceções:
- Sempre passe a exceção como último parâmetro do log
- Inclua informações contextuais (IDs, valores envolvidos)
- Use nível ERROR para exceções que afetam operações
- Use nível WARN para exceções esperadas e tratáveis
Prática 7 – Use Sempre Configuração Externa
Não especifique configurações de log via código. Use arquivos de configuração.
❌ EVITE:
// Configuração hardcoded
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.setLevel(Level.DEBUG); // Alguns frameworks permitem, mas evite!
✅ PREFIRA:
Arquivo logback-spring.xml:
<configuration>
<!-- Perfil de desenvolvimento -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.empresa.service" level="TRACE"/>
</springProfile>
<!-- Perfil de produção -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
</root>
<logger name="com.empresa.service" level="INFO"/>
</springProfile>
</configuration>
Prática 8 – Logue Processamentos em Batch
Para rotinas batch (não executadas por ação de usuários), grave início e fim com nível INFO.
Exemplo:
@Scheduled(cron = "0 0 2 * * *")
public void processMonthlyCommissions() {
logger.info("Iniciando processamento de comissões mensais");
int totalProcessed = 0;
int totalErrors = 0;
try {
List<Sale> sales = salesRepository.findPendingCommissions();
logger.info("Total de vendas a processar: {}", sales.size());
for (Sale sale : sales) {
try {
processCommission(sale);
totalProcessed++;
if (totalProcessed % 100 == 0) {
logger.info("Progresso: {} vendas processadas", totalProcessed);
}
} catch (Exception e) {
totalErrors++;
logger.error("Erro ao processar comissão da venda: {}",
sale.getId(), e);
}
}
logger.info("Processamento concluído. Sucesso: {}, Erros: {}",
totalProcessed, totalErrors);
} catch (Exception e) {
logger.error("Erro fatal no processamento de comissões", e);
throw e;
}
}
Para operações de banco de dados:
public Order saveOrder(Order order) {
logger.debug("Salvando pedido: {}", order.getId());
Order saved = orderRepository.save(order);
logger.info("Pedido {} salvo com sucesso", saved.getId());
return saved;
}
Prática 9 – Segurança: Não Logue Informações Sensíveis
NUNCA logue:
- Senhas ou tokens de autenticação
- Números de cartão de crédito
- CPF/RG completos
- Dados pessoais sensíveis (LGPD/GDPR)
- Chaves de API ou secrets
❌ PERIGOSO:
logger.info("Login: username={}, password={}", username, password);
logger.debug("Processando cartão: {}", creditCardNumber);
logger.info("Token de acesso: {}", accessToken);
✅ SEGURO:
logger.info("Login bem-sucedido: username={}", username);
logger.debug("Processando cartão terminado em: {}",
creditCardNumber.substring(creditCardNumber.length() - 4));
logger.info("Token gerado para usuário: {}", userId);
// Ou crie métodos de mascaramento
logger.debug("Dados do cliente: {}", maskSensitiveData(customer));
O uso de métodos utilitários de mascaramento para garantir que dados sensíveis nunca saiam para o log.
import static com.empresa.util.LogUtils.maskCreditCard;
import static com.empresa.util.LogUtils.maskCPF;
public void processPayment(String creditCardNumber, String cpf) {
// ❌ PERIGOSO: Expor dados completos
// logger.info("Processando: {} para o CPF: {}", creditCardNumber, cpf);
// ✅ SEGURO: Usar o wrapper de log com o método utilitário
logger.info("Processando transação. Cartão: {}, CPF: {}",
maskCreditCard(creditCardNumber),
maskCPF(cpf));
// ... lógica de processamento
}
Implementação do Utilitário de Mascaramento (Melhorado):
// com.empresa.util.LogUtils.java
public final class LogUtils {
private LogUtils() {} // Evita instanciação
public static String maskCreditCard(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 4) return "****";
// Mascara tudo, exceto os últimos 4 dígitos
return "***********" + cardNumber.substring(cardNumber.length() - 4);
}
public static String maskCPF(String cpf) {
if (cpf == null || cpf.length() < 3) return "***";
// Mascara os primeiros dígitos
return "***.***." + cpf.substring(cpf.length() - 3) + "-**";
}
}
Prática 10 – Evite Efeitos Colaterais
Logs não devem tornar a aplicação lenta. Não faça consultas ou chamadas externas na construção de mensagens de log.
❌ ERRADO:
logger.error("Erro para usuário ID: {}",
userRepository.findById(sessionId).getId()); // Consulta em cada log!
logger.info("Status do serviço externo: {}",
externalService.checkStatus()); // Chamada HTTP em cada log!
✅ CORRETO:
// Busque dados ANTES de logar
User user = userRepository.findById(sessionId);
logger.error("Erro para usuário ID: {}", user.getId());
// Ou use informações já disponíveis
logger.error("Erro para sessão: {}", sessionId);
// SLF4J só avalia parâmetros se o nível estiver habilitado
logger.debug("Detalhes do usuário: {}", user.getDetails()); // OK, só executa se DEBUG estiver ativo
O Problema (Má Prática) – Concatenação:
// ❌ Má Prática: getDadosComplexos() É CHAMADO SEMPRE!
logger.info("Dados do usuário: " + userRepository.getDadosComplexos(userId));
Explicação: O Java avalia a expressão userRepository.getDadosComplexos(userId) antes de chamar o logger.info(). Se o nível INFO estiver desabilitado, o tempo gasto é totalmente desperdiçado.
A Solução (Boa Prática) – Parametrização:
// ✅ Boa Prática: getDadosComplexos() SÓ É CHAMADO se o nível INFO estiver ativo.
logger.info("Dados do usuário: {}", userRepository.getDadosComplexos(userId));
Explicação: O logger.info() recebe apenas a String de Formato e o objeto userRepository.getDadosComplexos(userId). A função só é executada internamente pelo SLF4J/Logback se o nível INFO estiver habilitado.
Prática 11 – Não Use Métodos que Finalizam a Aplicação
Bibliotecas e serviços nunca devem finalizar a aplicação. Apenas a aplicação principal deve decidir sobre shutdown.
❌ ERRADO em bibliotecas:
if (error) {
logger.error("Erro crítico!");
System.exit(1); // NUNCA faça isso!
}
throw new RuntimeException(); // Também evite em libs
✅ CORRETO:
// Em bibliotecas: propague o erro
public void processData() throws DataProcessingException {
try {
// processamento
} catch (Exception e) {
logger.error("Erro ao processar dados", e);
throw new DataProcessingException("Falha no processamento", e);
}
}
// Na aplicação principal: decida o que fazer
public static void main(String[] args) {
try {
service.processData();
} catch (DataProcessingException e) {
logger.error("Erro fatal na aplicação", e);
System.exit(1); // OK aqui, é a aplicação principal
}
}
Prática 12 – Logue Integrações com Sistemas Externos
Quando integrando com APIs e sistemas externos, logue entrada e saída de dados.
Exemplo:
public PaymentResponse processPayment(PaymentRequest request) {
logger.info("Iniciando integração com gateway de pagamento. Pedido: {}, Valor: {}",
request.getOrderId(), request.getAmount());
// Log do request (cuidado com dados sensíveis!)
logger.debug("Request para gateway: {}", maskSensitiveFields(request));
try {
PaymentResponse response = paymentGateway.charge(request);
// Log do response
logger.info("Resposta do gateway: Status: {}, TransactionId: {}, Pedido: {}",
response.getStatus(), response.getTransactionId(), request.getOrderId());
logger.debug("Response completo: {}", response);
return response;
} catch (TimeoutException e) {
logger.error("Timeout ao processar pagamento. Pedido: {}, Tempo: {}ms",
request.getOrderId(), e.getTimeout(), e);
throw e;
} catch (Exception e) {
logger.error("Erro na integração com gateway. Pedido: {}",
request.getOrderId(), e);
throw e;
}
}
Prática 13 – Logue Argumentos e Retornos de Funções Complexas
Para funções complexas, considere logar entradas e saídas no nível DEBUG.
Exemplo:
public BigDecimal calculateDiscount(Order order, Coupon coupon) {
logger.debug("Calculando desconto. Pedido: {}, Cupom: {}, Valor original: {}",
order.getId(), coupon.getCode(), order.getTotalValue());
BigDecimal discount = BigDecimal.ZERO;
if (coupon.isPercentage()) {
discount = order.getTotalValue()
.multiply(coupon.getValue())
.divide(new BigDecimal("100"));
logger.debug("Desconto percentual aplicado: {}%", coupon.getValue());
} else {
discount = coupon.getValue();
logger.debug("Desconto fixo aplicado: {}", discount);
}
if (discount.compareTo(order.getTotalValue()) > 0) {
logger.warn("Desconto calculado ({}) maior que valor do pedido ({}). Ajustando.",
discount, order.getTotalValue());
discount = order.getTotalValue();
}
logger.debug("Desconto final calculado: {} para pedido: {}", discount, order.getId());
return discount;
}
Use MDC (Mapped Diagnostic Context) para rastreamento distribuído:
import org.slf4j.MDC;
public void processRequest(String requestId, String userId) {
// Adiciona contexto
MDC.put("requestId", requestId);
MDC.put("userId", userId);
MDC.put("traceId", UUID.randomUUID().toString());
try {
logger.info("Iniciando processamento"); // Automaticamente inclui MDC
// ... processamento
logger.info("Processamento concluído");
} finally {
// Sempre limpe o MDC
MDC.clear();
}
}
O que é MDC (Mapped Diagnostic Context)?
O MDC é um mapa de dados (chave-valor) associado à thread de execução atual. Ele permite que você injete contexto (como traceId, userId, sessionId) no início de uma requisição e tenha esses campos automaticamente anexados a todos os logs gerados por essa thread, sem precisar passá-los manualmente em cada chamada de logger.info().
Exemplo de Uso de MDC em um Filtro/Middleware (Spring Boot/Servlet Filter):
import org.slf4j.MDC;
public void filter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 1. Gerar ou Obter o ID de Rastreamento
String traceId = UUID.randomUUID().toString();
String userId = request.getHeader("X-User-ID");
// 2. Injetar o contexto no MDC
MDC.put("traceId", traceId);
MDC.put("userId", userId);
try {
// 3. Chamar o restante da cadeia de filtros/serviço
chain.doFilter(request, response);
} finally {
// 4. ESSENCIAL: Limpar o MDC ao final da requisição para evitar vazamento
// para outras threads que podem ser reutilizadas (pool de threads).
MDC.clear();
}
}
// ----------------------------------------------------------------------
// Uso no Código de Serviço (O logger inclui o contexto automaticamente!)
// ----------------------------------------------------------------------
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void createOrder(OrderDto order) {
// Log que agora contém traceId e userId, sem passá-los!
logger.info("Iniciando criação de pedido para o cliente: {}", order.getCustomerName());
// ...
logger.info("Pedido processado e salvo no banco de dados.");
// Se ocorrer um erro, o log de erro também terá o contexto completo
// logger.error("Erro no BD", exception);
}
}
Prática 14 – Monitore e Otimize Performance
Logs têm custo de I/O. Monitore o impacto e otimize quando necessário.
Estratégias de otimização:
- Use appenders assíncronos em produção
- Configure níveis apropriados por ambiente
- Evite logs em loops intensivos
- Use loggers específicos para componentes críticos
Exemplo de configuração otimizada:
<configuration>
<!-- Produção: Assíncrono + INFO -->
<springProfile name="prod">
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>false</neverBlock>
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC"/>
</root>
<!-- Componente crítico: menos logs -->
<logger name="com.empresa.highfrequency" level="WARN"/>
</springProfile>
</configuration>
Em loops intensivos:
// ❌ EVITE logs dentro de loops de alta frequência
for (int i = 0; i < 1000000; i++) {
logger.debug("Processando item: {}", i); // Milhões de logs!
process(i);
}
// ✅ PREFIRA logs periódicos
for (int i = 0; i < 1000000; i++) {
if (i % 10000 == 0) {
logger.info("Progresso: {} itens processados", i);
}
process(i);
}
Prática 15 – Analise e Gerencie Logs
Procedimentos recomendados:
- Monitoramento ativo: Configure alertas para padrões críticos
- Análise regular: Revise logs periodicamente em busca de anomalias
- Política de retenção: Defina tempo de armazenamento (geralmente 15-30 dias)
- Backup seletivo: Preserve logs de incidentes importantes
- Automação: Use ferramentas como ELK Stack, Splunk, ou DataDog
Exemplo de integração com alertas:
<!-- Logback com Sentry para alertas de erros -->
<appender name="SENTRY" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
Post-mortem de incidentes:
- Catalogar logs relevantes ao incidente
- Documentar localização dos logs para consulta futura
- Incluir análise de logs no relatório de post-mortem
Considerações Finais
O logging é uma das práticas mais fundamentais e, paradoxalmente, mais negligenciadas no desenvolvimento de software. Um sistema de logs bem implementado é a diferença entre diagnosticar um problema crítico em minutos ou passar horas (ou dias) às cegas tentando reproduzir um bug em produção.
O Valor Real dos Logs
O mecanismo de logging só gera valor quando as mensagens são estrategicamente planejadas e ativamente utilizadas. Logs efetivos servem como:
- Observabilidade em tempo real: Permitem entender o que está acontecendo no sistema sem precisar acessá-lo diretamente
- Diagnóstico rápido: Reduzem drasticamente o tempo de identificação e resolução de problemas
- Rastreabilidade: Criam uma trilha auditável de eventos e decisões do sistema
- Métricas de negócio: Fornecem dados para análise de comportamento e performance
- Prevenção proativa: Identificam problemas antes que se tornem críticos através de padrões anormais
O Equilíbrio é Fundamental
Logs demais geram ruído e degradam performance. Logs de menos deixam você no escuro quando mais precisa de informações. O segredo está no equilíbrio:
- Qualidade sobre quantidade: Um log contextual e bem escrito vale mais que dezenas de logs vagos
- Contexto é rei: Sempre inclua informações suficientes para reconstruir o cenário do problema
- Níveis apropriados: Use os níveis de log corretamente para facilitar filtros e alertas
- Pense no futuro: Quando escrever um log, imagine que você será chamado às 3h da manhã para resolver um bug. Que informações você gostaria de ter?
Logs são Parte da Arquitetura
Trate logs como cidadãos de primeira classe na sua arquitetura:
- Planeje desde o início: Adicionar logs durante o desenvolvimento é 10x mais fácil que depois
- Padronize no time: Estabeleça convenções de logging e garanta que todos sigam
- Integre com ferramentas: Use sistemas centralizados (ELK, Splunk, DataDog) para análise agregada
- Automatize alertas: Configure notificações para padrões críticos
- Revise regularmente: Logs desatualizados ou irrelevantes devem ser removidos ou ajustados
A Cultura de Logging Efetivo
Desenvolver uma cultura de logging efetivo na equipe requer:
- Educação contínua: Compartilhe exemplos de como logs ajudaram (ou falharam em ajudar) a resolver problemas
- Code review focado: Avalie a qualidade dos logs tanto quanto avalia o código
- Postmortems detalhados: Quando logs falharem em ajudar, documente o que estava faltando
- Ferramentas adequadas: Invista em ferramentas que tornem a análise de logs mais eficiente
- Feedback loops: Quando resolver um bug, verifique se os logs foram suficientes ou se precisam melhorar
Lembre-se
Gerar logs sem observá-los é desperdício de recursos. Mas observar logs bem estruturados e contextualizados é ter visão de raio-X do seu sistema em produção.
O que pode parecer tempo “perdido” escrevendo logs detalhados durante o desenvolvimento se paga exponencialmente quando você consegue diagnosticar e resolver um problema crítico em produção em 5 minutos, ao invés de horas de especulação e tentativas de reprodução.
Logs não são apenas para quando as coisas dão errado — são para entender como seu sistema realmente funciona no mundo real, com dados reais, sob condições reais. Invista tempo em fazer isso bem, e sua equipe (e seu futuro eu às 3h da manhã) agradecerão.
Próximos Passos
Se você está começando a melhorar o logging da sua aplicação:
- Escolha uma biblioteca (SLF4J + Logback é uma excelente combinação)
- Configure diferentes perfis para dev/staging/produção
- Estabeleça padrões de logging no time
- Implemente MDC para rastreamento distribuído
- Configure um sistema de análise centralizada (comece com ELK Stack)
- Defina alertas para padrões críticos
- Revise e melhore continuamente
Bons logs são um investimento que se paga todos os dias.
📚 Referências e Leitura Recomendada
Para aprofundar seu conhecimento sobre logging em Java, arquitetura de observabilidade e segurança, consulte as seguintes fontes oficiais e artigos da comunidade:
Documentação Oficial (A Base)
-
SLF4J User Manual
- A fonte definitiva sobre a fachada de logging padrão do Java. Explica em detalhes a parametrização de mensagens e a ligação (binding) com implementações.
- 🔗 https://www.slf4j.org/manual.html
-
Logback Manual: Appenders & Layouts
- Documentação essencial para entender como configurar rotação de arquivos, políticas de retenção e formatação JSON.
- 🔗 https://logback.qos.ch/manual/appenders.html
-
Apache Log4j 2: Asynchronous Loggers
- Leitura obrigatória para entender como obter a máxima performance (baixa latência) em sistemas de alto volume utilizando LMAX Disruptor.
- 🔗 https://logging.apache.org/log4j/2.x/manual/async.html
Arquitetura e Boas Práticas
-
The Twelve-Factor App: XI. Logs
- O manifesto que define os padrões modernos para aplicações nativas em nuvem (cloud-native), defendendo que logs devem ser tratados como fluxos de eventos (event streams) enviados para
stdout. - 🔗 https://12factor.net/logs
- O manifesto que define os padrões modernos para aplicações nativas em nuvem (cloud-native), defendendo que logs devem ser tratados como fluxos de eventos (event streams) enviados para
-
OWASP Logging Cheat Sheet
- Guia de segurança crucial. Detalha o que deve e o que não deve ser logado (dados sensíveis, PII) para evitar vazamentos de segurança e problemas de conformidade (GDPR/LGPD).
- 🔗 https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
-
MDC (Mapped Diagnostic Context) com SLF4J
- Tutorial prático (Baeldung) sobre como implementar rastreamento distribuído e contexto em aplicações Java.
- 🔗 https://www.baeldung.com/mdc-in-log4j-2-logback
Artigos e Livros Relacionados
-
“Release It!: Design and Deploy Production-Ready Software” (Livro)
- Autor: Michael T. Nygard.
- Embora seja sobre arquitetura geral, possui capítulos valiosos sobre monitoramento e como logs salvam (ou derrubam) sistemas em produção.
-
Artigo Original (Inspiração)
- Logs de Aplicações: Motivações e Melhores Práticas de Utilização por Valter Lobo.
- 🔗 https://dev.to/valterlobo/logs-de-aplicacoes-motivacoes-e-melhores-praticas-de-utilizacao-agi