Továbbfejlesztett Java naplózás leképezett diagnosztikai kontextussal (MDC)

1. Áttekintés

Ebben a cikkben megvizsgáljuk a Feltérképezett diagnosztikai kontextus (MDC) az alkalmazás naplózásának javítása érdekében.

Az alapgondolat Feltérképezett diagnosztikai kontextus A módszer célja a naplóüzenetek gazdagítása olyan információkkal, amelyek nem lehetnek elérhetőek abban a körben, ahol a naplózás valójában megtörténik, de amelyek valóban hasznosak lehetnek a program végrehajtásának jobb nyomon követéséhez.

2. Miért érdemes használni az MDC-t?

Kezdjük egy példával. Tegyük fel, hogy pénzt átutaló szoftvert kell írnunk. Felállítottunk egy Átruházás osztály néhány alapvető információt képvisel: egyedi átviteli azonosító és a feladó neve:

public class Transfer {private String ügyletId; privát String feladó; magán Hosszú összeg; nyilvános transzfer (karakterlánc-tranzakcióazonosító, karakterlánc-küldő, hosszú összeg) {this.transactionId = tranzakcióazonosító; this.sender = küldő; ez.összeg = összeg; } public String getSender () {return sender; } public String getTransactionId () {return tranzakcióazonosító; } public Long getAmount () {visszatérési összeg; }} 

Az átvitelhez egy egyszerű API által támogatott szolgáltatást kell használnunk:

public abstract class TransferService {nyilvános logikai átirányítás (hosszú összeg) {// a távoli szolgáltatáshoz csatlakozik, hogy pénzt valóban átutaljon} absztrakt védett void beforeTransfer (hosszú összeg); absztrakt védett érvénytelen afterTransfer (hosszú összeg, logikai eredmény); } 

A beforeTransfer () és afterTransfer () A metódusok felülírhatók az egyéni kód futtatásához közvetlenül az átvitel befejezése előtt és után.

Kihasználjuk a tőkeáttételt beforeTransfer () és afterTransfer () nak nek naplózjon néhány információt az átutalásról.

Hozzuk létre a szolgáltatás megvalósítását:

import org.apache.log4j.Logger; import com.baeldung.mdc.TransferService; a Public osztályú Log4JTransferService kiterjeszti a TransferService {private Logger logger = Logger.getLogger (Log4JTransferService.class); @Védje meg a védett void előtti átvitelt (hosszú összeg) {logger.info ("Átadás előkészítése" + összeg + "$".); } @Orride protected void afterTransfer (hosszú összeg, logikai kimenet) {logger.info ("A" + összeg + "$ átvitele sikeresen befejeződött?" + Kimenetel + "."); }} 

A fő kérdés, amelyet itt meg kell jegyezni, az az a naplóüzenet létrehozásakor nem lehet hozzáférni a Átruházás tárgy - csak az összeg érhető el, lehetetlenné téve a tranzakcióazonosító vagy a feladó naplózását.

Állítsuk be a szokásosat log4j.tulajdonságok fájl a konzolon történő bejelentkezéshez:

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

Állítsunk végre egy kis alkalmazást, amely képes egyszerre több átvitelt futtatni egy ExecutorService:

public class TransferDemo {public static void main (String [] args) {ExecutorService végrehajtó = Executors.newFixedThreadPool (3); TransactionFactoryactionFactory = new TransactionFactory (); for (int i = 0; i <10; i ++) {Transzfer tx = tranzakcióFactory.newInstance (); Futható feladat = new Log4JRunnable (tx); végrehajtó.beküldés (feladat); } végrehajtó.leállítás (); }}

Megjegyezzük, hogy a ExecutorService, be kell csomagolnunk a Log4JTransferService adapterben, mert végrehajtó. beküldés () elvárja a Futható:

Log4JRunnable nyilvános osztály Runnable {private Transfer tx; public Log4JRunnable (Transfer tx) {this.tx = tx; } public void run () {log4jBusinessService.transfer (tx.getAmount ()); }} 

Amikor egyidejűleg több átvitelt kezelő demo alkalmazást futtatunk, ezt nagyon gyorsan felfedezzük a napló nem hasznos, mint szeretnénk. Az egyes átutalások végrehajtásának bonyolult nyomon követése, mert az egyetlen naplózott hasznos információ az átutalt pénz mennyisége és az adott átutalást végrehajtó szál neve.

Ráadásul lehetetlen megkülönböztetni ugyanazon szál által végrehajtott két azonos összegű tranzakciót, mert a kapcsolódó naplósorok lényegében ugyanazok:

... 519 [pool-1-thread-3] INFO Log4JBusinessService - Felkészülés az 1393 $ átvitelére. 911 [pool-1-thread-2] INFO Log4JBusinessService - 1065 $ átutalása sikeresen befejeződött? igaz. 911 [pool-1-thread-2] INFO Log4JBusinessService - Felkészülés az 1189 $ átutalására. 989 [pool-1-thread-1] INFO Log4JBusinessService - 1350 $ átutalása sikeresen befejeződött? igaz. 989 [pool-1-thread-1] INFO Log4JBusinessService - Felkészülés az 1178 $ átadására. 1245 [pool-1-thread-3] INFO Log4JBusinessService - 1393 $ átutalása sikeresen befejeződött? igaz. 1246 [pool-1-thread-3] INFO Log4JBusinessService - Felkészülés az 1133 $ átadására. 1507 [pool-1-thread-2] INFO Log4JBusinessService - A 1189 $ átutalása sikeresen befejeződött? igaz. 1508 [pool-1-thread-2] INFO Log4JBusinessService - Felkészülés az 1907 $ átadására. 1639 [pool-1-thread-1] INFO Log4JBusinessService - 1178 $ átutalása sikeresen befejeződött? igaz. 1640 [pool-1-thread-1] INFO Log4JBusinessService - Felkészülés a 674 $ átadására. ... 

Szerencsére, MDC tud segíteni.

3. MDC a Log4j-ben

Bemutatjuk MDC.

MDC a Log4j-ben lehetővé teszi számunkra, hogy egy térképszerű struktúrát olyan információkkal töltsünk fel, amelyek a naplóüzenet tényleges megírásakor hozzáférhetők a hozzáfűző számára.

Az MDC struktúra ugyanúgy a végrehajtó szálhoz van csatolva a ThreadLocal változó lenne.

Tehát a magas szintű ötlet a következő:

