1. Segurança de threads no logging
Em programas single-thread tudo é simples: uma thread escreve os logs e ninguém atrapalha. Já em aplicações reais — serviços web, microsserviços — dezenas e centenas de threads rodam ao mesmo tempo. Imagine várias pessoas escrevendo com caneta na mesma linha de um caderno ao mesmo tempo — o resultado seria, para dizer o mínimo, ilegível.
Segurança de threads (thread safety) — é a garantia de que mesmo se 100500 threads escreverem logs simultaneamente, as mensagens não se misturem, não se fundam e não se percam.
Como isso é implementado nas bibliotecas?
As bibliotecas modernas de logging (Log4j 2, Logback, java.util.logging) são projetadas desde o início para serem thread-safe. Isso significa:
- Cada thread pode chamar com segurança os métodos do logger.
- Dentro da biblioteca são usadas sincronização e filas para que as mensagens não atrapalhem umas às outras.
- Mesmo que várias threads escrevam no mesmo arquivo ao mesmo tempo, os logs não se confundem.
IMPORTANTE: O próprio logger (por exemplo, o objeto Logger de SLF4J ou Log4j) pode ser usado como um campo static final em qualquer classe — isso não causará problemas de concorrência.
Exemplo
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MultiThreadedLoggerExample {
private static final Logger logger = LoggerFactory.getLogger(MultiThreadedLoggerExample.class);
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
logger.info("Thread {} escreve mensagem {}", Thread.currentThread().getName(), i);
}
};
Thread t1 = new Thread(task, "Primeiro");
Thread t2 = new Thread(task, "Segundo");
t1.start();
t2.start();
}
}
Nos logs você verá mensagens organizadas de ambas as threads — sem bagunça nem sobreposição.
2. Contexto de logging: MDC (Mapped Diagnostic Context)
Imagine: sua aplicação processa centenas de requisições simultaneamente, cada uma na sua própria thread. As mensagens piscam nos logs, mas não está claro o que pertence a qual requisição. Você quer ver não apenas “o que aconteceu”, mas com quem e em qual requisição isso aconteceu.
MDC (Mapped Diagnostic Context) é um mecanismo especial que permite “anexar” informações adicionais aos logs, vinculadas à thread atual. Todas as mensagens que a thread escreve recebem automaticamente esses dados adicionais.
Exemplo: registrando o identificador da requisição
Em uma aplicação web, cada requisição pode receber um ID único (por exemplo, um UUID). Com MDC, esse ID será adicionado automaticamente a todos os logs que a thread que atende a requisição escrever.
Como isso fica no código (SLF4J + Logback):
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;
public class MdcExample {
private static final Logger logger = LoggerFactory.getLogger(MdcExample.class);
public static void main(String[] args) {
Runnable task = () -> {
// Geramos um identificador de requisição único
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId); // adicionamos no MDC
logger.info("Processando a requisição");
doSomeWork();
logger.info("Processamento concluído");
MDC.clear(); // limpar obrigatoriamente após finalizar!
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
static void doSomeWork() {
logger.debug("Executando o trabalho...");
}
}
Configuração do formato do log (por exemplo, logback.xml):
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [requestId=%X{requestId}] - %msg%n</pattern>
</encoder>
Resultado:
12:01:23 [Thread-1] INFO MdcExample [requestId=ad8d...f3] - Processando a requisição
12:01:23 [Thread-1] DEBUG MdcExample [requestId=ad8d...f3] - Executando o trabalho...
12:01:23 [Thread-1] INFO MdcExample [requestId=ad8d...f3] - Processamento concluído
Importante!
- MDC funciona apenas no escopo de uma única thread. Se você passar o trabalho para outra thread (por exemplo, via pool de threads), será necessário transmitir manualmente os valores do MDC (ou usar bibliotecas especiais que façam isso automaticamente).
- Não se esqueça de limpar o MDC! Se não limpar, os dados podem “vazar” para a próxima requisição na mesma thread (por exemplo, em um pool de threads do servidor web). Use MDC.clear() no bloco finally.
3. Logging em aplicações web
Uma aplicação web não é apenas um programa que inicia e fica rodando. É uma verdadeira esteira: as requisições chegam, são processadas e as respostas saem. E tudo isso — simultaneamente, às centenas. Aqui o logging não é luxo, é necessidade!
O que logar em aplicações web?
- Requisições e respostas HTTP: método, URL, parâmetros, status da resposta, tempo de processamento.
- Erros e exceções: todas as falhas inesperadas, stack trace.
- Eventos de negócio: cadastro, login, criação de pedido, pagamento etc.
- Detalhes técnicos: interação com banco de dados, serviços externos, tempo de execução de operações.
Regra principal: logue de modo que, daqui a um mês, quando algo quebrar às 3 da manhã, você consiga entender o que deu errado.
Exemplo: logging de requisição HTTP (Spring Boot)
A forma mais simples é usar um filtro ou aspecto que registrará cada requisição de entrada.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Component
public class RequestLoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
HttpServletRequest httpRequest = (HttpServletRequest) request;
logger.info("Requisição: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI());
long start = System.currentTimeMillis();
try {
chain.doFilter(request, response); // seguindo a cadeia (até o controller)
} finally {
long duration = System.currentTimeMillis() - start;
logger.info("Resposta enviada, tempo de processamento: {} ms", duration);
MDC.clear();
}
}
}
Logging de erros e exceções
Em frameworks web (por exemplo, Spring) é comum usar handlers de erro especiais (@ExceptionHandler) para registrar de forma adequada todas as falhas inesperadas.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public String handleException(Exception ex) {
logger.error("Ocorreu um erro: ", ex); // registramos com o stack trace completo!
return "error"; // retornamos a página de erro
}
}
Integração com frameworks web
Quase todos os frameworks web modernos (Spring, Jakarta EE, Micronaut e outros) se integram com loggers “prontos para uso”. Geralmente basta adicionar a dependência SLF4J/Logback ao projeto — e todas as mensagens padrão (inicialização da aplicação, processamento de requisições, erros) serão logadas automaticamente.
4. Prática: exemplo de logging em uma tarefa multithread
Vamos adicionar ao nosso aplicativo didático (por exemplo, um serviço de processamento de pedidos) o processamento multithread e ver como o logging ajuda a manter a cabeça no lugar.
Exemplo: processamento de pedidos em múltiplas threads com MDC
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class OrderProcessingApp {
private static final Logger logger = LoggerFactory.getLogger(OrderProcessingApp.class);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
final int orderId = i;
executor.submit(() -> {
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
try {
logger.info("Iniciando o processamento do pedido {}", orderId);
processOrder(orderId);
logger.info("Pedido {} processado com sucesso", orderId);
} catch (Exception ex) {
logger.error("Erro ao processar o pedido " + orderId, ex);
} finally {
MDC.clear();
}
});
}
executor.shutdown();
}
static void processOrder(int orderId) throws InterruptedException {
if (orderId % 2 == 0) {
throw new RuntimeException("Simulação de erro para pedido par");
}
Thread.sleep(500); // simulação de trabalho
}
}
O que acontece:
- Cada pedido é processado em uma thread separada.
- Para cada thread é criado um requestId exclusivo (via MDC).
- Todos os logs de um pedido podem ser encontrados por esse identificador.
- Os erros são logados com a stack completa.
O formato do log é configurado para mostrar o requestId.
5. Nuances e particularidades importantes
- Threads, pools e MDC. Se você trabalha com pools de threads (e provavelmente trabalha), lembre-se: as threads do pool são reutilizadas! Se esquecer de limpar o MDC, dados de uma requisição podem parar nos logs de outra. Sempre chame MDC.clear() no final do trabalho.
- MDC e tarefas assíncronas. Em frameworks web assíncronos (por exemplo, Spring WebFlux) o MDC nem sempre funciona “pronto para uso”, porque o processamento da requisição pode saltar entre threads. Para esses casos existem extensões ou adaptadores específicos.
- Logging em microsserviços. Em arquitetura de microsserviços, é comum logar não apenas o identificador local da requisição, mas também o global (traceId), que é propagado entre serviços. Isso permite rastrear o caminho da requisição por todo o sistema (rastreamento distribuído). Para isso são usados com frequência sistemas como Zipkin, Jaeger, OpenTelemetry.
6. Demonstração: diferença entre System.out.println e logging
System.out.println — apenas imprime uma string no console. Em um ambiente multithread:
- As mensagens podem se misturar.
- Não há informação sobre tempo, thread, nível, contexto.
- Não é possível configurar saída para arquivo, formato, filtragem por nível.
O logger — escreve mensagens estruturadas, considera threads, níveis, formato e suporta saída para diferentes destinos (arquivo, console, rede).
Exemplo de comparação
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PrintVsLogger {
private static final Logger logger = LoggerFactory.getLogger(PrintVsLogger.class);
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 3; i++) {
System.out.println("System.out: " + Thread.currentThread().getName() + " passo " + i);
logger.info("Logger: passo {}", i);
}
};
new Thread(task, "T1").start();
new Thread(task, "T2").start();
}
}
Conclusão:
- System.out — as mensagens podem sair embaralhadas, sem tempo e sem nível.
- Logger — cada mensagem contém tempo, thread, nível; é possível filtrar e encontrar rapidamente o que precisa.
7. Erros típicos ao logar em aplicações multithread e web
Erro nº 1: usar System.out.println em vez de logger. Em ambiente multithread isso leva a “bagunça” no console, impossibilidade de filtrar mensagens e perda de informações de contexto.
Erro nº 2: ignorar o MDC ou usá-lo incorretamente. Se não usar MDC para transmitir o identificador da requisição/usuário, os logs se tornam sem sentido — é impossível entender a qual requisição o erro pertence. Se esquecer de limpar o MDC, os dados podem “vazar” para outra requisição.
Erro nº 3: criar o logger como variável local. Prefira private static final Logger — assim o logger é criado uma única vez por classe, não consome memória à toa e reduz o risco de erros.
Erro nº 4: logar dados sensíveis. Não devem ir para os logs senhas, cartões bancários, dados pessoais — isso é uma violação de segurança!
Erro nº 5: logar apenas “INFO” ou apenas “ERROR”. Use níveis adequados: DEBUG para depuração, INFO para eventos de negócio, ERROR para erros. Não escreva tudo em um único nível — caso contrário os logs perdem o sentido.
Erro nº 6: não logar o stack trace das exceções. Se escrever apenas logger.error("Erro: " + ex.getMessage()), perde-se informação sobre a causa do erro. Sempre logue a exceção completa: logger.error("Erro", ex).
Erro nº 7: loggers caseiros sem segurança de threads. Se alguém decidir “fazer seu próprio logger” sem sincronização — em ambiente multithread isso quase garante perda ou corrupção de logs.
GO TO FULL VERSION