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 é:
-
para preencher o MDC com informações que queremos disponibilizar ao solicitante
-
então registre uma mensagem
-
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.