Poboljšano Java zapisivanje s mapiranim dijagnostičkim kontekstom (MDC)

1. Pregled

U ovom ćemo članku istražiti upotrebu Mapirani dijagnostički kontekst (MDC) za poboljšanje evidentiranja aplikacija.

Osnovna ideja Mapirani dijagnostički kontekst je pružanje načina za obogaćivanje poruka dnevnika informacijama koje ne bi mogle biti dostupne u opsegu u kojem se evidentiranje zapravo događa, ali koje uistinu mogu biti korisne za bolje praćenje izvršenja programa.

2. Zašto koristiti MDC

Krenimo od primjera. Pretpostavimo da moramo napisati softver koji prenosi novac. Postavili smo a Prijenos klasa koja predstavlja neke osnovne informacije: jedinstveni ID prijenosa i ime pošiljatelja:

javna klasa Transfer {private StringactionId; privatni pošiljatelj niza; privatni Dugi iznos; javni prijenos (StringactionId, String pošiljatelj, dugačak iznos) {this.transactionId =actionId; this.sender = pošiljatelj; this.amount = iznos; } javni String getSender () {return sender; } javni String getTransactionId () {return transactionId; } javni Long getAmount () {povratni iznos; }} 

Da bismo izvršili prijenos, moramo koristiti uslugu potpomognutu jednostavnim API-jem:

