Verbesserte Java-Protokollierung mit MDC (Mapped Diagnostic Context)

Verbesserte Java-Protokollierung mit zugeordnetem Diagnosekontext (Mapped Diagnostic Context, MDC)

1. Überblick

In diesem Artikel werden wir die Verwendung vonMapped Diagnostic Context (MDC) untersuchen, um die Anwendungsprotokollierung zu verbessern.

Die Grundidee vonMapped Diagnostic Context besteht darin, eine Möglichkeit bereitzustellen, Protokollnachrichten mit Informationen anzureichern, die in dem Bereich, in dem die Protokollierung tatsächlich stattfindet, möglicherweise nicht verfügbar sind. Dies kann jedoch hilfreich sein, um die Ausführung des Programms besser zu verfolgen .

2. Gründe für die Verwendung von MDC

Beginnen wir mit einem Beispiel. Nehmen wir an, wir müssen eine Software schreiben, die Geld überweist. Wir haben eineTransfer-Klasse eingerichtet, um einige grundlegende Informationen darzustellen: eine eindeutige Übertragungs-ID und den Namen des Absenders:

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

Um die Übertragung durchzuführen, müssen wir einen Dienst verwenden, der durch eine einfache API unterstützt wird:

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

Die MethodenbeforeTransfer() undafterTransfer() können überschrieben werden, um benutzerdefinierten Code unmittelbar vor und unmittelbar nach Abschluss der Übertragung auszuführen.

Wir werdenbeforeTransfer() undafterTransfer() auflog some information about the transfer setzen.

Erstellen wir die Service-Implementierung:

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 + ".");
    }
}

Das Hauptproblem hierbei ist, dass -when the log message is created, it is not possible to access the Transfer object - nur der Betrag verfügbar ist, sodass weder die Transaktions-ID noch der Absender protokolliert werden können

Richten Sie die üblichelog4j.properties-Datei für die Anmeldung an der Konsole ein:

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

Lassen Sie uns endlich eine kleine Anwendung einrichten, die mehrere Übertragungen gleichzeitig überExecutorServiceausführen kann:

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

Wir stellen fest, dass wir zur Verwendung vonExecutorService die Ausführung vonLog4JTransferService in einen Adapter einschließen müssen, daexecutor.submit() einRunnable erwartet:

public class Log4JRunnable implements Runnable {
    private Transfer tx;

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

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

Wenn wir unsere Demo-Anwendung ausführen, die mehrere Übertragungen gleichzeitig verwaltet, stellen wir sehr schnell fest, dassthe log is not useful as we would like it to be. Es ist komplex, die Ausführung jeder Übertragung zu verfolgen, da die einzigen nützlichen Informationen, die protokolliert werden, der überwiesene Geldbetrag und der Name des Threads sind, der diese bestimmte Übertragung ausführt.

Darüber hinaus ist es unmöglich, zwischen zwei verschiedenen Transaktionen desselben Betrags zu unterscheiden, die von demselben Thread ausgeführt werden, da die zugehörigen Protokollzeilen im Wesentlichen gleich aussehen:

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

Zum Glück könnenMDC helfen.

3. MDC in Log4j

Lassen Sie unsMDC einführen.

MDC in Log4j ermöglicht es uns, eine kartenähnliche Struktur mit Informationen zu füllen, auf die der Appender zugreifen kann, wenn die Protokollnachricht tatsächlich geschrieben wird.

Die MDC-Struktur wird intern an den ausführenden Thread angehängt, so wie es eineThreadLocal-Variable wäre.

Und so lautet die übergeordnete Idee:

  1. das MDC mit Informationen zu füllen, die wir dem Appender zur Verfügung stellen wollen

  2. dann eine Nachricht aufzeichnen

  3. und schließlich löschen Sie die MDC

Das Muster des Appenders sollte offensichtlich geändert werden, um die im MDC gespeicherten Variablen abzurufen.

Ändern wir dann den Code gemäß den folgenden Richtlinien:

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

Es überrascht nicht, dassMDC.put() verwendet wird, um einen Schlüssel und einen entsprechenden Wert in der MDC hinzuzufügen, währendMDC.clear() die MDC leert.

Ändern Sie nun dielog4j.properties, um die Informationen zu drucken, die wir gerade im MDC gespeichert haben. Es reicht aus, das Konvertierungsmuster zu ändern, indem der Platzhalter%X\{}für jeden Eintrag in der MDC verwendet wird, die protokolliert werden soll:

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

Wenn wir nun die Anwendung ausführen, werden wir feststellen, dass jede Zeile auch die Informationen über die zu verarbeitende Transaktion enthält, was es uns erheblich erleichtert, die Ausführung der Anwendung zu verfolgen:

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

Die gleiche Funktion ist auch in Log4j2 verfügbar. Schauen wir uns also an, wie Sie sie verwenden.

Richten wir zunächst eineTransferService-Unterklasse ein, die mit Log4j2 protokolliert:

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

Ändern wir dann den Code, der den MDC verwendet, der in Log4j2 tatsächlichThreadContext heißt:

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

Wiederum fügtThreadContext.put() einen Eintrag im MDC hinzu undThreadContext.clearAll() entfernt alle vorhandenen Einträge.

Wir vermissen immer noch dielog4j2.xml-Datei, um die Protokollierung zu konfigurieren. Wie wir feststellen können, ist die Syntax zum Angeben der zu protokollierenden MDC-Einträge dieselbe wie die in Log4j verwendete:


    
        
            
        
    
    
        
        
            
        
    

Führen Sie die Anwendung erneut aus, und die MDC-Informationen werden im Protokoll gedruckt:

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

MDC ist auch in SLF4J verfügbar, sofern es von der zugrunde liegenden Protokollierungsbibliothek unterstützt wird.

Sowohl Logback als auch Log4j unterstützen MDC, wie wir gerade gesehen haben. Daher benötigen wir nichts Besonderes, um es mit einem Standard-Setup zu verwenden.

Bereiten wir die übliche UnterklasseTransferServicevor, diesmal mithilfe der Simple Logging Facade für 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);
    }
}

Verwenden wir jetzt die MDC-Variante des SLF4J. In diesem Fall sind Syntax und Semantik identisch mit denen von 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();
    }
}

Wir müssen die Logback-Konfigurationsdateilogback.xml bereitstellen:


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

Wieder werden wir sehen, dass die Informationen im MDC ordnungsgemäß zu den protokollierten Nachrichten hinzugefügt werden, obwohl diese Informationen in der log.info () -Methode nicht explizit angegeben werden:

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

Es ist erwähnenswert, dass im Falle, dass wir das SLF4J-Back-End für ein Protokollierungssystem einrichten, das MDC nicht unterstützt, alle zugehörigen Aufrufe einfach ohne Nebenwirkungen übersprungen werden.

6. Fazit

MDC hat viele Anwendungen, hauptsächlich in Szenarien, in denen die Ausführung mehrerer unterschiedlicher Threads verschachtelte Protokollnachrichten verursacht, die ansonsten schwer zu lesen wären.

Und wie wir gesehen haben, wird es von drei der am häufigsten verwendeten Protokollierungsframeworks in Java unterstützt.

Wie üblich finden Sie die Quellenover on GitHub.