Forbedret Java-logning med Mapped Diagnostic Context (MDC)

1. Oversigt

I denne artikel vil vi undersøge brugen af Kortlagt diagnostisk kontekst (MDC) for at forbedre applikationslogning.

Grundideen til Kortlagt diagnostisk kontekst er at give en måde at berige logmeddelelser med stykker information, der muligvis ikke er tilgængelige i det omfang, hvor loggningen faktisk finder sted, men som virkelig kan være nyttig til at spore udførelsen af ​​programmet bedre.

2. Hvorfor bruge MDC

Lad os starte med et eksempel. Lad os antage, at vi er nødt til at skrive software, der overfører penge. Vi oprettede en Overførsel klasse til at repræsentere nogle grundlæggende oplysninger: et unikt overførsels-id og afsenderens navn:

offentlig klasseoverførsel {privat strengtransaktionId; privat strengafsender; privat Langt beløb; offentlig overførsel (strengtransaktionId, strengafsender, langt beløb) {this.transactionId = transactionId; this.sender = afsender; dette.beløb = beløb; } offentlig String getSender () {return sender; } public String getTransactionId () {return transactionId; } offentlig Lang getAmount () {returbeløb; }} 

For at udføre overførslen skal vi bruge en tjeneste, der understøttes af en simpel API:

