Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
MDC (Mapped Diagnostic Context) в Java
Отличный вопрос про инструмент, который критичен для отладки production систем! MDC помогает отследить логи одного запроса через всю систему.
Что такое MDC?
MDC (Mapped Diagnostic Context) - это механизм логирования, который позволяет привязать контекстную информацию (например, ID запроса или ID пользователя) к одному потоку выполнения (thread), чтобы она автоматически добавлялась во все логи этого потока.
Проблема без MDC
Представь логи в production:
2024-03-23 10:15:30 [main] INFO UserService - Creating user
2024-03-23 10:15:31 [main] INFO PaymentService - Processing payment
2024-03-23 10:15:32 [main] INFO EmailService - Sending confirmation
2024-03-23 10:15:33 [main] ERROR Database - Connection failed
Когда у тебя 1000 одновременных запросов:
2024-03-23 10:15:30 [thread-1] INFO UserService - Creating user
2024-03-23 10:15:30 [thread-2] INFO UserService - Creating user
2024-03-23 10:15:31 [thread-1] INFO PaymentService - Processing payment
2024-03-23 10:15:31 [thread-2] INFO PaymentService - Processing payment
2024-03-23 10:15:32 [thread-1] INFO EmailService - Sending confirmation
2024-03-23 10:15:32 [thread-2] ERROR Database - Connection failed
Невозможно отследить логи одного запроса! Какой заказ относится к какому пользователю?
Решение - MDC
MDC добавляет уникальный ID к каждому логу:
2024-03-23 10:15:30 [request-001] INFO UserService - Creating user
2024-03-23 10:15:30 [request-002] INFO UserService - Creating user
2024-03-23 10:15:31 [request-001] INFO PaymentService - Processing payment
2024-03-23 10:15:31 [request-002] INFO PaymentService - Processing payment
2024-03-23 10:15:32 [request-001] INFO EmailService - Sending confirmation
2024-03-23 10:15:32 [request-002] ERROR Database - Connection failed
Теперь ясно: все логи с request-001 относятся к одному пользователю!
Как использовать MDC в Spring Boot
1. Добавьте зависимость (обычно уже есть в spring-boot-starter-logging):
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
2. Создайте фильтр для установки MDC в каждом запросе:
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component
public class MDCFilter extends OncePerRequestFilter {
private static final String REQUEST_ID = "requestId";
private static final String USER_ID = "userId";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// Генерируем уникальный ID запроса
String requestId = UUID.randomUUID().toString();
MDC.put(REQUEST_ID, requestId);
// Можно также добавить информацию о пользователе (если она доступна)
String userId = extractUserId(request);
if (userId != null) {
MDC.put(USER_ID, userId);
}
try {
filterChain.doFilter(request, response);
} finally {
// ВАЖНО: очищай MDC после выполнения запроса
MDC.clear();
}
}
private String extractUserId(HttpServletRequest request) {
// Извлекаем userId из header, token, session, и т.д.
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
// Парси JWT и извлекай userId
return "user-123"; // пример
}
return null;
}
}
3. Настройте logback для вывода MDC:
<!-- src/main/resources/logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Добавляем requestId и userId в логи -->
<pattern>
%d{HH:mm:ss.SSS} [%X{requestId}] [%X{userId}] %-5level %logger{36} - %msg%n
<!-- %X{requestId} - значение из MDC с ключом "requestId" -->
<!-- %X{userId} - значение из MDC с ключом "userId" -->
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
Пример использования в сервисе
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void createOrder(Order order) {
// requestId уже установлен в MDC фильтром
logger.info("Creating order: {}", order.getId());
// Лог автоматически включит requestId!
validateOrder(order);
saveOrder(order);
notifyCustomer(order);
}
private void validateOrder(Order order) {
logger.debug("Validating order items");
// requestId АВТОМАТИЧЕСКИ добавится в этот лог тоже
}
private void saveOrder(Order order) {
logger.info("Saving order to database");
// И в этот
}
private void notifyCustomer(Order order) {
logger.info("Sending notification email");
// И в этот
}
}
Результат логирования
2024-03-23 10:15:30.123 [550e8400-e29b-41d4-a716-446655440000] [user-456] INFO OrderService - Creating order: ORD-789
2024-03-23 10:15:30.124 [550e8400-e29b-41d4-a716-446655440000] [user-456] DEBUG OrderService - Validating order items
2024-03-23 10:15:30.125 [550e8400-e29b-41d4-a716-446655440000] [user-456] INFO OrderService - Saving order to database
2024-03-23 10:15:30.126 [550e8400-e29b-41d4-a716-446655440000] [user-456] INFO OrderService - Sending notification email
2024-03-23 10:15:30.127 [a1b2c3d4-e5f6-4g7h-i8j9-k0l1m2n3o4p5] [user-789] INFO OrderService - Creating order: ORD-790
2024-03-23 10:15:30.128 [a1b2c3d4-e5f6-4g7h-i8j9-k0l1m2n3o4p5] [user-789] DEBUG OrderService - Validating order items
Теперь чётко видно какие логи относятся к какому запросу и какому пользователю!
Важные моменты
1. Очистка MDC - КРИТИЧНО в thread pool:
// ПРАВИЛЬНО - очищаем в finally
try {
MDC.put("requestId", id);
processRequest();
} finally {
MDC.clear(); // ОБЯЗАТЕЛЬНО!
}
// НЕПРАВИЛЬНО - утечка MDC в thread pool
MDC.put("requestId", id);
processRequest();
// Если поток переиспользуется, old requestId останется!
2. MDC в асинхронных операциях:
// Проблема: асинхронный код выполняется в другом потоке
@Async
public void sendEmailAsync(String email) {
// requestId ПОТЕРЯЕТСЯ - мы в другом потоке!
logger.info("Sending email to {}", email);
}
// Решение: копируй MDC
public void sendEmailAsync(String email) {
Map<String, String> mdc = MDC.getCopyOfContextMap();
asyncExecutor.execute(() -> {
MDC.setContextMap(mdc); // восстанавливаем контекст
try {
logger.info("Sending email to {}", email);
} finally {
MDC.clear();
}
});
}
3. Несколько ключей в MDC:
MDC.put("requestId", "req-123");
MDC.put("userId", "user-456");
MDC.put("correlationId", "corr-789");
MDC.put("traceId", "trace-000");
// Все будут добавлены в логи согласно pattern в logback
// %X{requestId} %X{userId} %X{correlationId} %X{traceId}
Рекомендуемые ключи MDC
| Ключ | Значение | Пример |
|---|---|---|
requestId | Уникальный ID запроса | 550e8400-e29b-41d4 |
userId | ID пользователя | user-456 |
correlationId | ID для связи микросервисов | corr-789 |
traceId | Трейс для OpenTelemetry | trace-000 |
sessionId | ID сессии | session-123 |
Вывод
MDC - это essential инструмент для:
- Отслеживания запросов через всю систему
- Отладки production проблем - мгновенно найти все логи одного запроса
- Мониторинга и анализа - корреляция логов
- Безопасности - отслеживание действий пользователей
Best Practice: используй MDC в каждом Spring Boot приложении с фильтром, который добавляет requestId в начале и очищает в конце.