Vylepšené protokolovanie Java s mapovaným diagnostickým kontextom (MDC)

1. Prehľad

V tomto článku sa budeme venovať použitiu Mapovaný diagnostický kontext (MDC) na zlepšenie protokolovania aplikácií.

Základná myšlienka Zmapovaný diagnostický kontext je poskytnúť spôsob, ako obohatiť logovacie správy o informácie, ktoré by nemohli byť dostupné v rozsahu, v ktorom sa logovanie skutočne vyskytuje, ale ktoré môžu byť skutočne užitočné na lepšie sledovanie vykonávania programu.

2. Prečo používať MDC

Začnime príkladom. Predpokladajme, že musíme napísať softvér, ktorý prevádza peniaze. Založili sme a Prenos trieda, ktorá predstavuje niektoré základné informácie: jedinečné ID prenosu a meno odosielateľa:

verejná trieda Transfer {private String transactionId; súkromný odosielateľ reťazca; súkromná Dlhá suma; public Transfer (String transactionId, String sender, dlhé množstvo) {this.transactionId = transactionId; this.sender = odosielateľ; this.amount = suma; } public String getSender () {návrat odosielateľa; } public String getTransactionId () {return transactionId; } public Long getAmount () {vratna suma; }} 

Na vykonanie prenosu potrebujeme použiť službu podloženú jednoduchým 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); abstrakt chránený neplatný afterTransfer (veľké množstvo, boolovský výsledok); } 

The beforeTransfer () a afterTransfer () metódy môžu byť prepísané na spustenie vlastného kódu tesne pred a hneď po dokončení prenosu.

Ideme na pákový efekt beforeTransfer () a afterTransfer () do zaznamenajte nejaké informácie o prevode.

Vytvorme implementáciu služby:

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 (dlhé množstvo) {logger.info ("Príprava na prevod" + suma + "$."); } @Override protected void afterTransfer (dlhé množstvo, boolovský výsledok) {logger.info ("Bol prevod" + sumy + "$ úspešne dokončený?" + Výsledok + "."); }} 

Tu je potrebné poznamenať hlavne to po vytvorení správy protokolu nie je možné získať prístup k protokolu Prenos objekt - prístupná je iba suma, čo znemožňuje zaznamenanie ID transakcie alebo odosielateľa.

Nastavíme obvyklé log4j.properties súbor na prihlásenie na konzolu:

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

Poďme konečne nastaviť malú aplikáciu, ktorá je schopná spustiť viac prevodov súčasne cez ExecutorService:

public class TransferDemo {public static void main (String [] args) {ExecutorService executor = Executors.newFixedThreadPool (3); TransactionFactory transactionFactory = nový TransactionFactory (); pre (int i = 0; i <10; i ++) {Transfer tx = transactionFactory.newInstance (); Spustiteľná úloha = nový Log4JRunnable (tx); Exekútor.submit (úloha); } executor.shutdown (); }}

Berieme na vedomie, že za účelom použitia ExecutorService, musíme zabaliť vykonávanie Log4JTransferService v adaptéri, pretože Exekutor.submit () očakáva a Spustiteľné:

verejná trieda Log4JRunnable implementuje Runnable {private Transfer tx; public Log4JRunnable (Transfer tx) {this.tx = tx; } public void run () {log4jBusinessService.transfer (tx.getAmount ()); }} 

Keď spustíme našu ukážkovú aplikáciu, ktorá spravuje viac prevodov súčasne, veľmi rýchlo to zistíme denník nie je užitočný, ako by sme chceli. Sledovanie vykonania každého prevodu je zložité, pretože jedinou zaznamenávanou užitočnou informáciou je množstvo prevedených peňazí a názov vlákna, ktoré daný prevod vykonáva.

A čo viac, je nemožné rozlíšiť medzi dvoma rôznymi transakciami rovnakej sumy vykonanými rovnakým vláknom, pretože súvisiace riadky denníka vyzerajú v podstate rovnako:

