Förbättrad Java-loggning med Mapped Diagnostic Context (MDC)

1. Översikt

I den här artikeln kommer vi att undersöka användningen av Mapped Diagnostic Context (MDC) för att förbättra applikationsloggningen.

Den grundläggande idén med Mapped Diagnostic Context är att tillhandahålla ett sätt att berika loggmeddelanden med bitar av information som inte kan vara tillgänglig i den omfattning där loggningen faktiskt sker, men det kan verkligen vara användbart för att bättre spåra genomförandet av programmet.

2. Varför använda MDC

Låt oss börja med ett exempel. Låt oss anta att vi måste skriva programvara som överför pengar. Vi skapade en överföringsklass för att representera grundläggande information: ett unikt överförings-id och avsändarens namn:

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; } } 

För att utföra överföringen måste vi använda en tjänst som stöds av ett enkelt API:

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); } 

Den beforeTransfer () och afterTransfer () metoder kan åsidosättas för att köra egen kod precis före och precis efter överföringen är klar.

Vi kommer att utnyttja föreTransfer () och efterTransfer () för att logga lite information om överföringen .

Låt oss skapa tjänsteimplementeringen:

import org.apache.log4j.Logger; import com.baeldung.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 + "."); } } 

Den viktigaste frågan att notera här är att när loggmeddelandet skapas, är det inte möjligt att komma åt överföringsobjekt - bara beloppet är tillgängligt, vilket gör det omöjligt att logga antingen transaktions-ID eller avsändaren.

Låt oss ställa in den vanliga log4j.properties- filen för att logga in på konsolen:

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 

Låt oss äntligen skapa en liten applikation som kan köra flera överföringar samtidigt via en ExecutorService :

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(); } }

Vi noterar att för att kunna använda ExecutorService måste vi sätta in körningen av Log4JTransferService i en adapter eftersom executor.submit () förväntar sig en körbar :

public class Log4JRunnable implements Runnable { private Transfer tx; public Log4JRunnable(Transfer tx) { this.tx = tx; } public void run() { log4jBusinessService.transfer(tx.getAmount()); } } 

När vi kör vår demo-applikation som hanterar flera överföringar samtidigt upptäcker vi mycket snabbt att loggen inte är användbar som vi skulle vilja ha den . Det är komplicerat att spåra genomförandet av varje överföring eftersom den enda användbara informationen som loggas är mängden överförda pengar och namnet på tråden som utför den specifika överföringen.

Dessutom är det omöjligt att skilja mellan två olika transaktioner av samma belopp som utförs av samma tråd eftersom de relaterade logglinjerna i stort sett ser likadana ut:

... 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$. ... 

Lyckligtvis kan MDC hjälpa till.

3. MDC i Log4j

Låt oss introducera MDC .

MDC i Log4j tillåter oss att fylla en kartliknande struktur med informationsstycken som är tillgängliga för appendern när loggmeddelandet faktiskt skrivs.

MDC-strukturen är internt kopplad till den exekverande tråden på samma sätt som en ThreadLocal- variabel skulle vara.

Och så är idén på hög nivå:

  1. för att fylla MDC med bitar av information som vi vill göra tillgänglig för appendern
  2. logga sedan ett meddelande
  3. och slutligen rensa MDC

Mönstret för appendern bör uppenbarligen ändras för att hämta de variabler som är lagrade i MDC.

Så låt oss sedan ändra koden enligt dessa riktlinjer:

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(); } } 

Inte överraskande används MDC.put () för att lägga till en nyckel och ett motsvarande värde i MDC medan MDC.clear () tömmer MDC.

Låt oss nu ändra log4j.properties för att skriva ut informationen som vi just har lagrat i MDC. Det räcker att ändra omvandlingsmönstret med platshållaren % X {} för varje post i MDC som vi vill logga in:

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

Nu, om vi kör applikationen, kommer vi att notera att varje rad också innehåller informationen om transaktionen som behandlas vilket gör det mycket lättare för oss att spåra körningen av applikationen:

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 i Log4j2

Samma funktion finns också i Log4j2, så låt oss se hur man använder den.

Låt oss först ställa in en TransferService- underklass som loggar med 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); } } 

Låt oss sedan ändra koden som använder MDC, som faktiskt kallas ThreadContext i 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(); } } 

Återigen lägger ThreadContext.put () till en post i MDC och ThreadContext.clearAll () tar bort alla befintliga poster.

Vi saknar fortfarande log4j2.xml- filen för att konfigurera loggningen. Som vi kan notera är syntaxen för att specificera vilka MDC-poster som ska loggas densamma som den som används i Log4j:

Återigen, låt oss köra applikationen och vi ser MDC-informationen skrivs ut i loggen:

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 i SLF4J / Logback

MDC finns också i SLF4J under förutsättning att det stöds av det underliggande loggningsbiblioteket.

Både Logback och Log4j stöder MDC som vi just sett, så vi behöver inget speciellt för att använda den med en standarduppsättning.

Let's prepare the usual TransferService subclass, this time using the 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); } } 

Let's now use the SLF4J's flavor of MDC. In this case, the syntax and semantics are the same as that in 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(); } } 

We have to provide the Logback configuration file, logback.xml:

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

Again, we'll see that the information in the MDC is properly added to the logged messages, even though this information is not explicitly provided in the log.info() method:

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

It is worth noting that in case we set up the SLF4J back-end to a logging system that does not support MDC, all the related invocations will be simply skipped without side effects.

6. MDC and Thread Pools

MDC implementations are usually using ThreadLocals to store the contextual information. That's an easy and reasonable way to achieve thread-safety. However, we should be careful using MDC with thread pools.

Let's see how the combination of ThreadLocal-based MDCs and thread pools can be dangerous:

  1. We get a thread from the thread pool.
  2. Then we store some contextual information in MDC using MDC.put() or ThreadContext.put().
  3. We use this information in some logs and somehow we forgot to clear the MDC context.
  4. The borrowed thread comes back to the thread pool.
  5. After a while, the application gets the same thread from the pool.
  6. Since we didn't clean up the MDC last time, this thread still owns some data from the previous execution.

This may cause some unexpected inconsistencies between executions. One way to prevent this is to always remember to clean up the MDC context at the end of each execution. This approach usually needs rigorous human supervision and, therefore, is error-prone.

Another approach is to use ThreadPoolExecutor hooks and perform necessary cleanups after each execution. To do that, we can extend the ThreadPoolExecutor class and override the afterExecute() hook:

public class MdcAwareThreadPoolExecutor extends ThreadPoolExecutor { public MdcAwareThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } @Override protected void afterExecute(Runnable r, Throwable t) { System.out.println("Cleaning the MDC context"); MDC.clear(); org.apache.log4j.MDC.clear(); ThreadContext.clearAll(); } }

This way, the MDC cleanup would happen after each normal or exceptional execution automatically. So, there is no need to do it manually:

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

Nu kan vi skriva om samma demo med vårt nya genomförandeimplementering:

ExecutorService executor = new MdcAwareThreadPoolExecutor(3, 3, 0, MINUTES, new LinkedBlockingQueue(), Thread::new, new AbortPolicy()); TransactionFactory transactionFactory = new TransactionFactory(); for (int i = 0; i < 10; i++) { Transfer tx = transactionFactory.newInstance(); Runnable task = new Slf4jRunnable(tx); executor.submit(task); } executor.shutdown();

7. Slutsats

MDC har många applikationer, främst i scenarier där körningen av flera olika trådar orsakar sammanflätade loggmeddelanden som annars skulle vara svåra att läsa.

Och som vi har sett stöds det av tre av de mest använda loggningsramarna i Java.

Som vanligt hittar du källorna på GitHub.