Что такое MDC в контексте логирования в Java и для чего он используется?

Ответ

MDC (Mapped Diagnostic Context) — это механизм, предоставляемый фреймворками логирования (SLF4J/Logback, Log4j 2), для прикрепления контекстной информации ко всем log-сообщениям, создаваемым в рамках текущего потока выполнения (ThreadLocal).

Основное назначение: Сквозная идентификация запросов или операций в логах, что критически важно для отладки распределенных систем.

Типичные сценарии использования:

  • ID запроса (requestId): Для группировки всех логов, относящихся к одному HTTP-запросу.
  • ID пользователя (userId): Для отслеживания действий конкретного пользователя.
  • ID сессии (sessionId): Для анализа поведения в рамках сессии.
  • ID транзакции (transactionId): В batch-процессах или финансовых операциях.

Пример использования в веб-приложении (с использованием фильтра):

import org.slf4j.MDC;
import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

public class MdcLoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            // Помещаем уникальный ID запроса в MDC
            MDC.put("requestId", UUID.randomUUID().toString());
            MDC.put("userIp", request.getRemoteAddr());
            // Все логи внутри цепочки фильтров и обработки запроса будут содержать эти поля
            chain.doFilter(request, response);
        } finally {
            // Очищаем контекст после обработки запроса (важно!)
            MDC.clear();
        }
    }
}

Конфигурация в logback.xml для вывода значений MDC:

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{HH:mm:ss} [%thread] [%X{requestId}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

Вывод в лог: 14:25:03 [http-nio-8080-exec-1] [abc-123-def] INFO c.example.MyController - Processing request

Важные особенности:

  • ThreadLocal: Контекст привязан к потоку. При ручном создании потоков или использовании пулов (например, ExecutorService) MDC не копируется автоматически. Для передачи контекста нужно использовать MDC.getCopyOfContextMap() и MDC.setContextMap().
  • Очистка: Крайне важно очищать MDC (например, в блоке finally) после завершения операции, чтобы избежать утечки памяти и некорректных данных в последующих операциях, обработанных тем же потоком из пула.

Ответ 18+ 🔞

А, MDC! Ну это ж, блядь, та самая штука, которая позволяет не ебаться как конь в попытках понять, какой из тысячи одинаковых логов к какому запросу относится. Представь: у тебя сервис, на него наваливается овердохуища запросов одновременно, логи летят в одну кучу, и ты потом, как мудак, полдня сидишь и пытаешьсь собрать пазл — что к чему. Вот MDC эту проблему и решает, нахуй.

Суть проще пареной репы: это как временная татуировка для потока выполнения. Зашёл запрос в поток — нарисовали на нём маркером requestId=abc-123. И всё, что этот поток дальше ни делает, все его логи будут помечены этим ID. Красота, а не жизнь!

Где это, блядь, реально спасает жопу:

  • Сквозной ID запроса — главный кейс. Без этого в микросервисах просто пиздец, а не отладка.
  • ID юзверя — чтобы отследить, что этот ёбаный пользователь опять что-то сломал.
  • ID сессии — ну, для полного аудита, чтобы знать, кто, когда и какую дичь творил.

Вот как это выглядит в коде, на примере простого фильтра:

import org.slf4j.MDC;
import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

public class MdcLoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            // Вешаем на поток бирку с уникальным номером запроса
            MDC.put("requestId", UUID.randomUUID().toString());
            MDC.put("userIp", request.getRemoteAddr());
            // Теперь все логи внутри цепочки будут с этими полями
            chain.doFilter(request, response);
        } finally {
            // А это, сука, ВАЖНО! Стираем бирку после работы. Как в бане — помылся, убери за собой!
            MDC.clear();
        }
    }
}

А в конфиге логгера (logback.xml) просто добавляем волшебный %X{ключ}:

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{HH:mm:ss} [%thread] [%X{requestId}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

И в логах вместо каши будет чётко: 14:25:03 [http-nio-8080-exec-1] [abc-123-def] INFO c.example.MyController - Processing request

Но есть, блядь, подводные грабли, о которые все расшибают лбы:

  • ThreadLocal, ёпта! Контекст живёт только в одном потоке. Если ты внутри запроса запускаешь асинхронную хуйню через ExecutorService или новые потоки — там MDC будет пустой! Контекст не копируется автоматом. Придётся вручную передавать словарик (MDC.getCopyOfContextMap()) и в новом потоке его применять (MDC.setContextMap()). Иначе логи потеряются, и ты останешься с носом.
  • Очистка — святое дело! Забыл MDC.clear() в finally? Поздравляю, у тебя теперь утечка контекста. Поток из пула вернётся с чужим requestId на борту, и следующий запрос в логах будет под левым ID. Полный пиздец и неразбериха. Так что не забывай за собой убирать, мартышлюшка!