Log Java aprimorado com contexto de diagnóstico mapeado (MDC)

Log Java aprimorado com contexto de diagnóstico mapeado (MDC)

1. Visão geral

Neste artigo, exploraremos o uso deMapped Diagnostic Context (MDC) para melhorar o log do aplicativo.

A ideia básica deMapped Diagnostic Context é fornecer uma maneira de enriquecer as mensagens de log com informações que poderiam não estar disponíveis no escopo onde o log realmente ocorre, mas que podem ser úteis para acompanhar melhor a execução do programa .

2. Por que usar o MDC

Vamos começar com um exemplo. Vamos supor que tenhamos que escrever um software que transfira dinheiro. Configuramos uma classeTransfer para representar algumas informações básicas: um ID de transferência exclusivo e o nome do remetente:

public class Transfer {
    private String transactionId;
    private String sender;
    private Long amount;

    public Transfer(String transactionId, String sender, long amount) {
        this.transactionId = transactionId;
        this.sender = sender;
        this.amount = amount;
    }

    public String getSender() {
        return sender;
    }

    public String getTransactionId() {
        return transactionId;
    }

    public Long getAmount() {
        return amount;
    }
}

Para realizar a transferência - precisamos usar um serviço apoiado por uma API simples:

public abstract class TransferService {

    public boolean transfer(long amount) {
        // connects to the remote service to actually transfer money
    }

    abstract protected void beforeTransfer(long amount);

    abstract protected void afterTransfer(long amount, boolean outcome);
}

Os métodosbeforeTransfer()eafterTransfer() podem ser substituídos para executar o código personalizado antes e logo após a conclusão da transferência.

Vamos alavancarbeforeTransfer()eafterTransfer() paralog some information about the transfer.

Vamos criar a implementação do serviço:

import org.apache.log4j.Logger;
import com.example.mdc.TransferService;

public class Log4JTransferService extends TransferService {
    private Logger logger = Logger.getLogger(Log4JTransferService.class);

    @Override
    protected void beforeTransfer(long amount) {
        logger.info("Preparing to transfer " + amount + "$.");
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info(
          "Has transfer of " + amount + "$ completed successfully ? " + outcome + ".");
    }
}

O principal problema a ser observado aqui é que -when the log message is created, it is not possible to access the Transfer object - apenas a quantidade está acessível tornando impossível registrar o id da transação ou o remetente

Vamos configurar o arquivolog4j.properties usual para fazer logon no console:

log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=%-4r [%t] %5p %c %x - %m%n
log4j.rootLogger = TRACE, consoleAppender

Vamos finalmente configurar um pequeno aplicativo que é capaz de executar várias transferências ao mesmo tempo por meio de umExecutorService:

public class TransferDemo {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        TransactionFactory transactionFactory = new TransactionFactory();
        for (int i = 0; i < 10; i++) {
            Transfer tx = transactionFactory.newInstance();
            Runnable task = new Log4JRunnable(tx);
            executor.submit(task);
        }
        executor.shutdown();
    }
}

Notamos que, para usarExecutorService, precisamos envolver a execução deLog4JTransferService em um adaptador porqueexecutor.submit() espera umRunnable:

public class Log4JRunnable implements Runnable {
    private Transfer tx;

    public Log4JRunnable(Transfer tx) {
        this.tx = tx;
    }

    public void run() {
        log4jBusinessService.transfer(tx.getAmount());
    }
}

Quando executamos nosso aplicativo de demonstração que gerencia várias transferências ao mesmo tempo, descobrimos muito rapidamente quethe log is not useful as we would like it to be. É complexo rastrear a execução de cada transferência porque a única informação útil sendo registrada é a quantidade de dinheiro transferida e o nome do thread que está executando essa transferência específica.

Além do mais, é impossível distinguir entre duas transações diferentes da mesma quantidade executadas pelo mesmo encadeamento porque as linhas de registro relacionadas parecem substancialmente as mesmas:

...
519  [pool-1-thread-3]  INFO Log4JBusinessService
  - Preparing to transfer 1393$.
