Улучшено ведение журнала Java с отображенным контекстом диагностики (MDC)

Улучшенное ведение журнала Java с отображенным диагностическим контекстом (MDC)

1. обзор

В этой статье мы рассмотрим использованиеMapped Diagnostic Context (MDC) для улучшения ведения журнала приложений.

Основная идеяMapped Diagnostic Context состоит в том, чтобы предоставить способ обогащения сообщений журнала частями информации, которые могут быть недоступны в области, где фактически происходит ведение журнала, но это действительно может быть полезно для лучшего отслеживания выполнения программы. .

2. Зачем использовать MDC

Давайте начнем с примера. Предположим, нам нужно написать программу, которая переводит деньги. Мы создали классTransfer для представления некоторой базовой информации: уникального идентификатора передачи и имени отправителя:

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 - доступна только сумма, что делает невозможным регистрацию идентификатора транзакции или отправителя.

Давайте настроим обычный файл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();
    }
}

Отметим, что для использованияExecutorService нам нужно обернуть выполнениеLog4JTransferService в адаптер, потому чтоexecutor.submit() ожидаетRunnable:

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. Сложно отслеживать выполнение каждой передачи, потому что единственная полезная информация, которая регистрируется, - это сумма переведенных денег и имя потока, который выполняет этот конкретный перевод.

Более того, невозможно различить две разные транзакции с одинаковым объемом, выполняемые одним потоком, потому что связанные строки журнала выглядят практически одинаково:

...
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. MDC в Log4j

ВведемMDC.

MDC в Log4j позволяет нам заполнить структуру, похожую на карту, фрагментами информации, доступными для приложения, когда сообщение журнала фактически записано.

Структура 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. Достаточно изменить шаблон преобразования, используя заполнитель%X\{} для каждой записи, содержащейся в MDC, которую мы хотели бы регистрировать:

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. MDC в Log4j2

Та же функция доступна и в Log4j2, так что давайте посмотрим, как ее использовать.

Давайте сначала настроим подклассTransferService, который будет вести журнал с помощью 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(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, который на самом деле называетсяThreadContext в Log4j2:

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. MDC в SLF4J / Logback

MDC также доступен в SLF4J при условии, что он поддерживается базовой библиотекой журналирования.

Как Logback, так и Log4j поддерживают MDC, как мы только что видели, поэтому нам не нужно ничего особенного, чтобы использовать его со стандартной настройкой.

Давайте подготовим обычный подклассTransferService, на этот раз используя Simple Logging Facade для 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(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 от SLF4J. В этом случае синтаксис и семантика такие же, как у 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();
    }
}

Мы должны предоставить файл конфигурации Logbacklogback.xml:


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

Мы снова увидим, что информация в MDC правильно добавляется к зарегистрированным сообщениям, даже если эта информация явно не предоставляется в методе log.info ():

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

Стоит отметить, что в случае, если мы настроим серверную часть SLF4J для системы регистрации, которая не поддерживает MDC, все связанные вызовы будут просто пропущены без побочных эффектов.

6. Заключение

MDC имеет множество приложений, в основном в сценариях, в которых выполнение нескольких разных потоков вызывает чередующиеся сообщения журнала, которые в противном случае было бы трудно прочитать

И, как мы видели, он поддерживается тремя наиболее широко используемыми платформами ведения журналов в Java.

Как обычно, вы найдете источникиover on GitHub.