javna apstraktna klasa TransferService {javni logički prijenos (dugačak iznos) {// povezuje se s udaljenom uslugom za stvarni prijenos novca} sažetak zaštićena praznina beforeTransfer (dugačak iznos); sažetak zaštićena praznina afterTransfer (duga količina, logički ishod); } 

The beforeTransfer () i afterTransfer () metode se mogu nadjačati za pokretanje prilagođenog koda neposredno prije i odmah nakon završetka prijenosa.

Idemo iskoristiti beforeTransfer () i afterTransfer () do zabilježite neke podatke o prijenosu.

Stvorimo implementaciju usluge:

import org.apache.log4j.Logger; import com.baeldung.mdc.TransferService; javna klasa Log4JTransferService proširuje TransferService {private Logger logger = Logger.getLogger (Log4JTransferService.class); @Preuzmi zaštićenu prazninu prije prijenosa (dugačak iznos) {logger.info ("Priprema za prijenos" + iznos + "$."); } @Override protected void afterTransfer (dugačak iznos, logički ishod) {logger.info ("Je li prijenos" + iznosa + "$ uspješno dovršen?" + Ishod + "."); }} 

Ovdje je glavno pitanje koje treba primijetiti kada se stvori poruka dnevnika, nije moguće pristupiti datoteci Prijenos objekt - dostupan je samo iznos, što onemogućava evidentiranje ID-a transakcije ili pošiljatelja.

Postavimo uobičajeno log4j.svojstva datoteka za prijavu na konzolu:

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

Postavimo napokon malu aplikaciju koja može istodobno pokretati više prijenosa kroz ExecutorService:

javna klasa TransferDemo {javna statička void glavna (String [] args) {ExecutorService izvršitelj = Izvršitelji.newFixedThreadPool (3); TransactionFactoryactionFactory = novo TransactionFactory (); za (int i = 0; i <10; i ++) {Prijenos tx =actionFactory.newInstance (); Izvršni zadatak = novi Log4JRunnable (tx); izvršilac.podnijeti (zadatak); } egzekutor.šutdown (); }}

Primjećujemo da kako bismo koristili ExecutorService, moramo završiti izvršenje Log4JTransferService u adapteru jer izvršitelj.submit () očekuje a Izvodljivo:

javna klasa Log4JRunnable implementira Runnable {private Transfer tx; javni Log4JRunnable (Prijenos tx) {this.tx = tx; } javni void run () {log4jBusinessService.transfer (tx.getAmount ()); }} 

Kada pokrenemo našu demo aplikaciju koja istovremeno upravlja s više prijenosa, vrlo brzo to otkrijemo zapisnik nije koristan kakav bismo željeli. Složeno je pratiti izvršenje svakog prijenosa jer su jedine korisne informacije koje se bilježe količina prenesenog novca i ime niti koja izvršava taj određeni prijenos.

Štoviše, nemoguće je razlikovati dvije različite transakcije istog iznosa koje izvršava ista nit jer povezane linije dnevnika izgledaju u osnovi iste:

... 519 [pool-1-thread-3] INFO Log4JBusinessService - Priprema za prijenos 1393 $. 911 [pool-1-thread-2] INFO Log4JBusinessService - Je li prijenos od 1065 $ uspješno dovršen? pravi. 911 [pool-1-thread-2] INFO Log4JBusinessService - Priprema za prijenos 1189 $. 989 [pool-1-thread-1] INFO Log4JBusinessService - Je li prijenos od 1350 $ uspješno dovršen? pravi. 989 [pool-1-thread-1] INFO Log4JBusinessService - Priprema za prijenos 1178 $. 1245 [pool-1-thread-3] INFO Log4JBusinessService - Je li prijenos od 1393 $ uspješno dovršen? pravi. 1246 [pool-1-thread-3] INFO Log4JBusinessService - Priprema za prijenos 1133 $. 1507 [pool-1-thread-2] INFO Log4JBusinessService - Je li prijenos od 1189 $ uspješno dovršen? pravi. 1508 [pool-1-thread-2] INFO Log4JBusinessService - Priprema za prijenos 1907 $. 1639 [pool-1-thread-1] INFO Log4JBusinessService - Je li prijenos od 1178 $ uspješno dovršen? pravi. 1640 [pool-1-thread-1] INFO Log4JBusinessService - Priprema za prijenos 674 $. ... 

Srećom, MDC mogu pomoći.

3. MDC u Log4j

Uvedimo MDC.

MDC u Log4j omogućuje nam da ispunimo strukturu sličnu mapi dijelovima informacija koji su dostupni programu za dodavanje dok je poruka dnevnika zapravo napisana.

Struktura MDC-a iznutra je pričvršćena na izvršnu nit na isti način a ThreadLocal varijabla bi bila.

Dakle, ideja na visokoj razini je:

  1. ispuniti MDC informacijama koje želimo staviti na raspolaganje podnositelju zahtjeva
  2. zatim zabilježite poruku
  3. i na kraju, očistite MDC

Uzorak dodavača trebalo bi očito promijeniti kako bi se dohvatile varijable pohranjene u MDC-u.

Pa onda promijenimo kod prema ovim smjernicama:

uvoz org.apache.log4j.MDC; javna klasa Log4JRunnable implementira Runnable {private Transfer tx; privatna statička Log4JTransferService log4jBusinessService = nova Log4JTransferService (); javni Log4JRunnable (Prijenos tx) {this.tx = tx; } javna void run () {MDC.put ("action.id ", tx.getTransactionId ()); MDC.put ("vlasnik transakcije", tx.getSender ()); log4jBusinessService.transfer (tx.getAmount ()); MDC.clear (); }} 

neiznenađujuće MDC.put () koristi se za dodavanje ključa i odgovarajuće vrijednosti u MDC dok MDC.clear () isprazni MDC.

Promijenimo sada log4j.svojstva za ispis podataka koje smo upravo pohranili u MDC-u. Dovoljno je promijeniti obrazac pretvorbe pomoću %X{} rezervirano mjesto za svaki unos sadržan u MDC-u koji bismo željeli prijaviti:

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

Sada, ako pokrenemo aplikaciju, primijetit ćemo da svaki redak sadrži i podatke o transakciji koja se obrađuje, što nam olakšava praćenje izvršenja aplikacije:

638 [pool-1-thread-2] INFO Log4JBusinessService - Je li prijenos od 1104 $ uspješno dovršen? pravi. - tx.id = 2 tx.owner = Marc 638 [pool-1-thread-2] INFO Log4JBusinessService - Priprema za prijenos 1685 $. - tx.id = 4 tx.owner = John 666 [pool-1-thread-1] INFO Log4JBusinessService - Je li prijenos 1985 $ uspješno završen? pravi. - tx.id = 1 tx.owner = Marc 666 [pool-1-thread-1] INFO Log4JBusinessService - Priprema za prijenos 958 $. - tx.id = 5 tx.owner = Susan 739 [pool-1-thread-3] INFO Log4JBusinessService - Je li prijenos od 783 $ uspješno dovršen? pravi. - tx.id = 3 tx.owner = Samantha 739 [pool-1-thread-3] INFO Log4JBusinessService - Priprema za prijenos 1024 $. - tx.id = 6 tx.owner = John 1259 [pool-1-thread-2] INFO Log4JBusinessService - Je li prijenos od 1685 $ uspješno dovršen? lažno. - tx.id = 4 tx.owner = John 1260 [pool-1-thread-2] INFO Log4JBusinessService - Priprema za prijenos 1667 $. - tx.id = 7 tx.owner = Marc 

4. MDC u Log4j2

Ista je značajka dostupna i u Log4j2, pa da vidimo kako je koristiti.

Prvo postavimo a TransferService podrazred koji zapisuje pomoću Log4j2:

import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; javna klasa Log4J2TransferService proširuje TransferService {private static final Logger logger = LogManager.getLogger (); @Override protected void beforeTransfer (dugačak iznos) {logger.info ("Priprema za prijenos {} $.", Iznos); } @Override protected void afterTransfer (dugačak iznos, logički ishod) {logger.info ("Je li prijenos od {} $ uspješno dovršen? {}.", Iznos, ishod); }} 

Promijenimo onda kod koji koristi MDC, koji se zapravo zove ThreadContext u Log4j2:

uvoz org.apache.log4j.MDC; javna klasa Log4J2Runnable implementira Runnable {private final Transaction tx; private Log4J2BusinessService log4j2BusinessService = novo Log4J2BusinessService (); javni Log4J2Runnable (Transakcija tx) {this.tx = tx; } javna void run () {ThreadContext.put ("action.id ", tx.getTransactionId ()); ThreadContext.put ("owner.owner", tx.getOwner ()); log4j2BusinessService.transfer (tx.getAmount ()); ThreadContext.clearAll (); }} 

Opet, ThreadContext.put () dodaje unos u MDC i ThreadContext.clearAll () uklanja sve postojeće unose.

Još uvijek nam nedostaje log4j2.xml datoteku za konfiguriranje zapisnika. Kao što možemo primijetiti, sintaksa koja određuje koji MDC unosi trebaju biti zabilježeni jednaka je onoj koja se koristi u Log4j:

Ponovno, izvršimo aplikaciju i vidjet ćemo kako se MDC podaci ispisuju u zapisnik:

1119 [pool-1-thread-3] INFO Log4J2BusinessService - Je li prijenos od 1198 $ uspješno dovršen? pravi. - tx.id = 3 tx.owner = Samantha 1120 [pool-1-thread-3] INFO Log4J2BusinessService - Priprema za prijenos 1723 $. - tx.id = 5 tx.owner = Samantha 1170 [pool-1-thread-2] INFO Log4J2BusinessService - Je li prijenos od 701 $ uspješno završen? pravi. - tx.id = 2 tx.owner = Susan 1171 [pool-1-thread-2] INFO Log4J2BusinessService - Priprema za prijenos 1108 $. - tx.id = 6 tx.owner = Susan 1794 [pool-1-thread-1] INFO Log4J2BusinessService - Je li prijenos od 645 $ uspješno dovršen? pravi. - tx.id = 4 tx.owner = Susan 

5. MDC u SLF4J / povratna informacija

MDC je dostupan i u SLF4J, pod uvjetom da ga podržava temeljna knjižnica zapisivanja.

I Logback i Log4j podržavaju MDC kao što smo upravo vidjeli, pa nam ne treba ništa posebno da bismo ga koristili sa standardnim postavkama.

Pripremimo uobičajeno TransferService potklasa, ovaj put koristeći Simple Logging Facade za Javu:

uvoz org.slf4j.Logger; uvoz org.slf4j.LoggerFactory; završna klasa Slf4TransferService proširuje TransferService {private static final Logger logger = LoggerFactory.getLogger (Slf4TransferService.class); @Override protected void beforeTransfer (dugačak iznos) {logger.info ("Priprema za prijenos {} $.", Iznos); } @Override protected void afterTransfer (dugačak iznos, logički ishod) {logger.info ("Je li prijenos od {} $ uspješno dovršen? {}.", Iznos, ishod); }} 

Upotrijebimo sada SLF4J-ov okus MDC-a. U ovom su slučaju sintaksa i semantika iste kao u log4j:

uvoz org.slf4j.MDC; javna klasa Slf4jRunnable implementira Runnable {private final Transaction tx; javni Slf4jRunnable (Transakcija tx) {this.tx = tx; } javna void run () {MDC.put ("action.id ", tx.getTransactionId ()); MDC.put ("transaction.owner", tx.getOwner ()); novi Slf4TransferService (). transfer (tx.getAmount ()); MDC.clear (); }} 

Moramo dostaviti konfiguracijsku datoteku Logback, logback.xml:

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

Ponovno ćemo vidjeti da su podaci u MDC-u ispravno dodani u zabilježene poruke, iako te informacije nisu izričito navedene u log.info () metoda:

1020 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Je li prijenos od 1869 $ uspješno dovršen? pravi. - tx.id = 3 tx.owner = John 1021 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Priprema za prijenos 1303 $. - tx.id = 6 tx.owner = Samantha 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Je li prijenos od 1498 $ uspješno dovršen? pravi. - tx.id = 4 tx.owner = Marc 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Priprema za prijenos 1528 $. - tx.id = 7 tx.owner = Samantha 1492 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Je li prijenos od 1110 $ uspješno završen? pravi. - tx.id = 5 tx.owner = Samantha 1493 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Priprema za prijenos 644 $. - tx.id = 8 tx.owner = John

Vrijedno je napomenuti da će u slučaju da SLF4J postavimo pozadinu na sustav za evidentiranje koji ne podržava MDC, svi povezani pozivi biti jednostavno preskočeni bez nuspojava.

6. MDC i Thread Pools

MDC implementacije obično koriste ThreadLocals za pohranu kontekstualnih podataka. To je jednostavan i razuman način za postizanje sigurnosti niti. Međutim, trebali bismo biti oprezni pri korištenju MDC-a s spremištima niti.

Pogledajmo kako kombinacija ThreadLocalMDC-ovi i spremišta niti na temelju mogu biti opasni:

  1. Dobivamo nit iz spremišta niti.
  2. Tada u MDC pohranjujemo neke kontekstualne informacije pomoću MDC.put () ili ThreadContext.put ().
  3. Te podatke koristimo u nekim zapisnicima i nekako smo zaboravili očistiti MDC kontekst.
  4. Posuđena nit vraća se u spremište niti.
  5. Nakon nekog vremena aplikacija dobiva istu nit iz spremišta.
  6. Budući da prošli put nismo očistili MDC, ova nit i dalje posjeduje neke podatke iz prethodnog izvršavanja.

To može prouzročiti neke neočekivane neusklađenosti između izvršenja. Jedan od načina da se to spriječi jest da se uvijek sjetite očistiti MDC kontekst na kraju svakog izvršenja. Ovaj pristup obično treba strog ljudski nadzor i stoga je sklon pogreškama.

Drugi pristup je korištenje ThreadPoolExeecuter zakači i izvrši potrebna čišćenja nakon svakog izvršenja. Da bismo to učinili, možemo proširiti ThreadPoolExeecuter klase i nadjačati afterExecute () kuka:

javna klasa MdcAwareThreadPoolExecutor proširuje ThreadPoolExecutor {public MdcAwareThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedHeaveVeaLeeSeaEEEEEUeSeaEEEEEEEUE } @Override protected void afterExecute (Runnable r, Throwable t) {System.out.println ("Čišćenje MDC konteksta"); MDC.clear (); org.apache.log4j.MDC.clear (); ThreadContext.clearAll (); }}

Na taj bi se način MDC čišćenje događalo automatski nakon svakog normalnog ili iznimnog izvršavanja. Dakle, nema potrebe to raditi ručno:

@Override public void run () {MDC.put ("action.id ", tx.getTransactionId ()); MDC.put ("vlasnik transakcije", tx.getSender ()); novi Slf4TransferService (). transfer (tx.getAmount ()); }

Sada isti demo možemo ponovno napisati s novom implementacijom izvršitelja:

ExecutorService izvršitelj = novi MdcAwareThreadPoolExecutor (3, 3, 0, MINUTE, novi LinkedBlockingQueue (), Tema :: novo, novo AbortPolicy ()); TransactionFactoryactionFactory = novo TransactionFactory (); za (int i = 0; i <10; i ++) {Prijenos tx =actionFactory.newInstance (); Izvodljivi zadatak = novi Slf4jRunnable (tx); izvršilac.podnijeti (zadatak); } egzekutor.šutdown ();

7. Zaključak

MDC ima puno aplikacija, uglavnom u scenarijima u kojima izvršavanje nekoliko različitih niti uzrokuje isprepletene poruke dnevnika koje bi inače bilo teško pročitati.

I kao što smo vidjeli, podržavaju ga tri najčešće korištena okvira za evidentiranje u Javi.

Kao i obično, izvore ćete pronaći na GitHubu.