... 519 [pool-1-thread-3] INFO Log4JBusinessService - Príprava na prevod 1393 $. 911 [pool-1-thread-2] INFO Log4JBusinessService - Bol prevod 1065 $ úspešne dokončený? pravda. 911 [pool-1-thread-2] INFO Log4JBusinessService - príprava na prevod 1189 $. 989 [pool-1-thread-1] INFO Log4JBusinessService - Bol prevod 1350 $ úspešne dokončený? pravda. 989 [pool-1-thread-1] INFO Log4JBusinessService - príprava na prevod 1178 $. 1245 [pool-1-thread-3] INFO Log4JBusinessService - Bol prevod 1393 $ úspešne dokončený? pravda. 1246 [pool-1-thread-3] INFO Log4JBusinessService - príprava na prevod 1133 $. 1507 [pool-1-thread-2] INFO Log4JBusinessService - Bol prevod 1189 $ úspešne dokončený? pravda. 1508 [pool-1-thread-2] INFO Log4JBusinessService - príprava na prevod 1907 $. 1639 [pool-1-thread-1] INFO Log4JBusinessService - Bol prevod 1178 $ úspešne dokončený? pravda. 1640 [pool-1-thread-1] INFO Log4JBusinessService - príprava na prevod 674 $. ... 

Našťastie MDC môžem pomôcť.

3. MDC v Log4j

Poďme sa predstaviť MDC.

MDC v Log4j nám umožňuje vyplniť štruktúru podobnú mape informáciami, ktoré sú prístupné appenderovi, keď je správa protokolu skutočne napísaná.

Štruktúra MDC je vnútorne pripojená k vykonávajúcemu vláknu rovnakým spôsobom a ThreadLocal premenná by bola.

Myšlienka na vysokej úrovni je:

  1. vyplniť MDC informáciami, ktoré chceme sprístupniť prihlasovateľovi
  2. potom prihlásiť správu
  3. a nakoniec vyčistite MDC

Vzor prihlasovateľa by sa mal zjavne zmeniť, aby sa získali premenné uložené v MDC.

Poďme teda zmeniť kód podľa týchto pokynov:

import org.apache.log4j.MDC; verejná trieda Log4JRunnable implementuje Runnable {private Transfer tx; súkromný statický Log4JTransferService log4jBusinessService = nový 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 (); }} 

neprekvapujúco MDC.put () sa používa na pridanie kľúča a zodpovedajúcej hodnoty do MDC while MDC.clear () vyprázdňuje MDC.

Poďme teraz zmeniť log4j.properties vytlačiť informácie, ktoré sme práve uložili v MDC. Postačí zmeniť vzor premeny pomocou %X{} zástupný symbol pre každý záznam obsiahnutý v MDC, ktorý by sme chceli byť prihlásení:

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

Ak teraz spustíme aplikáciu, všimneme si, že každý riadok obsahuje aj informácie o spracovávanej transakcii, čo nám umožňuje oveľa ľahšie sledovať vykonávanie aplikácie:

638 [pool-1-thread-2] INFO Log4JBusinessService - Bol prevod 1104 $ úspešne dokončený? pravda. - tx.id = 2 tx.owner = Marc 638 [pool-1-thread-2] INFO Log4JBusinessService - príprava na prevod 1685 $. - tx.id = 4 tx.owner = John 666 [pool-1-thread-1] INFO Log4JBusinessService - Bol prevod 1985 $ úspešne dokončený? pravda. - tx.id = 1 tx.owner = Marc 666 [pool-1-thread-1] INFO Log4JBusinessService - príprava na prevod 958 $. - tx.id = 5 tx.owner = Susan 739 [pool-1-thread-3] INFO Log4JBusinessService - Bol prevod 783 $ úspešne dokončený? pravda. - tx.id = 3 tx.owner = Samantha 739 [pool-1-thread-3] INFO Log4JBusinessService - príprava na prevod 1024 $. - tx.id = 6 tx.owner = John 1259 [pool-1-thread-2] INFO Log4JBusinessService - Bol prevod 1685 $ úspešne dokončený? nepravdivé. - tx.id = 4 tx.owner = John 1260 [pool-1-thread-2] INFO Log4JBusinessService - príprava na prevod 1667 $. - tx.id = 7 tx.vlastník = Marc 

