CodeGym /Cursos /JAVA 25 SELF /Logging em aplicações multithread e web

Logging em aplicações multithread e web

JAVA 25 SELF
Nível 63 , Lição 2
Disponível

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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION