マップ診断コンテキスト(MDC)によるJavaロギングの改善

マップされた診断コンテキスト(MDC)を使用したJavaロギングの改善

1. 概要

この記事では、Mapped Diagnostic Context(MDC)を使用してアプリケーションのログを改善する方法について説明します。

Mapped Diagnostic Contextの基本的な考え方は、ログが実際に発生するスコープでは利用できない情報でログメッセージを充実させる方法を提供することですが、プログラムの実行をより適切に追跡するのに実際に役立ちます。 。

2. MDCを使用する理由

例から始めましょう。 送金するソフトウェアを作成する必要があるとしましょう。 いくつかの基本情報を表すためにTransferクラスを設定します:一意の転送IDと送信者の名前:

public class Transfer {
    private String transactionId;
    private String sender;
    private Long amount;

    public Transfer(String transactionId, String sender, long amount) {
        this.transactionId = transactionId;
        this.sender = sender;
        this.amount = amount;
    }

    public String getSender() {
        return sender;
    }

    public String getTransactionId() {
        return transactionId;
    }

    public Long getAmount() {
        return amount;
    }
}

転送を実行するには、単純な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);

    abstract protected void afterTransfer(long amount, boolean outcome);
}

beforeTransfer()メソッドとafterTransfer()メソッドをオーバーライドして、転送が完了する直前と直後にカスタムコードを実行できます。

beforeTransfer()afterTransfer()log some information about the transferに活用します。

サービスの実装を作成しましょう:

import org.apache.log4j.Logger;
import com.example.mdc.TransferService;

public class Log4JTransferService extends TransferService {
    private Logger logger = Logger.getLogger(Log4JTransferService.class);

    @Override
    protected void beforeTransfer(long amount) {
        logger.info("Preparing to transfer " + amount + "$.");
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info(
          "Has transfer of " + amount + "$ completed successfully ? " + outcome + ".");
    }
}

ここで注意すべき主な問題は、–when the log message is created, it is not possible to access the Transfer object –金額だけがアクセス可能であり、トランザクションIDまたは送信者のいずれかをログに記録できないことです。

コンソールにログオンするために、通常のlog4j.propertiesファイルを設定しましょう。

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

最後に、ExecutorServiceを介して複数の転送を同時に実行できる小さなアプリケーションをセットアップしましょう。

public class TransferDemo {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        TransactionFactory transactionFactory = new TransactionFactory();
        for (int i = 0; i < 10; i++) {
            Transfer tx = transactionFactory.newInstance();
            Runnable task = new Log4JRunnable(tx);
            executor.submit(task);
        }
        executor.shutdown();
    }
}

executor.submit()Runnableを予期しているため、ExecutorServiceを使用するには、Log4JTransferServiceの実行をアダプターでラップする必要があることに注意してください。

public class Log4JRunnable implements Runnable {
    private Transfer tx;

    public Log4JRunnable(Transfer tx) {
        this.tx = tx;
    }

    public void run() {
        log4jBusinessService.transfer(tx.getAmount());
    }
}

複数の転送を同時に管理するデモアプリケーションを実行すると、the log is not useful as we would like it to beがすぐに見つかります。 ログに記録される有用な情報は、送金された金額とその特定の送金を実行しているスレッドの名前だけであるため、各転送の実行を追跡するのは複雑です。

さらに、関連するログ行は実質的に同じように見えるため、同じスレッドによって実行された同じ量の2つの異なるトランザクションを区別することは不可能です。

...
519  [pool-1-thread-3]  INFO Log4JBusinessService
  - Preparing to transfer 1393$.
911  [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1065$ completed successfully ? true.
911  [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1189$.
989  [pool-1-thread-1]  INFO Log4JBusinessService
  - Has transfer of 1350$ completed successfully ? true.
989  [pool-1-thread-1]  INFO Log4JBusinessService
  - Preparing to transfer 1178$.
1245 [pool-1-thread-3]  INFO Log4JBusinessService
  - Has transfer of 1393$ completed successfully ? true.
1246 [pool-1-thread-3]  INFO Log4JBusinessService
  - Preparing to transfer 1133$.
1507 [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1189$ completed successfully ? true.
1508 [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1907$.
1639 [pool-1-thread-1]  INFO Log4JBusinessService
  - Has transfer of 1178$ completed successfully ? true.
1640 [pool-1-thread-1]  INFO Log4JBusinessService
  - Preparing to transfer 674$.
...

幸いなことに、MDCが役立ちます。

3. Log4jのMDC

MDCを紹介しましょう。

Log4jのMDCを使用すると、ログメッセージが実際に書き込まれるときに、アペンダーがアクセスできる情報でマップのような構造を埋めることができます。

MDC構造体は、ThreadLocal変数と同じ方法で、実行中のスレッドに内部的にアタッチされます。

したがって、高レベルのアイデアは次のとおりです。

  1. アペンダーが利用できるようにする情報をMDCに入力する

  2. その後、メッセージを記録します

  3. そして最後に、MDCをクリアします

MDCに保存されている変数を取得するには、アペンダーのパターンを明らかに変更する必要があります。

それでは、次のガイドラインに従ってコードを変更しましょう。

import org.apache.log4j.MDC;

public class Log4JRunnable implements Runnable {
    private Transfer tx;
    private static Log4JTransferService log4jBusinessService = new 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();
    }
}

当然のことながら、MDC.put()はMDCにキーと対応する値を追加するために使用され、MDC.clear()はMDCを空にします。

log4j.propertiesを変更して、MDCに保存したばかりの情報を出力してみましょう。 ログに記録するMDCに含まれる各エントリの%X\{}プレースホルダーを使用して、変換パターンを変更するだけで十分です。

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

ここで、アプリケーションを実行すると、各行に処理中のトランザクションに関する情報も含まれているため、アプリケーションの実行を追跡するのがはるかに簡単になります。

638  [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1104$ completed successfully ? true. - tx.id=2 tx.owner=Marc
638  [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1685$. - tx.id=4 tx.owner=John
666  [pool-1-thread-1]  INFO Log4JBusinessService
  - Has transfer of 1985$ completed successfully ? true. - tx.id=1 tx.owner=Marc
666  [pool-1-thread-1]  INFO Log4JBusinessService
  - Preparing to transfer 958$. - tx.id=5 tx.owner=Susan
739  [pool-1-thread-3]  INFO Log4JBusinessService
  - Has transfer of 783$ completed successfully ? true. - tx.id=3 tx.owner=Samantha
739  [pool-1-thread-3]  INFO Log4JBusinessService
  - Preparing to transfer 1024$. - tx.id=6 tx.owner=John
1259 [pool-1-thread-2]  INFO Log4JBusinessService
  - Has transfer of 1685$ completed successfully ? false. - tx.id=4 tx.owner=John
1260 [pool-1-thread-2]  INFO Log4JBusinessService
  - Preparing to transfer 1667$. - tx.id=7 tx.owner=Marc

4. Log4j2のMDC

Log4j2でもまったく同じ機能を利用できるので、その使用方法を見てみましょう。

まず、Log4j2を使用してログを記録するTransferServiceサブクラスを設定しましょう。

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(long amount) {
        logger.info("Preparing to transfer {}$.", amount);
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info("Has transfer of {}$ completed successfully ? {}.", amount, outcome);
    }
}

次に、MDCを使用するコードを変更しましょう。これは、Log4j2では実際にはThreadContextと呼ばれています。

import org.apache.log4j.MDC;

public class Log4J2Runnable implements Runnable {
    private final Transaction tx;
    private Log4J2BusinessService log4j2BusinessService = new 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();
    }
}

ここでも、ThreadContext.put()はMDCにエントリを追加し、ThreadContext.clearAll()は既存のすべてのエントリを削除します。

ロギングを構成するためのlog4j2.xmlファイルがまだありません。 注意できるように、どのMDCエントリをログに記録するかを指定する構文は、Log4jで使用される構文と同じです。


    
        
            
        
    
    
        
        
            
        
    

もう一度、アプリケーションを実行してみましょう。MDC情報がログに出力されます。

1119 [pool-1-thread-3]  INFO Log4J2BusinessService
  - Has transfer of 1198$ completed successfully ? true. - tx.id=3 tx.owner=Samantha
1120 [pool-1-thread-3]  INFO Log4J2BusinessService
  - Preparing to transfer 1723$. - tx.id=5 tx.owner=Samantha
1170 [pool-1-thread-2]  INFO Log4J2BusinessService
  - Has transfer of 701$ completed successfully ? true. - tx.id=2 tx.owner=Susan
1171 [pool-1-thread-2]  INFO Log4J2BusinessService
  - Preparing to transfer 1108$. - tx.id=6 tx.owner=Susan
1794 [pool-1-thread-1]  INFO Log4J2BusinessService
  - Has transfer of 645$ completed successfully ? true. - tx.id=4 tx.owner=Susan

5. SLF4J / LogbackのMDC

MDCは、基盤となるロギングライブラリでサポートされているという条件の下で、SLF4Jでも使用できます。

LogbackとLog4jはどちらも、今見たようにMDCをサポートしているため、標準のセットアップで使用するために特別なことは何も必要ありません。

通常のTransferServiceサブクラスを準備しましょう。今回は、Java用のSimple LoggingFacadeを使用します。

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(long amount) {
        logger.info("Preparing to transfer {}$.", amount);
    }

    @Override
    protected void afterTransfer(long amount, boolean outcome) {
        logger.info("Has transfer of {}$ completed successfully ? {}.", amount, outcome);
    }
}

SLF4JのMDCフレーバーを使用してみましょう。 この場合、構文とセマンティクスはlog4jのものと同じです。

import org.slf4j.MDC;

public class Slf4jRunnable implements 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());
        new Slf4TransferService().transfer(tx.getAmount());
        MDC.clear();
    }
}

ログバック構成ファイルlogback.xmlを提供する必要があります。


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

この情報がlog.info()メソッドで明示的に提供されていなくても、MDCの情報がログに記録されたメッセージに適切に追加されていることがわかります。

1020 [pool-1-thread-3]  INFO c.b.m.s.Slf4jBusinessService
  - Has transfer of 1869$ completed successfully ? true. - tx.id=3 tx.owner=John
1021 [pool-1-thread-3]  INFO c.b.m.s.Slf4jBusinessService
  - Preparing to transfer 1303$. - tx.id=6 tx.owner=Samantha
1221 [pool-1-thread-1]  INFO c.b.m.s.Slf4jBusinessService
  - Has transfer of 1498$ completed successfully ? true. - tx.id=4 tx.owner=Marc
1221 [pool-1-thread-1]  INFO c.b.m.s.Slf4jBusinessService
  - Preparing to transfer 1528$. - tx.id=7 tx.owner=Samantha
1492 [pool-1-thread-2]  INFO c.b.m.s.Slf4jBusinessService
  - Has transfer of 1110$ completed successfully ? true. - tx.id=5 tx.owner=Samantha
1493 [pool-1-thread-2]  INFO c.b.m.s.Slf4jBusinessService
  - Preparing to transfer 644$. - tx.id=8 tx.owner=John

MDCをサポートしないロギングシステムにSLF4Jバックエンドを設定した場合、関連するすべての呼び出しが副作用なしで単純にスキップされることに注意してください。

6. 結論

MDCには多くのアプリケーションがあります。主に、いくつかの異なるスレッドの実行により、読みにくいログメッセージがインターリーブされるシナリオです。

これまで見てきたように、Javaで最も広く使用されている3つのロギングフレームワークによってサポートされています。

いつものように、ソースover on GitHubが見つかります。