Forbedret Java-logging med kartlagt diagnostisk kontekst (MDC)

1. Oversikt

I denne artikkelen vil vi utforske bruken av Kartlagt diagnostisk kontekst (MDC) for å forbedre applikasjonsloggingen.

Den grunnleggende ideen om Kartlagt diagnostisk kontekst er å gi en måte å berike loggmeldinger med informasjon som ikke kan være tilgjengelig i det omfanget hvor loggingen faktisk skjer, men som virkelig kan være nyttig for å spore gjennomføringen av programmet bedre.

2. Hvorfor bruke MDC

La oss starte med et eksempel. La oss anta at vi må skrive programvare som overfører penger. Vi satte opp en Overføre klasse for å representere grunnleggende informasjon: en unik overførings-ID og navnet på avsenderen:

offentlig klasseoverføring {privat strengtransaksjon; privat streng avsender; privat Lang beløp; public Transfer (String transactionId, String sender, long amount) {this.transactionId = transactionId; this.sender = avsender; dette.beløp = beløp; } offentlig String getSender () {retur avsender; } public String getTransactionId () {return transactionId; } offentlig Lang getAmount () {returbeløp; }} 

For å utføre overføringen må vi bruke en tjeneste støttet av et enkelt API:

offentlig abstrakt klasse TransferService {offentlig boolsk overføring (langt beløp) {// kobles til fjerntjenesten for faktisk å overføre penger} abstrakt beskyttet ugyldighet førTransfer (langt beløp); abstrakt beskyttet tomrom etter overføring (lang mengde, boolsk utfall); } 

De beforeTransfer () og etter overføring () metoder kan overstyres for å kjøre tilpasset kode rett før og rett etter at overføringen er fullført.

Vi kommer til å utnytte beforeTransfer () og etter overføring () til logg litt informasjon om overføringen.

La oss lage tjenesteimplementeringen:

importer org.apache.log4j.Logger; importere com.baeldung.mdc.TransferService; offentlig klasse Log4JTransferService utvider TransferService {private Logger logger = Logger.getLogger (Log4JTransferService.class); @Override beskyttet tomrom før Transfer (lang mengde) {logger.info ("Forbereder seg på overføring" + beløp + "$."); } @ Override beskyttet ugyldig etter overføring (langt beløp, boolsk utfall) {logger.info ("Har overføringen av" + beløp + "$ fullført?" + Utfall + "."); }} 

Hovedproblemet å merke seg her er at når loggmeldingen er opprettet, er det ikke mulig å få tilgang til Overføre gjenstand - bare beløpet er tilgjengelig, noe som gjør det umulig å logge verken transaksjons-ID eller avsender.

La oss sette opp det vanlige log4j.egenskaper fil for å 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 

La oss endelig sette opp et lite program som er i stand til å kjøre flere overføringer samtidig gjennom en ExecutorService:

public class 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 (); Kjørbar oppgave = ny Log4JRunnable (tx); executor.submit (oppgave); } executor.shutdown (); }}

Vi bemerker at for å bruke ExecutorService, må vi pakke utførelsen av Log4JTransferService i en adapter fordi executor.submit () forventer a Kjørbar:

offentlig klasse Log4JRunnable implementerer Runnable {private Transfer tx; offentlig Log4JRunnable (Overfør tx) {this.tx = tx; } public void run () {log4jBusinessService.transfer (tx.getAmount ()); }} 

Når vi kjører demo-applikasjonen vår som administrerer flere overføringer samtidig, oppdager vi veldig raskt det loggen er ikke nyttig slik vi ønsker at den skal være. Det er komplisert å spore gjennomføringen av hver overføring, fordi den eneste nyttige informasjonen som blir logget, er hvor mye penger som er overført og navnet på tråden som utfører den aktuelle overføringen.

Dessuten er det umulig å skille mellom to forskjellige transaksjoner av samme beløp utført av samme tråd fordi de relaterte logglinjene ser stort sett ut:

