Verbeterde Java-logboekregistratie met Mapped Diagnostic Context (MDC)

1. Overzicht

In dit artikel zullen we het gebruik van Toegewezen diagnostische context (MDC) om de logboekregistratie van toepassingen te verbeteren.

Het basisidee van Toegewezen diagnostische context is om een ​​manier te bieden om logboekberichten te verrijken met stukjes informatie die mogelijk niet beschikbaar zijn in het bereik waar de logboekregistratie daadwerkelijk plaatsvindt, maar die inderdaad nuttig kan zijn om de uitvoering van het programma beter te volgen.

2. Waarom MDC gebruiken

Laten we beginnen met een voorbeeld. Stel dat we software moeten schrijven die geld overmaakt. We hebben een Overdracht klasse om wat basisinformatie weer te geven: een uniek overdrachts-ID en de naam van de afzender:

openbare klasse Transfer {private String transactionId; privé String-afzender; privé Long bedrag; openbare overdracht (String transactionId, String afzender, lang bedrag) {this.transactionId = transactionId; this.sender = afzender; this.amount = bedrag; } public String getSender () {retourzender; } public String getTransactionId () {return transactionId; } public Long getAmount () {retourbedrag; }} 

Om de overdracht uit te voeren, hebben we een service nodig die wordt ondersteund door een eenvoudige API:

openbare abstracte klasse TransferService {openbare booleaanse overdracht (lang bedrag) {// maakt verbinding met de service op afstand om daadwerkelijk geld over te maken} abstracte beschermde leegte beforeTransfer (lang bedrag); abstract protected void afterTransfer (lang bedrag, booleaanse uitkomst); } 

De beforeTransfer () en afterTransfer () methoden kunnen worden overschreven om aangepaste code uit te voeren vlak voordat en direct nadat de overdracht is voltooid.

We gaan profiteren beforeTransfer () en afterTransfer () naar log wat informatie over de overdracht.

Laten we de service-implementatie maken:

importeer org.apache.log4j.Logger; importeer com.baeldung.mdc.TransferService; public class Log4JTransferService breidt TransferService uit {private Logger logger = Logger.getLogger (Log4JTransferService.class); @Override protected void beforeTransfer (lang bedrag) {logger.info ("Voorbereiding op overboeking" + bedrag + "$."); } @Override beschermde ongeldige afterTransfer (lang bedrag, booleaanse uitkomst) {logger.info ("Heeft de overdracht van" + bedrag + "$ succesvol voltooid?" + Uitkomst + "."); }} 

Het belangrijkste probleem dat hier moet worden opgemerkt, is dat wanneer het logbericht is gemaakt, is het niet mogelijk om toegang te krijgen tot het Overdracht voorwerp - alleen het bedrag is toegankelijk, waardoor het onmogelijk is om de transactie-ID of de afzender te loggen.

Laten we het gebruikelijke opzetten log4j.properties bestand om in te loggen op de 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 

Laten we eindelijk een kleine applicatie opzetten die meerdere overdrachten tegelijk kan uitvoeren via een ExecutorService:

openbare klasse TransferDemo {openbare statische leegte hoofd (String [] args) {ExecutorService executor = Executors.newFixedThreadPool (3); TransactionFactory transactionFactory = nieuwe TransactionFactory (); voor (int i = 0; i <10; i ++) {Transfer tx = transactionFactory.newInstance (); Uitvoerbare taak = nieuwe Log4JRunnable (tx); executor.submit (taak); } executor.shutdown (); }}

We merken op dat om de ExecutorService, we moeten de uitvoering van het Log4JTransferService in een adapter omdat uitvoerder.submit () verwacht een Runnable:

openbare klasse Log4JRunnable implementeert Runnable {private Transfer tx; openbare Log4JRunnable (Transfer tx) {this.tx = tx; } openbare ongeldige run () {log4jBusinessService.transfer (tx.getAmount ()); }} 

Wanneer we onze demo-applicatie draaien die meerdere transfers tegelijkertijd beheert, ontdekken we dat heel snel het logboek is niet bruikbaar zoals we zouden willen dat het is. Het is ingewikkeld om de uitvoering van elke overboeking te volgen, omdat de enige nuttige informatie die wordt geregistreerd, de hoeveelheid overgemaakt geld is en de naam van de thread die die specifieke overboeking uitvoert.

Bovendien is het onmogelijk om onderscheid te maken tussen twee verschillende transacties van hetzelfde bedrag die door dezelfde thread worden uitgevoerd, omdat de gerelateerde logboekregels er in wezen hetzelfde uitzien:

... 519 [pool-1-thread-3] INFO Log4JBusinessService - Voorbereiding op overboeking 1393 $. 911 [pool-1-thread-2] INFO Log4JBusinessService - Is de overdracht van $ 1065 met succes voltooid? waar. 911 [pool-1-thread-2] INFO Log4JBusinessService - Voorbereiding op overboeking 1189 $. 989 [pool-1-thread-1] INFO Log4JBusinessService - Is de overdracht van $ 1350 met succes voltooid? waar. 989 [pool-1-thread-1] INFO Log4JBusinessService - Voorbereiding op overboeking 1178 $. 1245 [pool-1-thread-3] INFO Log4JBusinessService - Is de overdracht van $ 1393 met succes voltooid? waar. 1246 [pool-1-thread-3] INFO Log4JBusinessService - Voorbereiding op overboeking 1133 $. 1507 [pool-1-thread-2] INFO Log4JBusinessService - Is de overdracht van $ 1189 met succes voltooid? waar. 1508 [pool-1-thread-2] INFO Log4JBusinessService - Voorbereiding op overboeking 1907 $. 1639 [pool-1-thread-1] INFO Log4JBusinessService - Is de overdracht van $ 1178 met succes voltooid? waar. 1640 [pool-1-thread-1] INFO Log4JBusinessService - Voorbereiding op overboeking 674 $. ... 

Gelukkig, MDC kunnen helpen.

3. MDC in Log4j

Laten we voorstellen MDC.

MDC in Log4j kunnen we een kaartachtige structuur vullen met stukjes informatie die toegankelijk zijn voor de appender wanneer het logbericht daadwerkelijk wordt geschreven.

De MDC-structuur is intern verbonden met de uitvoerende thread op dezelfde manier als a ThreadLocal variabele zou zijn.

En dus is het idee op hoog niveau:

  1. om de MDC te vullen met stukjes informatie die we beschikbaar willen stellen aan de appender
  2. log dan een bericht in
  3. en ten slotte de MDC wissen

Het patroon van de appender moet uiteraard worden gewijzigd om de variabelen die in de MDC zijn opgeslagen, op te halen.

Laten we dus de code wijzigen volgens deze richtlijnen:

importeer org.apache.log4j.MDC; openbare klasse Log4JRunnable implementeert Runnable {private Transfer tx; privé statische Log4JTransferService log4jBusinessService = nieuwe Log4JTransferService (); openbare 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 (); }} 

niet verrassend MDC.put () wordt gebruikt om een ​​sleutel en een bijbehorende waarde toe te voegen in de MDC while MDC.clear () leegt de MDC.

Laten we nu de log4j.properties om de informatie af te drukken die we zojuist in de MDC hebben opgeslagen. Het is voldoende om het conversiepatroon te wijzigen met de %X{} tijdelijke aanduiding voor elk item in de MDC die we graag willen registreren:

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

Als we de applicatie uitvoeren, zullen we opmerken dat elke regel ook de informatie bevat over de transactie die wordt verwerkt, waardoor we de uitvoering van de applicatie veel gemakkelijker kunnen volgen:

638 [pool-1-thread-2] INFO Log4JBusinessService - Is de overdracht van $ 1104 met succes voltooid? waar. - tx.id = 2 tx.owner = Marc 638 [pool-1-thread-2] INFO Log4JBusinessService - Voorbereiding op overboeking 1685 $. - tx.id = 4 tx.owner = John 666 [pool-1-thread-1] INFO Log4JBusinessService - Is de overdracht van $ 1985 met succes voltooid? waar. - tx.id = 1 tx.owner = Marc 666 [pool-1-thread-1] INFO Log4JBusinessService - Voorbereiding op overboeking 958 $. - tx.id = 5 tx.owner = Susan 739 [pool-1-thread-3] INFO Log4JBusinessService - Is de overdracht van 783 $ succesvol voltooid? waar. - tx.id = 3 tx.owner = Samantha 739 [pool-1-thread-3] INFO Log4JBusinessService - Voorbereiding op overdracht van 1024 $. - tx.id = 6 tx.owner = John 1259 [pool-1-thread-2] INFO Log4JBusinessService - Is de overdracht van $ 1685 met succes voltooid? false. - tx.id = 4 tx.owner = John 1260 [pool-1-thread-2] INFO Log4JBusinessService - Voorbereiding op overboeking 1667 $. - tx.id = 7 tx.owner = Marc 

4. MDC in Log4j2

Dezelfde functie is ook beschikbaar in Log4j2, dus laten we eens kijken hoe we deze kunnen gebruiken.

Laten we eerst een TransferService subklasse die logt met Log4j2:

importeer org.apache.logging.log4j.LogManager; importeer org.apache.logging.log4j.Logger; openbare klasse Log4J2TransferService breidt TransferService uit {privé statische laatste Logger-logger = LogManager.getLogger (); @Override protected void beforeTransfer (lang bedrag) {logger.info ("Voorbereiding op overboeking {} $.", Bedrag); } @Override protected void afterTransfer (lang bedrag, booleaanse uitkomst) {logger.info ("Is de overdracht van {} $ met succes voltooid? {}.", Bedrag, uitkomst); }} 

Laten we dan de code wijzigen die de MDC gebruikt, die eigenlijk wordt genoemd ThreadContext in Log4j2:

importeer org.apache.log4j.MDC; openbare klasse Log4J2Runnable implementeert Runnable {private final Transaction tx; privé Log4J2BusinessService log4j2BusinessService = nieuwe Log4J2BusinessService (); openbare Log4J2Runnable (transactie 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 (); }} 

Opnieuw, ThreadContext.put () voegt een vermelding toe aan de MDC en ThreadContext.clearAll () verwijdert alle bestaande vermeldingen.

We missen nog steeds de log4j2.xml bestand om de logboekregistratie te configureren. Zoals we kunnen opmerken, is de syntaxis om te specificeren welke MDC-vermeldingen moeten worden geregistreerd dezelfde als die in Log4j:

Nogmaals, laten we de applicatie uitvoeren en we zullen zien dat de MDC-informatie wordt afgedrukt in het logboek:

1119 [pool-1-thread-3] INFO Log4J2BusinessService - Is de overdracht van $ 1198 met succes voltooid? waar. - tx.id = 3 tx.owner = Samantha 1120 [pool-1-thread-3] INFO Log4J2BusinessService - Voorbereiding om 1723 $ over te dragen. - tx.id = 5 tx.owner = Samantha 1170 [pool-1-thread-2] INFO Log4J2BusinessService - Is de overdracht van 701 $ succesvol voltooid? waar. - tx.id = 2 tx.owner = Susan 1171 [pool-1-thread-2] INFO Log4J2BusinessService - Voorbereiding op overboeking 1108 $. - tx.id = 6 tx.owner = Susan 1794 [pool-1-thread-1] INFO Log4J2BusinessService - Is de overdracht van 645 $ succesvol voltooid? waar. - tx.id = 4 tx.owner = Susan 

5. MDC in SLF4J / Logback

MDC is ook beschikbaar in SLF4J, op voorwaarde dat het wordt ondersteund door de onderliggende logboekregistratiebibliotheek.

Zowel Logback als Log4j ondersteunen MDC zoals we zojuist hebben gezien, dus we hebben niets speciaals nodig om het te gebruiken met een standaardopstelling.

Laten we het gebruikelijke voorbereiden TransferService subklasse, dit keer met behulp van de Simple Logging Facade voor Java:

importeer org.slf4j.Logger; importeer org.slf4j.LoggerFactory; laatste klasse Slf4TransferService breidt TransferService uit {privé statische laatste Logger-logger = LoggerFactory.getLogger (Slf4TransferService.class); @Override protected void beforeTransfer (lang bedrag) {logger.info ("Voorbereiding op overboeking {} $.", Bedrag); } @Override protected void afterTransfer (lang bedrag, booleaanse uitkomst) {logger.info ("Is de overdracht van {} $ met succes voltooid? {}.", Bedrag, uitkomst); }} 

Laten we nu de smaak van MDC van de SLF4J gebruiken. In dit geval zijn de syntaxis en semantiek dezelfde als die in log4j:

importeer org.slf4j.MDC; openbare klasse Slf4jRunnable implementeert Runnable {private final Transaction tx; openbare Slf4jRunnable (transactie tx) {this.tx = tx; } public void run () {MDC.put ("transaction.id", tx.getTransactionId ()); MDC.put ("transaction.owner", tx.getOwner ()); nieuwe Slf4TransferService (). transfer (tx.getAmount ()); MDC.clear (); }} 

We moeten het Logback-configuratiebestand verstrekken, logback.xml:

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

Nogmaals, we zullen zien dat de informatie in de MDC correct wordt toegevoegd aan de geregistreerde berichten, ook al wordt deze informatie niet expliciet verstrekt in de log.info () methode:

1020 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Is de overdracht van $ 1869 met succes voltooid? waar. - tx.id = 3 tx.owner = John 1021 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Voorbereiding op overdracht van 1303 $. - tx.id = 6 tx.owner = Samantha 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Is de overdracht van $ 1498 met succes voltooid? waar. - tx.id = 4 tx.owner = Marc 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Voorbereiding op overboeking 1528 $. - tx.id = 7 tx.owner = Samantha 1492 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Is de overdracht van $ 1110 met succes voltooid? waar. - tx.id = 5 tx.owner = Samantha 1493 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Voorbereiding op overdracht van 644 $. - tx.id = 8 tx.owner = John

Het is vermeldenswaard dat in het geval we de SLF4J-back-end instellen op een logsysteem dat geen MDC ondersteunt, alle gerelateerde aanroepen eenvoudigweg worden overgeslagen zonder bijwerkingen.

6. MDC- en Thread-pools

MDC-implementaties gebruiken meestal ThreadLocals om de contextuele informatie op te slaan. Dat is een gemakkelijke en redelijke manier om draadveiligheid te bereiken. We moeten echter voorzichtig zijn met het gebruik van MDC met threadpools.

Laten we eens kijken hoe de combinatie van ThreadLocal-gebaseerde MDC's en threadpools kunnen gevaarlijk zijn:

  1. We halen een draad uit de draadpool.
  2. Vervolgens slaan we wat contextuele informatie op in MDC met behulp van MDC.put () of ThreadContext.put ().
  3. We gebruiken deze informatie in sommige logboeken en op de een of andere manier zijn we vergeten de MDC-context te wissen.
  4. De geleende draad komt terug in de draadpool.
  5. Na een tijdje krijgt de applicatie dezelfde thread uit de pool.
  6. Omdat we de MDC de laatste keer niet hebben opgeruimd, bezit deze thread nog steeds enkele gegevens van de vorige uitvoering.

Dit kan enkele onverwachte inconsistenties tussen uitvoeringen veroorzaken. Een manier om dit te voorkomen, is door altijd te onthouden om de MDC-context aan het einde van elke uitvoering op te schonen. Deze benadering vereist gewoonlijk strikt menselijk toezicht en is daarom foutgevoelig.

Een andere benadering is om te gebruiken ThreadPoolExecutor hooks en voer de nodige opruimacties uit na elke uitvoering. Om dat te doen, kunnen we het ThreadPoolExecutor class en overschrijf de afterExecute () haak:

openbare klasse MdcAwareThreadPoolExecutor breidt ThreadPoolExecutor uit {openbare MdcAwareThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit-eenheid, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler workQueue, handler, rejectedExecutionHandler handler, maximumAlivePoolSue, maximumAlivePoolSue, keepAlivePoolSue) } @Override protected void afterExecute (Runnable r, Throwable t) {System.out.println ("De MDC-context opschonen"); MDC.clear (); org.apache.log4j.MDC.clear (); ThreadContext.clearAll (); }}

Op deze manier zou de MDC-opschoning automatisch plaatsvinden na elke normale of uitzonderlijke uitvoering. Het is dus niet nodig om het handmatig te doen:

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

Nu kunnen we dezelfde demo herschrijven met onze nieuwe executor-implementatie:

ExecutorService executor = nieuwe MdcAwareThreadPoolExecutor (3, 3, 0, MINUTEN, nieuwe LinkedBlockingQueue (), Thread :: nieuwe, nieuwe AbortPolicy ()); TransactionFactory transactionFactory = nieuwe TransactionFactory (); voor (int i = 0; i <10; i ++) {Transfer tx = transactionFactory.newInstance (); Uitvoerbare taak = nieuwe Slf4jRunnable (tx); executor.submit (taak); } executor.shutdown ();

7. Conclusie

MDC heeft veel toepassingen, voornamelijk in scenario's waarin de uitvoering van verschillende threads interleaved logboekberichten veroorzaakt die anders moeilijk te lezen zouden zijn.

En zoals we hebben gezien, wordt het ondersteund door drie van de meest gebruikte logging-frameworks in Java.

Zoals gewoonlijk vind je de bronnen op GitHub.