offentlig abstrakt klasse TransferService {offentlig boolsk overførsel (langt beløb) {// opretter forbindelse til fjerntjenesten for faktisk at overføre penge} abstrakt beskyttet ugyldighed førTransfer (langt beløb); abstrakt beskyttet ugyldighed efter overførsel (lang mængde, boolsk resultat); } 

Det beforeTransfer () og efter overførsel () metoder kan tilsidesættes for at køre brugerdefineret kode lige før og lige efter overførslen er afsluttet.

Vi vil udnytte beforeTransfer () og efter overførsel () til log nogle oplysninger om overførslen.

Lad os oprette serviceimplementeringen:

import org.apache.log4j.Logger; import com.baeldung.mdc.TransferService; offentlig klasse Log4JTransferService udvider TransferService {private Logger logger = Logger.getLogger (Log4JTransferService.class); @ Override beskyttet ugyldigt førTransfer (langt beløb) {logger.info ("Forbereder sig på overførsel" + beløb + "$."); } @ Override beskyttet ugyldigt efterTransfer (langt beløb, boolsk resultat) {logger.info ("Er overførsel af" + beløb + "$ gennemført med succes?" + Resultat + "."); }} 

Det vigtigste spørgsmål at bemærke her er, at når logbeskeden oprettes, er det ikke muligt at få adgang til Overførsel objekt - kun beløbet er tilgængeligt, hvilket gør det umuligt at logge hverken transaktions-id'et eller afsenderen.

Lad os indstille det sædvanlige log4j.egenskaber fil til at logge på konsollen:

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 

Lad os endelig oprette en lille applikation, der er i stand til at køre flere overførsler på samme tid gennem en ExecutorService:

offentlig klasse TransferDemo {public static void main (String [] args) {ExecutorService executor = Executors.newFixedThreadPool (3); TransactionFactory transactionFactory = ny TransactionFactory (); for (int i = 0; i <10; i ++) {Transfer tx = transactionFactory.newInstance (); Kørbar opgave = ny Log4JRunnable (tx); executor.submit (opgave); } executor.shutdown (); }}

Vi bemærker, at for at bruge ExecutorService, er vi nødt til at pakke udførelsen af Log4JTransferService i en adapter, fordi executor.submit () forventer en Kan køres:

offentlig klasse Log4JRunnable implementerer Runnable {private Transfer tx; offentlig Log4JRunnable (Transfer tx) {this.tx = tx; } offentlig ugyldig kørsel () {log4jBusinessService.transfer (tx.getAmount ()); }} 

Når vi kører vores demo-applikation, der administrerer flere overførsler på samme tid, opdager vi det meget hurtigt loggen er ikke nyttig, som vi gerne vil have den. Det er kompliceret at spore udførelsen af ​​hver overførsel, fordi den eneste nyttige information, der logges, er det overførte beløb og navnet på den tråd, der udfører den pågældende overførsel.

Hvad mere er, er det umuligt at skelne mellem to forskellige transaktioner med det samme beløb, der udføres af den samme tråd, fordi de relaterede loglinjer ser stort set ens ud:

... 519 [pool-1-thread-3] INFO Log4JBusinessService - Forbereder overførsel af 1393 $. 911 [pool-1-thread-2] INFO Log4JBusinessService - Er overførslen på 1065 $ gennemført med succes? rigtigt. 911 [pool-1-thread-2] INFO Log4JBusinessService - Forberedelse til overførsel af 1189 $. 989 [pool-1-thread-1] INFO Log4JBusinessService - Er overførslen på 1350 $ gennemført med succes? rigtigt. 989 [pool-1-thread-1] INFO Log4JBusinessService - Forberedelse til overførsel af 1178 $. 1245 [pool-1-thread-3] INFO Log4JBusinessService - Er overførslen på 1393 $ gennemført? rigtigt. 1246 [pool-1-thread-3] INFO Log4JBusinessService - Forbereder overførsel af 1133 $. 1507 [pool-1-thread-2] INFO Log4JBusinessService - Er overførslen på 1189 $ gennemført med succes? rigtigt. 1508 [pool-1-thread-2] INFO Log4JBusinessService - Forberedelse til overførsel af 1907 $. 1639 [pool-1-thread-1] INFO Log4JBusinessService - Er overførslen på 1178 $ gennemført med succes? rigtigt. 1640 [pool-1-thread-1] INFO Log4JBusinessService - Forbereder overførsel af 674 $. ... 

Heldigvis MDC kan hjælpe.

3. MDC i Log4j

Lad os introducere MDC.

MDC i Log4j giver os mulighed for at udfylde en kortlignende struktur med informationstykker, der er tilgængelige for appenderen, når logbeskeden faktisk skrives.

MDC-strukturen er internt knyttet til den udførende tråd på samme måde a Trådlokal variabel ville være.

Og så er ideen på højt niveau:

  1. at udfylde MDC med stykker information, som vi vil stille til rådighed for appenderen
  2. log derefter en besked
  3. og til sidst skal du rydde MDC

Mønsteret for appenderen skal naturligvis ændres for at hente de variabler, der er gemt i MDC.

Så lad os derefter ændre koden i henhold til disse retningslinjer:

import org.apache.log4j.MDC; offentlig klasse Log4JRunnable implementerer Runnable {private Transfer tx; privat statisk Log4JTransferService log4jBusinessService = ny Log4JTransferService (); offentlig Log4JRunnable (Transfer tx) {this.tx = tx; } public void run () {MDC.put ("transaction.id", tx.getTransactionId ()); MDC.put ("transaktion.ejer", tx.getSender ()); log4jBusinessService.transfer (tx.getAmount ()); MDC.klar (); }} 

Ikke overraskende MDC.put () bruges til at tilføje en nøgle og en tilsvarende værdi i MDC mens MDC.klar () tømmer MDC.

Lad os nu ændre log4j.egenskaber for at udskrive de oplysninger, som vi lige har gemt i MDC. Det er nok at ændre konverteringsmønsteret ved hjælp af %X{} pladsholder for hver post indeholdt i MDC, vi vil gerne være logget:

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

Hvis vi nu kører applikationen, bemærker vi, at hver linje også indeholder oplysningerne om den transaktion, der behandles, hvilket gør det meget nemmere for os at spore udførelsen af ​​applikationen:

638 [pool-1-thread-2] INFO Log4JBusinessService - Er overførslen på 1104 $ gennemført med succes? rigtigt. - tx.id = 2 tx.owner = Marc 638 [pool-1-thread-2] INFO Log4JBusinessService - Forbereder overførsel af 1685 $. - tx.id = 4 tx.owner = John 666 [pool-1-thread-1] INFO Log4JBusinessService - Er overførslen af ​​1985 $ gennemført med succes? rigtigt. - tx.id = 1 tx.owner = Marc 666 [pool-1-thread-1] INFO Log4JBusinessService - Forberedelse til overførsel af 958 $. - tx.id = 5 tx.owner = Susan 739 [pool-1-thread-3] INFO Log4JBusinessService - Er overførslen på 783 $ gennemført med succes? rigtigt. - tx.id = 3 tx.owner = Samantha 739 [pool-1-thread-3] INFO Log4JBusinessService - Forbereder overførsel af 1024 $. - tx.id = 6 tx.owner = John 1259 [pool-1-thread-2] INFO Log4JBusinessService - Er overførslen på 1685 $ gennemført med succes? falsk. - tx.id = 4 tx.owner = John 1260 [pool-1-thread-2] INFO Log4JBusinessService - Forberedelse til overførsel af 1667 $. - tx.id = 7 tx.ejer = Marc 

4. MDC i Log4j2

Den samme funktion er også tilgængelig i Log4j2, så lad os se, hvordan du bruger den.

Lad os først oprette en TransferService underklasse, der logger ved hjælp af Log4j2:

import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; offentlig klasse Log4J2TransferService udvider TransferService {privat statisk endelig Logger-logger = LogManager.getLogger (); @Override beskyttet ugyldigt førTransfer (langt beløb) {logger.info ("Forbereder at overføre {} $.", Beløb); } @ Override beskyttet ugyldigt efterTransfer (langt beløb, boolsk resultat) {logger.info ("Er overførslen af ​​{} $ gennemført med succes? {}.", Beløb, resultat); }} 

Lad os derefter ændre den kode, der bruger MDC, som faktisk kaldes Trådkontekst i Log4j2:

import org.apache.log4j.MDC; offentlig klasse Log4J2Runnable implementerer Runnable {private final Transaction tx; privat Log4J2BusinessService log4j2BusinessService = ny Log4J2BusinessService (); offentlig Log4J2Runnable (Transaktion 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 (); }} 

Igen, ThreadContext.put () tilføjer en post i MDC og ThreadContext.clearAll () fjerner alle eksisterende poster.

Vi savner stadig log4j2.xml fil for at konfigurere logning. Som vi kan bemærke, er syntaksen for at specificere hvilke MDC-poster, der skal logges, den samme som den, der blev brugt i Log4j:

Lad os igen udføre applikationen, og vi ser MDC-oplysningerne blive udskrevet i loggen:

1119 [pool-1-thread-3] INFO Log4J2BusinessService - Er overførslen på 1198 $ gennemført med succes? rigtigt. - tx.id = 3 tx.owner = Samantha 1120 [pool-1-thread-3] INFO Log4J2BusinessService - Forbereder overførsel af 1723 $. - tx.id = 5 tx.owner = Samantha 1170 [pool-1-thread-2] INFO Log4J2BusinessService - Er overførslen på 701 $ gennemført med succes? rigtigt. - tx.id = 2 tx.owner = Susan 1171 [pool-1-thread-2] INFO Log4J2BusinessService - Forberedelse til overførsel af 1108 $. - tx.id = 6 tx.owner = Susan 1794 [pool-1-thread-1] INFO Log4J2BusinessService - Er overførslen på 645 $ gennemført med succes? rigtigt. - tx.id = 4 tx.ejer = Susan 

5. MDC i SLF4J / Logback

MDC er også tilgængelig i SLF4J under den betingelse, at den understøttes af det underliggende loggningsbibliotek.

Både Logback og Log4j understøtter MDC, som vi lige har set, så vi har ikke brug for noget specielt for at bruge det med en standardopsætning.

Lad os forberede det sædvanlige TransferService underklasse, denne gang ved hjælp af Simple Logging Facade til Java:

import org.slf4j.Logger; import org.slf4j.LoggerFactory; endelig klasse Slf4TransferService udvider TransferService {privat statisk endelig Logger-logger = LoggerFactory.getLogger (Slf4TransferService.class); @Override beskyttet ugyldigt førTransfer (langt beløb) {logger.info ("Forbereder at overføre {} $.", Beløb); } @ Override beskyttet ugyldigt efterTransfer (langt beløb, boolsk resultat) {logger.info ("Er overførslen af ​​{} $ gennemført med succes? {}.", Beløb, resultat); }} 

Lad os nu bruge SLF4Js smag af MDC. I dette tilfælde er syntaksen og semantikken den samme som i log4j:

import org.slf4j.MDC; offentlig klasse Slf4jRunnable implementerer Runnable {private final Transaction tx; offentlig Slf4jRunnable (Transaktion tx) {this.tx = tx; } public void run () {MDC.put ("transaction.id", tx.getTransactionId ()); MDC.put ("transaktion.ejer", tx.getOwner ()); ny Slf4TransferService (). overførsel (tx.getAmount ()); MDC.klar (); }} 

Vi er nødt til at give logback-konfigurationsfilen, logback.xml:

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

Igen ser vi, at oplysningerne i MDC tilføjes korrekt til de loggede meddelelser, selvom disse oplysninger ikke udtrykkeligt er angivet i log.info () metode:

1020 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Er overførslen på 1869 $ gennemført med succes? rigtigt. - tx.id = 3 tx.owner = John 1021 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Forberedelse til overførsel af 1303 $. - tx.id = 6 tx.owner = Samantha 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Er overførslen på 1498 $ gennemført med succes? rigtigt. - tx.id = 4 tx.owner = Marc 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Forberedelse til overførsel af 1528 $. - tx.id = 7 tx.owner = Samantha 1492 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Er overførslen på 1110 $ gennemført med succes? rigtigt. - tx.id = 5 tx.owner = Samantha 1493 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Forberedelse til overførsel af 644 $. - tx.id = 8 tx.ejer = John

Det er værd at bemærke, at hvis vi opretter SLF4J-backend til et logningssystem, der ikke understøtter MDC, springes alle relaterede påkald simpelthen over uden bivirkninger.

6. MDC og trådbassiner

MDC-implementeringer bruger normalt Trådlokals til at gemme den kontekstuelle information. Det er en nem og rimelig måde at opnå trådsikkerhed på. Vi skal dog være forsigtige med at bruge MDC med trådpuljer.

Lad os se, hvordan kombinationen af Trådlokal-baserede MDC'er og trådpuljer kan være farlige:

  1. Vi får en tråd fra trådpuljen.
  2. Derefter gemmer vi nogle kontekstuelle oplysninger i MDC ved hjælp af MDC.put () eller ThreadContext.put ().
  3. Vi bruger disse oplysninger i nogle logfiler, og på en eller anden måde glemte vi at rydde MDC-sammenhængen.
  4. Den lånte tråd kommer tilbage til trådpuljen.
  5. Efter et stykke tid får applikationen den samme tråd fra puljen.
  6. Da vi ikke ryddede op i MDC sidste gang, ejer denne tråd stadig nogle data fra den tidligere udførelse.

Dette kan medføre uventede uoverensstemmelser mellem henrettelser. En måde at forhindre dette på er altid at huske at rydde op i MDC-konteksten i slutningen af ​​hver udførelse. Denne fremgangsmåde kræver normalt strengt menneskeligt tilsyn og er derfor fejlbehæftet.

En anden tilgang er at bruge ThreadPoolExecutor kroge og udføre nødvendige oprydninger efter hver udførelse. For at gøre det kan vi udvide ThreadPoolExecutor klasse og tilsidesætte afterExecute () krog:

offentlig klasse MdcAwareThreadPoolExecutor udvider ThreadPoolExecutor {public MdcAwareThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandoolShareLue ,izeTime, timeTime, TimeTrackTime, TimeTrackTime, TimeTit, TimeTit, TimeTit, TimeTit, TimeTrackTime, TimeTrackTime) } @ Override beskyttet ugyldigt efterExecute (Runnable r, Throwable t) {System.out.println ("Rengøring af MDC-kontekst"); MDC.klar (); org.apache.log4j.MDC.clear (); ThreadContext.clearAll (); }}

På denne måde ville MDC-oprydningen ske automatisk efter hver normale eller ekstraordinære udførelse. Så det er ikke nødvendigt at gøre det manuelt:

@Override public void run () {MDC.put ("transaction.id", tx.getTransactionId ()); MDC.put ("transaktion.ejer", tx.getSender ()); ny Slf4TransferService (). overførsel (tx.getAmount ()); }

Nu kan vi omskrive den samme demo med vores nye eksekveringsimplementering:

ExecutorService-eksekutor = ny MdcAwareThreadPoolExecutor (3, 3, 0, MINUTES, new LinkedBlockingQueue (), Thread :: new, new AbortPolicy ()); TransactionFactory transactionFactory = ny TransactionFactory (); for (int i = 0; i <10; i ++) {Transfer tx = transactionFactory.newInstance (); Kørbar opgave = ny Slf4jRunnable (tx); executor.submit (opgave); } executor.shutdown ();

7. Konklusion

MDC har mange applikationer, hovedsageligt i scenarier, hvor udførelsen af ​​flere forskellige tråde forårsager sammenflettede logbeskeder, der ellers ville være svære at læse.

Og som vi har set, understøttes det af tre af de mest anvendte logningsrammer i Java.

Som sædvanligt finder du kilderne på GitHub.