  1. hogy az MDC-t olyan információkkal töltsék meg, amelyeket elérhetővé kívánunk tenni az alkalmazás számára
  2. majd naplóz egy üzenetet
  3. és végül törölje az MDC-t

Az MDC-ben tárolt változók lekérése érdekében nyilvánvalóan meg kell változtatni az alkalmazás mintázatát.

Tehát változtassuk meg a kódot az alábbi irányelvek szerint:

import org.apache.log4j.MDC; Log4JRunnable nyilvános osztály Runnable {private Transfer tx; privát statikus Log4JTransferService log4jBusinessService = új Log4JTransferService (); public Log4JRunnable (Transfer tx) {this.tx = tx; } public void run () {MDC.put ("tranzakció.id", tx.getTransactionId ()); MDC.put ("tranzakció.tulajdonos", tx.getSender ()); log4jBusinessService.transfer (tx.getAmount ()); MDC.clear (); }} 

Nem meglepő MDC.put () a kulcs és egy megfelelő érték hozzáadására szolgál az MDC-ben, míg MDC.clear () kiüríti az MDC-t.

Változtassunk most a log4j.tulajdonságok kinyomtatni azokat az információkat, amelyeket az MDC-ben tároltunk. Elég a konverziós mintát megváltoztatni a %X{} helyőrző minden egyes MDC-bejegyzéshez, amelyet naplózni szeretnénk:

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

Most, ha futtatjuk az alkalmazást, megjegyezzük, hogy minden sor tartalmazza a feldolgozott tranzakcióval kapcsolatos információkat is, így sokkal könnyebben nyomon követhetjük az alkalmazás végrehajtását:

638 [pool-1-thread-2] INFO Log4JBusinessService - 1104 $ átvitele sikeresen befejeződött? igaz. - tx.id = 2 tx.owner = Marc 638 [pool-1-thread-2] INFO Log4JBusinessService - Felkészülés az 1685 $ átadására. - tx.id = 4 tx.owner = John 666 [pool-1-thread-1] INFO Log4JBusinessService - Sikeresen befejeződött-e az 1985 $ átvitel? igaz. - tx.id = 1 tx.owner = Marc 666 [pool-1-thread-1] INFO Log4JBusinessService - Felkészülés a 958 $ átadására. - tx.id = 5 tx.owner = Susan 739 [pool-1-thread-3] INFO Log4JBusinessService - A 783 $ átutalása sikeresen befejeződött? igaz. - tx.id = 3 tx.owner = Samantha 739 [pool-1-thread-3] INFO Log4JBusinessService - Felkészülés az 1024 $ átadására. - tx.id = 6 tx.owner = John 1259 [pool-1-thread-2] INFO Log4JBusinessService - 1685 $ átutalása sikeresen befejeződött? hamis. - tx.id = 4 tx.owner = John 1260 [pool-1-thread-2] INFO Log4JBusinessService - Felkészülés az 1667 $ átadására. - tx.id = 7 tx. tulajdonos = Marc 

4. MDC a Log4j2-ben

Ugyanez a szolgáltatás elérhető a Log4j2-ben is, ezért nézzük meg, hogyan kell használni.

Először állítsunk fel egy TransferService a Log4j2 használatával naplózó alosztály:

import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; a public osztályú Log4J2TransferService kiterjeszti a TransferService {private static final Logger logger = LogManager.getLogger (); @Orride protected void beforeTransfer (hosszú összeg) {logger.info ("Felkészülés az átutalásra {} $.", Összeg); } @Orride protected void afterTransfer (hosszú összeg, logikai kimenet) {logger.info ("Sikeresen befejeződött-e a (z) {} $ átvitele? {}., Összeg, kimenetel); }} 

Ezután változtassuk meg az MDC-t használó kódot, amelyet valójában hívnak ThreadContext a Log4j2-ben:

import org.apache.log4j.MDC; Log4J2Runnable nyilvános osztály Runnable {private final Transaction tx; privát Log4J2BusinessService log4j2BusinessService = új Log4J2BusinessService (); public Log4J2Runnable (Tranzakció tx) {this.tx = tx; } public void run () {ThreadContext.put ("tranzakció.id", tx.getTransactionId ()); ThreadContext.put ("tranzakció.tulajdonos", tx.getOwner ()); log4j2BusinessService.transfer (tx.getAmount ()); ThreadContext.clearAll (); }} 

Újra, ThreadContext.put () hozzáad egy bejegyzést az MDC-be és ThreadContext.clearAll () eltávolítja az összes létező bejegyzést.

Még mindig hiányzik a log4j2.xml fájl a naplózás konfigurálásához. Mint megjegyezhetjük, az MDC bejegyzések naplózására szolgáló szintaxis megegyezik a Log4j-ben használtéval:

Ismét futtassuk az alkalmazást, és látni fogjuk, hogy az MDC információk kinyomtatásra kerülnek a naplóban:

1119 [pool-1-thread-3] INFO Log4J2BusinessService - Az 1198 $ átutalása sikeresen befejeződött? igaz. - tx.id = 3 tx.owner = Samantha 1120 [pool-1-thread-3] INFO Log4J2BusinessService - Felkészülés az 1723 $ átadására. - tx.id = 5 tx.owner = Samantha 1170 [pool-1-thread-2] INFO Log4J2BusinessService - A 701 $ átvitele sikeresen befejeződött? igaz. - tx.id = 2 tx.owner = Susan 1171 [pool-1-thread-2] INFO Log4J2BusinessService - Felkészülés az 1108 $ átadására. - tx.id = 6 tx.owner = Susan 1794 [pool-1-thread-1] INFO Log4J2BusinessService - A 645 $ átvitele sikeresen befejeződött? igaz. - tx.id = 4 tx. tulajdonos = Susan 

5. MDC SLF4J / Logback formátumban

MDC az SLF4J-ben is elérhető, azzal a feltétellel, hogy az alatta lévő naplókönyvtár támogatja.

A Logback és a Log4j is támogatja az MDC-t, amint az imént láttuk, ezért nincs szükség semmi különösre, hogy szabványos beállítással használjuk.

Készítsük elő a szokásosat TransferService alosztály, ezúttal a Java egyszerű naplózási homlokzatával:

import org.slf4j.Logger; import org.slf4j.LoggerFactory; az Slf4TransferService végleges osztály kiterjeszti a TransferService {private static final Logger logger = LoggerFactory.getLogger (Slf4TransferService.class); @Orride protected void beforeTransfer (hosszú összeg) {logger.info ("Felkészülés az átutalásra {} $.", Összeg); } @Orride protected void afterTransfer (hosszú összeg, logikai kimenet) {logger.info ("Sikeresen befejeződött-e a (z) {} $ átvitele? {}., Összeg, kimenetel); }} 

Most használjuk az SLF4J MDC ízét. Ebben az esetben a szintaxis és a szemantika megegyezik a log4j-vel:

import org.slf4j.MDC; Slf4jRunnable nyilvános osztály Runnable {private final Transaction tx; public Slf4jRunnable (Tranzakció tx) {this.tx = tx; } public void run () {MDC.put ("tranzakció.id", tx.getTransactionId ()); MDC.put ("tranzakció.tulajdonos", tx.getOwner ()); új Slf4TransferService (). transfer (tx.getAmount ()); MDC.clear (); }} 

Meg kell adnunk a Logback konfigurációs fájlt, logback.xml:

   % -4r [% t]% 5p% c {1} -% m - tx.id =% X {ügylet.id} tx.tulajdonos =% X {tranzakció.tulajdonos}% n 

Ismét látni fogjuk, hogy az MDC-ben lévő információk megfelelően vannak hozzáadva a naplózott üzenetekhez, annak ellenére, hogy ezeket az információkat nem log.info () módszer:

1020 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - 1869 $ átutalása sikeresen befejeződött? igaz. - tx.id = 3 tx.owner = John 1021 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Felkészülés az 1303 $ átadására. - tx.id = 6 tx.owner = Samantha 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Az 1498 $ átutalása sikeresen befejeződött? igaz. - tx.id = 4 tx.owner = Marc 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Felkészülés az 1528 $ átadására. - tx.id = 7 tx.owner = Samantha 1492 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - 1110 $ átvitele sikeresen befejeződött? igaz. - tx.id = 5 tx.owner = Samantha 1493 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Felkészülés a 644 $ átadására. - tx.id = 8 tx. tulajdonos = John

Érdemes megjegyezni, hogy abban az esetben, ha az SLF4J back-end-et egy MDC-t nem támogató naplózási rendszerhez állítjuk be, az összes kapcsolódó invokációt egyszerűen kihagyják mellékhatások nélkül.

6. MDC és szálkészletek

Az MDC implementációk általában ThreadLocals a kontextuális információk tárolására. Ez egyszerű és ésszerű módszer a szálbiztonság elérésére. Vigyáznunk kell azonban az MDC szálkészletekkel történő használatára.

Lássuk, hogy a kombináció ThreadLocalalapú MDC-k és szálkészletek veszélyesek lehetnek:

  1. Egy szálat kapunk a szálmedencéből.
  2. Ezután néhány kontextuális információt tárolunk az MDC segítségével MDC.put () vagy ThreadContext.put ().
  3. Néhány naplóban felhasználjuk ezeket az információkat, és valahogy elfelejtettük törölni az MDC kontextust.
  4. A kölcsönzött szál visszatér a szálkészletbe.
  5. Egy idő után az alkalmazás ugyanazt a szálat kapja a készletből.
  6. Mivel legutóbb nem tisztítottuk meg az MDC-t, ennek a szálnak még mindig van néhány adata az előző végrehajtásból.

Ez váratlan következetlenségeket okozhat a kivégzések között. Ennek megakadályozásának egyik módja az, hogy mindig emlékezzen az MDC kontextus tisztítására az egyes végrehajtások végén. Ez a megközelítés általában szigorú emberi felügyeletet igényel, ezért hibára hajlamos.

Egy másik megközelítés a használat ThreadPoolExecutor akasztani és minden végrehajtás után elvégezni a szükséges tisztításokat. Ehhez kibővíthetjük a ThreadPoolExecutor osztály és felülírja a afterExecute () horog:

public class MdcAwareThreadPoolExecutor kiterjed ThreadPoolExecutor {nyilvános MdcAwareThreadPoolExecutor (int corePoolSize, int maximumPoolSize, hosszú KeepAliveTime, TimeUnit egység, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler felvezető) {super (corePoolSize, maximumPoolSize, KeepAliveTime, egység, workQueue, threadFactory, felvezető); } @Orride protected void afterExecute (Runnable r, Throwable t) {System.out.println ("Az MDC-kontextus tisztítása"); MDC.clear (); org.apache.log4j.MDC.clear (); ThreadContext.clearAll (); }}

Így az MDC tisztítása minden normál vagy kivételes végrehajtás után automatikusan megtörténne. Tehát nem kell manuálisan megtenni:

@Orride public void run () {MDC.put ("tranzakció.id", tx.getTransactionId ()); MDC.put ("tranzakció.tulajdonos", tx.getSender ()); új Slf4TransferService (). transfer (tx.getAmount ()); }

Most újraírhatjuk ugyanazt a demót az új végrehajtó implementációnkkal:

ExecutorService végrehajtó = new MdcAwareThreadPoolExecutor (3, 3, 0, PERC, új LinkedBlockingQueue (), Téma :: új, új AbortPolicy ()); TransactionFactoryactionFactory = new TransactionFactory (); for (int i = 0; i <10; i ++) {Transzfer tx = tranzakcióFactory.newInstance (); Futható feladat = new Slf4jRunnable (tx); végrehajtó.beküldés (feladat); } végrehajtó.leállítás ();

7. Következtetés

Az MDC-nek rengeteg alkalmazása van, főként olyan esetekben, amikor több különböző szál végrehajtása olyan átlapolt naplóüzeneteket okoz, amelyeket egyébként nehéz lenne elolvasni.

És amint láttuk, a Java három legelterjedtebb naplózási keretrendszere támogatja.

Szokás szerint a forrásokat a GitHubon találja meg.