... 519 [pool-1-thread-3] INFO Log4JBusinessService - Forbereder overføring av 1393 $. 911 [pool-1-thread-2] INFO Log4JBusinessService - Er overføringen på 1065 $ fullført? ekte. 911 [pool-1-thread-2] INFO Log4JBusinessService - Forbereder overføring av 1189 $. 989 [pool-1-thread-1] INFO Log4JBusinessService - Er overføringen på 1350 $ fullført? ekte. 989 [pool-1-thread-1] INFO Log4JBusinessService - Forbereder overføring av 1178 $. 1245 [pool-1-thread-3] INFO Log4JBusinessService - Er overføringen på 1393 $ fullført? ekte. 1246 [pool-1-thread-3] INFO Log4JBusinessService - Forbereder overføring av 1133 $. 1507 [pool-1-thread-2] INFO Log4JBusinessService - Er overføringen på 1189 $ fullført? ekte. 1508 [pool-1-thread-2] INFO Log4JBusinessService - Forbereder overføring av 1907 $. 1639 [pool-1-thread-1] INFO Log4JBusinessService - Er overføringen på 1178 $ fullført? ekte. 1640 [pool-1-thread-1] INFO Log4JBusinessService - Forbereder overføring av 674 $. ... 

Heldigvis, MDC kan hjelpe.

3. MDC i Log4j

La oss introdusere MDC.

MDC i Log4j lar vi oss fylle en kartlignende struktur med informasjon som er tilgjengelig for appenderen når loggmeldingen faktisk er skrevet.

MDC-strukturen er internt festet til den utførende tråden på samme måte a Trådlokal variabel ville være.

Og så er ideen på høyt nivå:

  1. å fylle MDC med informasjon som vi ønsker å gjøre tilgjengelig for appender
  2. logg deretter en melding
  3. og til slutt, fjern MDC

Mønsteret til appender bør åpenbart endres for å hente variablene som er lagret i MDC.

Så la oss så endre koden i henhold til disse retningslinjene:

importer 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 ("transaction.owner", tx.getSender ()); log4jBusinessService.transfer (tx.getAmount ()); MDC.klar (); }} 

Ikke overraskende MDC.put () brukes til å legge til en nøkkel og en tilsvarende verdi i MDC mens MDC.klar () tømmer MDC.

La oss nå endre log4j.egenskaper for å skrive ut informasjonen vi nettopp har lagret i MDC. Det er nok å endre konverteringsmønsteret ved hjelp av % X {} plassholder for hver oppføring i MDC, vi ønsker å bli logget:

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

Nå, hvis vi kjører applikasjonen, vil vi merke oss at hver linje også inneholder informasjonen om transaksjonen som behandles, noe som gjør det mye enklere for oss å spore gjennomføringen av applikasjonen:

638 [pool-1-thread-2] INFO Log4JBusinessService - Er overføringen på 1104 $ fullført? ekte. - tx.id = 2 tx.owner = Marc 638 [pool-1-thread-2] INFO Log4JBusinessService - Forbereder overføring av 1685 $. - tx.id = 4 tx.owner = John 666 [pool-1-thread-1] INFO Log4JBusinessService - Er overføringen på 1985 $ fullført? ekte. - tx.id = 1 tx.owner = Marc 666 [pool-1-thread-1] INFO Log4JBusinessService - Forbereder overføring av 958 $. - tx.id = 5 tx.owner = Susan 739 [pool-1-thread-3] INFO Log4JBusinessService - Er overføringen på 783 $ fullført? ekte. - tx.id = 3 tx.owner = Samantha 739 [pool-1-thread-3] INFO Log4JBusinessService - Forbereder overføring av 1024 $. - tx.id = 6 tx.owner = John 1259 [pool-1-thread-2] INFO Log4JBusinessService - Er overføringen på 1685 $ fullført? falsk. - tx.id = 4 tx.owner = John 1260 [pool-1-thread-2] INFO Log4JBusinessService - Forbereder overføring av 1667 $. - tx.id = 7 tx.eier = Marc 