4. MDC v Log4j2

Rovnaká funkcia je k dispozícii aj v Log4j2, takže sa pozrime, ako ju používať.

Najprv si založme a TransferService podtrieda, ktorá sa prihlasuje pomocou 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 (dlhé množstvo) {logger.info ("Príprava na prevod {} $.", Suma); } @Override protected void afterTransfer (dlhé množstvo, boolean výsledok) {logger.info ("Bol prevod {} $ úspešne dokončený? {}.", Suma, výsledok); }} 

Potom zmeňme kód, ktorý používa MDC, ktorý sa v skutočnosti volá ThreadContext v Log4j2:

import org.apache.log4j.MDC; verejná trieda Log4J2Runnable implementuje Runnable {private final Transaction tx; private Log4J2BusinessService log4j2BusinessService = nový 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 (); }} 

Opäť ThreadContext.put () pridáva záznam v MDC a ThreadContext.clearAll () odstráni všetky existujúce položky.

Stále nám chýba log4j2.xml súbor na konfiguráciu protokolovania. Ako si môžeme všimnúť, syntax na určenie, ktoré položky MDC by sa mali zaznamenávať, je rovnaká ako syntax použitá v Log4j:

Znova vykonajme aplikáciu a uvidíme, ako sa v protokole vytlačia informácie o MDC:

1119 [pool-1-thread-3] INFO Log4J2BusinessService - Bol prevod 1198 $ úspešne dokončený? pravda. - tx.id = 3 tx.owner = Samantha 1120 [pool-1-thread-3] INFO Log4J2BusinessService - príprava na prevod 1723 $. - tx.id = 5 tx.owner = Samantha 1170 [pool-1-thread-2] INFO Log4J2BusinessService - Bol prevod 701 $ úspešne dokončený? pravda. - tx.id = 2 tx.owner = Susan 1171 [pool-1-thread-2] INFO Log4J2BusinessService - príprava na prevod 1108 $. - tx.id = 6 tx.owner = Susan 1794 [pool-1-thread-1] INFO Log4J2BusinessService - Bol prevod 645 $ úspešne dokončený? pravda. - tx.id = 4 tx. majiteľ = Susan 

5. MDC v SLF4J / Logback

MDC je k dispozícii aj v SLF4J, pod podmienkou, že ho podporuje príslušná knižnica protokolovania.

Logback aj Log4j podporujú MDC, ako sme práve videli, takže pri štandardnom nastavení nepotrebujeme nič zvláštne.

Pripravíme si obvyklé TransferService podtrieda, tentokrát s využitím Simple Logging Facade pre 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 (dlhé množstvo) {logger.info ("Príprava na prevod {} $.", Suma); } @Override protected void afterTransfer (dlhé množstvo, boolovský výsledok) {logger.info ("Bol prevod {} $ úspešne dokončený? {}.", Suma, výsledok); }} 

Poďme teraz použiť príchuť MDF SLF4J. V tomto prípade sú syntax a sémantika rovnaké ako v log4j:

import org.slf4j.MDC; verejná trieda Slf4jRunnable implementuje 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 ()); nový Slf4TransferService (). transfer (tx.getAmount ()); MDC.clear (); }} 

Musíme poskytnúť konfiguračný súbor Logback, logback.xml:

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

Opäť uvidíme, že informácie v MDC sú správne pridané k prihláseným správam, aj keď tieto informácie nie sú výslovne uvedené v log.info () metóda:

1020 [pool-1-thread-3] INFO c.b.m.s.lf4jBusinessService - Je prevod 1869 $ úspešne dokončený? pravda. - tx.id = 3 tx.owner = John 1021 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Príprava prevodu 1303 $. - tx.id = 6 tx.owner = Samantha 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Bol prevod 1498 $ úspešne dokončený? pravda. - tx.id = 4 tx.owner = Marc 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Príprava prevodu 1528 $. - tx.id = 7 tx.owner = Samantha 1492 [pool-1-thread-2] INFO c.b.m.s.lf4jBusinessService - Bol prevod 1110 $ úspešne dokončený? pravda. - tx.id = 5 tx.owner = Samantha 1493 [pool-1-thread-2] INFO c.b.m.s.lf4jBusinessService - príprava na prevod 644 $. - tx.id = 8 tx.vlastník = John

Stojí za zmienku, že v prípade, že nastavíme back-end SLF4J na logovací systém, ktorý nepodporuje MDC, všetky súvisiace vyvolania budú jednoducho preskočené bez vedľajších účinkov.

6. MDC a skupiny vlákien

Implementácie MDC sa zvyčajne používajú ThreadLocals na ukladanie kontextových informácií. Je to ľahký a rozumný spôsob, ako dosiahnuť bezpečnosť závitov. Mali by sme však byť opatrní pri používaní MDC s fondmi vlákien.

Pozrime sa, ako bude kombinácia ThreadLocalMDC založené na fondoch vlákien a skupiny vlákien môžu byť nebezpečné:

  1. Získame vlákno z fondu vlákien.
  2. Potom uložíme niektoré kontextové informácie do MDC pomocou MDC.put () alebo ThreadContext.put ().
  3. Tieto informácie používame v niektorých denníkoch a nejako sme zabudli vyčistiť kontext MDC.
  4. Vypožičaná niť sa vracia späť do fondu nití.
  5. Po chvíli získa aplikácia rovnaké vlákno z fondu.
  6. Pretože sme MDC nevyčistili minule, toto vlákno stále vlastní niektoré dáta z predchádzajúcej exekúcie.

Môže to spôsobiť neočakávané nezrovnalosti medzi vykonaním. Jedným zo spôsobov, ako tomu zabrániť, je vždy pamätať na vyčistenie kontextu MDC na konci každého spustenia. Tento prístup zvyčajne vyžaduje prísny ľudský dozor, a preto je náchylný na chyby.

Ďalším prístupom je použitie ThreadPoolExecutor háky a po každom vykonaní vykonajte potrebné čistenie. Aby sme to dosiahli, môžeme rozšíriť ThreadPoolExecutor triedy a prepísať afterExecute () háčik:

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

Týmto spôsobom by sa čistenie MDC stalo po každom normálnom alebo výnimočnom vykonaní automaticky. Nie je teda potrebné to robiť manuálne:

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

Teraz môžeme prepísať to isté demo s našou novou implementáciou exekútora:

ExecutorService Exekútor = nový MdcAwareThreadPoolExecutor (3, 3, 0, MINUTES, nový LinkedBlockingQueue (), vlákno :: nový, nový AbortPolicy ()); TransactionFactory transactionFactory = nový TransactionFactory (); pre (int i = 0; i <10; i ++) {Transfer tx = transactionFactory.newInstance (); Spustiteľná úloha = nový Slf4jRunnable (tx); Exekútor.submit (úloha); } executor.shutdown ();

7. Záver

MDC má veľa aplikácií, hlavne v scenároch, v ktorých vykonávanie niekoľkých rôznych vlákien spôsobí vložené správy protokolu, ktoré by sa inak ťažko čítali.

A ako sme videli, je podporovaný tromi z najbežnejšie používaných rámcov protokolovania v Jave.

Ako obvykle, zdroje nájdete na GitHub.


$config[zx-auto] not found$config[zx-overlay] not found