911  [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1065$ completed successfully ? true.
911  [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1189$.
989  [pool-1-thread-1]  INFO Log4JBusinessService
  - Has transfer of 1350$ completed successfully ? true.
989  [pool-1-thread-1]  INFO Log4JBusinessService
  - Preparing to transfer 1178$.
1245 [pool-1-thread-3]  INFO Log4JBusinessService
  - Has transfer of 1393$ completed successfully ? true.
1246 [pool-1-thread-3]  INFO Log4JBusinessService
  - Preparing to transfer 1133$.
1507 [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1189$ completed successfully ? true.
1508 [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1907$.
1639 [pool-1-thread-1]  INFO Log4JBusinessService
  - Has transfer of 1178$ completed successfully ? true.
1640 [pool-1-thread-1]  INFO Log4JBusinessService
  - Preparing to transfer 674$.
...

Felizmente,MDC pode ajudar.

3. MDC no Log4j

Vamos apresentarMDC.

MDC no Log4j nos permite preencher uma estrutura semelhante a um mapa com pedaços de informações que são acessíveis ao appender quando a mensagem de log é realmente escrita.

A estrutura do MDC é anexada internamente ao thread em execução da mesma forma que uma variávelThreadLocal seria.

E assim, a ideia de alto nível é:

  1. para preencher o MDC com informações que queremos disponibilizar ao solicitante

  2. então registre uma mensagem

  3. e, finalmente, limpe o MDC

O padrão do appender deve obviamente ser alterado para recuperar as variáveis ​​armazenadas no MDC.

Então, vamos mudar o código de acordo com estas diretrizes:

import org.apache.log4j.MDC;

public class Log4JRunnable implements Runnable {
    private Transfer tx;
    private static Log4JTransferService log4jBusinessService = new Log4JTransferService();

    public Log4JRunnable(Transfer tx) {
        this.tx = tx;
    }

    public void run() {
        MDC.put("transaction.id", tx.getTransactionId());
        MDC.put("transaction.owner", tx.getSender());
        log4jBusinessService.transfer(tx.getAmount());
        MDC.clear();
    }
}

Não é novidade queMDC.put() é usado para adicionar uma chave e um valor correspondente no MDC enquantoMDC.clear() esvazia o MDC.

Vamos agora alterar olog4j.properties para imprimir as informações que acabamos de armazenar no MDC. Basta alterar o padrão de conversão, usando o marcador%X\{} para cada entrada contida no MDC que gostaríamos de registrar:

log4j.appender.consoleAppender.layout.ConversionPattern=
  %-4r [%t] %5p %c{1} %x - %m - tx.id=%X{transaction.id} tx.owner=%X{transaction.owner}%n

Agora, se executarmos o aplicativo, notaremos que cada linha carrega também as informações sobre a transação que está sendo processada, tornando muito mais fácil para nós rastrearmos a execução do aplicativo:

638  [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1104$ completed successfully ? true. - tx.id=2 tx.owner=Marc
638  [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1685$. - tx.id=4 tx.owner=John
666  [pool-1-thread-1]  INFO Log4JBusinessService
  - Has transfer of 1985$ completed successfully ? true. - tx.id=1 tx.owner=Marc
666  [pool-1-thread-1]  INFO Log4JBusinessService
  - Preparing to transfer 958$. - tx.id=5 tx.owner=Susan
739  [pool-1-thread-3]  INFO Log4JBusinessService
  - Has transfer of 783$ completed successfully ? true. - tx.id=3 tx.owner=Samantha
739  [pool-1-thread-3]  INFO Log4JBusinessService
  - Preparing to transfer 1024$. - tx.id=6 tx.owner=John
1259 [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1685$ completed successfully ? false. - tx.id=4 tx.owner=John
1260 [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1667$. - tx.id=7 tx.owner=Marc

4. MDC no Log4j2

O mesmo recurso está disponível no Log4j2 também, então vamos ver como usá-lo.

Vamos primeiro configurar uma subclasseTransferService que registra usando Log4j2:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4J2TransferService extends TransferService {
    private static final Logger logger = LogManager.getLogger();

    @Override
    protected void beforeTransfer(long amount) {
        logger.info("Preparing to transfer {}$.", amount);
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info("Has transfer of {}$ completed successfully ? {}.", amount, outcome);
    }
}

Vamos então mudar o código que usa o MDC, que na verdade é chamado deThreadContext no Log4j2:

import org.apache.log4j.MDC;

public class Log4J2Runnable implements Runnable {
    private final Transaction tx;
    private Log4J2BusinessService log4j2BusinessService = new Log4J2BusinessService();

    public Log4J2Runnable(Transaction tx) {
        this.tx = tx;
    }

    public void run() {
        ThreadContext.put("transaction.id", tx.getTransactionId());
        ThreadContext.put("transaction.owner", tx.getOwner());
        log4j2BusinessService.transfer(tx.getAmount());
        ThreadContext.clearAll();
    }
}

Novamente,ThreadContext.put() adiciona uma entrada no MDC eThreadContext.clearAll() remove todas as entradas existentes.

Ainda sentimos falta do arquivolog4j2.xml para configurar o log. Como podemos observar, a sintaxe para especificar quais entradas do MDC devem ser registradas é a mesma que a usada no Log4j:


    
        
            
        
    
    
        
        
            
        
    

Novamente, vamos executar o aplicativo e veremos as informações do MDC sendo impressas no log:

1119 [pool-1-thread-3]  INFO Log4J2BusinessService
  - Has transfer of 1198$ completed successfully ? true. - tx.id=3 tx.owner=Samantha
1120 [pool-1-thread-3]  INFO Log4J2BusinessService
  - Preparing to transfer 1723$. - tx.id=5 tx.owner=Samantha
1170 [pool-1-thread-2]  INFO Log4J2BusinessService
  - Has transfer of 701$ completed successfully ? true. - tx.id=2 tx.owner=Susan
1171 [pool-1-thread-2]  INFO Log4J2BusinessService
  - Preparing to transfer 1108$. - tx.id=6 tx.owner=Susan
1794 [pool-1-thread-1]  INFO Log4J2BusinessService
  - Has transfer of 645$ completed successfully ? true. - tx.id=4 tx.owner=Susan

5. MDC no SLF4J / Logback

MDC também está disponível em SLF4J, sob a condição de que seja suportado pela biblioteca de registro subjacente.

Tanto Logback quanto Log4j suportam MDC como acabamos de ver, então não precisamos de nada especial para usá-lo com uma configuração padrão.

Vamos preparar a subclasseTransferService usual, desta vez usando a Simple Logging Facade for Java:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class Slf4TransferService extends TransferService {
    private static final Logger logger = LoggerFactory.getLogger(Slf4TransferService.class);

    @Override
    protected void beforeTransfer(long amount) {
        logger.info("Preparing to transfer {}$.", amount);
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info("Has transfer of {}$ completed successfully ? {}.", amount, outcome);
    }
}

Vamos agora usar o sabor de MDC do SLF4J. Nesse caso, a sintaxe e a semântica são iguais às de log4j:

import org.slf4j.MDC;

public class Slf4jRunnable implements Runnable {
    private final Transaction tx;

    public Slf4jRunnable(Transaction tx) {
        this.tx = tx;
    }

    public void run() {
        MDC.put("transaction.id", tx.getTransactionId());
        MDC.put("transaction.owner", tx.getOwner());
        new Slf4TransferService().transfer(tx.getAmount());
        MDC.clear();
    }
}

Temos que fornecer o arquivo de configuração de Logbacklogback.xml:


    
        
            %-4r [%t] %5p %c{1} - %m - tx.id=%X{transaction.id} tx.owner=%X{transaction.owner}%n
    
    
    
        
    

Novamente, veremos que as informações no MDC são adicionadas corretamente às mensagens registradas, mesmo que essas informações não sejam explicitamente fornecidas no método log.info ():

1020 [pool-1-thread-3]  INFO c.b.m.s.Slf4jBusinessService
  - Has transfer of 1869$ completed successfully ? true. - tx.id=3 tx.owner=John
1021 [pool-1-thread-3]  INFO c.b.m.s.Slf4jBusinessService
  - Preparing to transfer 1303$. - tx.id=6 tx.owner=Samantha
1221 [pool-1-thread-1]  INFO c.b.m.s.Slf4jBusinessService
  - Has transfer of 1498$ completed successfully ? true. - tx.id=4 tx.owner=Marc
1221 [pool-1-thread-1]  INFO c.b.m.s.Slf4jBusinessService
  - Preparing to transfer 1528$. - tx.id=7 tx.owner=Samantha
1492 [pool-1-thread-2]  INFO c.b.m.s.Slf4jBusinessService
  - Has transfer of 1110$ completed successfully ? true. - tx.id=5 tx.owner=Samantha
1493 [pool-1-thread-2]  INFO c.b.m.s.Slf4jBusinessService
  - Preparing to transfer 644$. - tx.id=8 tx.owner=John

Vale ressaltar que, caso configuremos o back-end do SLF4J para um sistema de registro que não suporte o MDC, todas as chamadas relacionadas serão simplesmente ignoradas sem efeitos colaterais.

6. Conclusão

O MDC tem muitos aplicativos, principalmente em cenários nos quais a execução de vários threads diferentes causa mensagens de log intercaladas que, de outra forma, seriam difíceis de ler.

E, como vimos, é suportado por três das estruturas de registro mais amplamente usadas em Java.

Como de costume, você encontrará as fontesover on GitHub.