4. MDC i Log4j2

Den samme funksjonen er også tilgjengelig i Log4j2, så la oss se hvordan du bruker den.

La oss først sette opp en TransferService underklasse som logger ved hjelp av Log4j2:

importer org.apache.logging.log4j.LogManager; importer org.apache.logging.log4j.Logger; offentlig klasse Log4J2TransferService utvider TransferService {privat statisk slutt Logger logger = LogManager.getLogger (); @ Override beskyttet tomrom før overføring (langt beløp) {logger.info ("Forbereder seg på overføring {} $.", Beløp); } @ Override beskyttet ugyldig etter overføring (langt beløp, boolsk utfall) {logger.info ("Har overføringen av {} $ fullført? {}.", Beløp, utfall); }} 

La oss så endre koden som bruker MDC, som faktisk kalles Trådkontekst i Log4j2:

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

En gang til, ThreadContext.put () legger til en oppføring i MDC og ThreadContext.clearAll () fjerner alle eksisterende oppføringer.

Vi savner fortsatt log4j2.xml fil for å konfigurere loggingen. Som vi kan merke, er syntaksen for å spesifisere hvilke MDC-oppføringer som skal logges den samme som den som ble brukt i Log4j:

Igjen, la oss utføre applikasjonen, og vi vil se MDC-informasjonen skrives ut i loggen:

1119 [pool-1-thread-3] INFO Log4J2BusinessService - Er overføringen på 1198 $ fullført? ekte. - tx.id = 3 tx.owner = Samantha 1120 [pool-1-thread-3] INFO Log4J2BusinessService - Forbereder overføring av 1723 $. - tx.id = 5 tx.owner = Samantha 1170 [pool-1-thread-2] INFO Log4J2BusinessService - Er overføringen på 701 $ fullført? ekte. - tx.id = 2 tx.owner = Susan 1171 [pool-1-thread-2] INFO Log4J2BusinessService - Forbereder overføring av 1108 $. - tx.id = 6 tx.owner = Susan 1794 [pool-1-thread-1] INFO Log4J2BusinessService - Er overføringen på 645 $ fullført? ekte. - tx.id = 4 tx.eeier = Susan 

5. MDC i SLF4J / Logback

MDC er tilgjengelig i SLF4J også under forutsetning av at den støttes av det underliggende loggbiblioteket.

Både Logback og Log4j støtter MDC som vi nettopp har sett, så vi trenger ikke noe spesielt for å bruke den med et standardoppsett.

La oss forberede det vanlige TransferService underklasse, denne gangen ved hjelp av Simple Logging Facade for Java:

importer org.slf4j.Logger; importer org.slf4j.LoggerFactory; endelig klasse Slf4TransferService utvider TransferService {privat statisk slutt Logger logger = LoggerFactory.getLogger (Slf4TransferService.class); @ Override beskyttet tomrom før overføring (langt beløp) {logger.info ("Forbereder seg på overføring {} $.", Beløp); } @ Override beskyttet ugyldig etter overføring (langt beløp, boolsk utfall) {logger.info ("Har overføringen av {} $ fullført? {}.", Beløp, utfall); }} 

La oss nå bruke SLF4Js smak av MDC. I dette tilfellet er syntaksen og semantikken den samme som i log4j:

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

Vi må gi logback-konfigurasjonsfilen, logback.xml:

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

Igjen vil vi se at informasjonen i MDC er riktig lagt til de loggede meldingene, selv om denne informasjonen ikke eksplisitt er gitt i log.info () metode:

1020 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Er overføringen på 1869 $ fullført? ekte. - tx.id = 3 tx.owner = John 1021 [pool-1-thread-3] INFO c.b.m.s.Slf4jBusinessService - Forbereder overføring av 1303 $. - tx.id = 6 tx.owner = Samantha 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Er overføringen på 1498 $ fullført? ekte. - tx.id = 4 tx.owner = Marc 1221 [pool-1-thread-1] INFO c.b.m.s.Slf4jBusinessService - Forbereder overføring av 1528 $. - tx.id = 7 tx.owner = Samantha 1492 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Er overføringen på 1110 $ fullført? ekte. - tx.id = 5 tx.owner = Samantha 1493 [pool-1-thread-2] INFO c.b.m.s.Slf4jBusinessService - Forbereder overføring av 644 $. - tx.id = 8 tx.eier = John

Det er verdt å merke seg at i tilfelle vi setter opp SLF4J-back-enden til et loggesystem som ikke støtter MDC, vil alle relaterte påkallelser bare hoppes over uten bivirkninger.

6. MDC og trådbassenger

MDC-implementeringer bruker vanligvis Trådlokals for å lagre den kontekstuelle informasjonen. Det er en enkel og rimelig måte å oppnå trådsikkerhet på. Vi bør imidlertid være forsiktige med å bruke MDC med trådpooler.

La oss se hvordan kombinasjonen av Trådlokal-baserte MDC og trådbassenger kan være farlige:

  1. Vi får en tråd fra trådbassenget.
  2. Deretter lagrer vi litt kontekstuell informasjon i MDC ved hjelp av MDC.put () eller ThreadContext.put ().
  3. Vi bruker denne informasjonen i noen logger, og på en eller annen måte glemte vi å fjerne MDC-konteksten.
  4. Den lånte tråden kommer tilbake til trådbassenget.
  5. Etter en stund får applikasjonen den samme tråden fra bassenget.
  6. Siden vi ikke ryddet opp i MDC sist, eier denne tråden fremdeles noen data fra forrige utførelse.

Dette kan føre til uventede uoverensstemmelser mellom henrettelser. En måte å forhindre dette på er å alltid huske å rydde opp i MDC-konteksten på slutten av hver utførelse. Denne tilnærmingen trenger vanligvis streng menneskelig tilsyn og er derfor utsatt for feil.

En annen tilnærming er å bruke ThreadPoolExecutor kroker og utfør nødvendige oppryddinger etter hver utførelse. For å gjøre det kan vi utvide ThreadPoolExecutor klasse og overstyre afterExecute () krok:

offentlig klasse MdcAwareThreadPoolExecutor utvider ThreadPoolExecutor {public MdcAwareThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandlerProcessiveSize, timeTimeTime, SizeTime, TimeTrader, SuperTimeTime, TimeTrader, SuperTimeTime, TimeTrader, SuperTimeTime, TimeTit, TimeTrader, SuperTimeTime, TimeTit, TimeTit, TimeTrader, SuperTimeTime) } @ Override-beskyttet tomrom etterExecute (Runnable r, Throwable t) {System.out.println ("Rengjøring av MDC-kontekst"); MDC.klar (); org.apache.log4j.MDC.clear (); ThreadContext.clearAll (); }}

På denne måten vil MDC-oppryddingen skje automatisk etter hver normal eller eksepsjonell kjøring. Så det er ikke nødvendig å gjøre det manuelt:

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

Nå kan vi skrive om den samme demoen med den nye implementeringen av vår eksekutor:

ExecutorService executor = new 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 (); Kjørbar oppgave = ny Slf4jRunnable (tx); executor.submit (oppgave); } executor.shutdown ();

7. Konklusjon

MDC har mange applikasjoner, hovedsakelig i scenarier der utførelsen av flere forskjellige tråder forårsaker sammenflettede loggmeldinger som ellers ville være vanskelig å lese.

Og som vi har sett, støttes den av tre av de mest brukte loggerammene i Java.

Som vanlig finner du kildene på GitHub.


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