Structured logging (JSON) + Logback + MDC
Цель урока
Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.
Теория · для собеса
1 Зачем JSON-логи ▸
Plain text логи отлично читает человек. Но машина их парсит плохо. JSON-логи:
- Удобно индексируются (ELK, Loki, Splunk)
- Легко фильтровать (
level=ERROR AND service=taskflow) - Структурированы (поля = ключи)
2 MDC (Mapped Diagnostic Context) ▸
MDC — это ThreadLocal хранилище «контекста» запроса:
- requestId
- userId
- sessionId
- traceId (для distributed tracing)
Каждый лог-запись автоматически включает значения MDC.
{
"@timestamp": "2026-06-29T16:55:00.000Z",
"level": "INFO",
"service": "taskflow",
"requestId": "abc-123",
"userId": "alice",
"message": "Task created with id=42"
}
3 Logback + Spring Boot ▸
Spring Boot по умолчанию использует Logback. Конфиг — logback-spring.xml в src/main/resources.
4 logstash-logback-encoder ▸
Библиотека, которая добавляет JSON-аппендер к Logback. Без неё — только plain text.
---
Практика: улучшаем CRUD
-
1Зависимости
dependencies { implementation("net.logstash.logback:logstash-logback-encoder:8.0") } -
2
src/main/resources/logback-spring.xml%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n requestId userId traceId {"service":"${appName}","profile":"${activeProfile}"} /var/log/taskflow/app.log /var/log/taskflow/app.%d{yyyy-MM-dd}.%i.log.gz 100MB 30 10GB requestId userId -
3Фильтр для MDC
logging/RequestLoggingFilter.java:package com.taskflow.logging; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.lang.NonNull; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.UUID; @Component public class RequestLoggingFilter extends OncePerRequestFilter { private static final String REQUEST_ID_HEADER = "X-Request-Id"; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain) throws ServletException, IOException { // Берём X-Request-Id из заголовка (если есть) или генерим String requestId = request.getHeader(REQUEST_ID_HEADER); if (requestId == null || requestId.isBlank()) { requestId = UUID.randomUUID().toString(); } MDC.put("requestId", requestId); response.setHeader(REQUEST_ID_HEADER, requestId); // UserId из SecurityContext (если есть) Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getName())) { MDC.put("userId", auth.getName()); } try { long start = System.currentTimeMillis(); chain.doFilter(request, response); long duration = System.currentTimeMillis() - start; MDC.put("durationMs", String.valueOf(duration)); } finally { MDC.clear(); } } } -
4Использование в сервисе
@Slf4j // Lombok, генерирует log @Service public class TaskService { // ... public TaskResponse create(CreateTaskRequest req) { log.info("Creating task: title={}", req.title()); // MDC автоматически в каждой строке // ... log.info("Task created with id={}", saved.getId()); return mapper.toResponse(saved); } }@Slf4j— аннотация Lombok. Если не используете Lombok:private static final Logger log = LoggerFactory.getLogger(TaskService.class); -
5Пример JSON-вывода (prod)
{ "@timestamp": "2026-06-29T16:55:00.123Z", "level": "INFO", "service": "taskflow", "profile": "prod", "requestId": "f3e1b5a0-1234-5678-9abc-def012345678", "userId": "alice", "durationMs": "45", "logger_name": "com.taskflow.service.TaskService", "thread_name": "http-nio-8080-exec-1", "message": "Creating task: title=Test" } -
6Маскирование чувствительных данных
{"password":"***","secret":"***"} Или через
MessageConverterдля конкретных классов. -
7Тест
# Запускаем в prod SPRING_PROFILES_ACTIVE=prod ./gradlew bootRun # Делаем запрос с X-Request-Id $ curl -H "X-Request-Id: test-123" -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/api/tasks # В логах: {"@timestamp":"...","level":"INFO","requestId":"test-123","userId":"alice","message":"Creating task: title=Test"} {"@timestamp":"...","level":"INFO","requestId":"test-123","userId":"alice","message":"Task created with id=42"}Все логи в рамках одного запроса имеют
requestId=test-123— легко найти все записи одного запроса.---
Зачем это на собесе
После урока ты должен уметь ответить на:
level=INFO AND userId=alice.5 вопросов на углубление
Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.
Разбор внутри: API логирования и реализация по умолчанию.
Разбор внутри: фильтр важности и правильная детализация логов.
Разбор внутри: requestId/userId как контекст каждого лога.
Разбор внутри: пароли, токены, PII и маскирование.
Разбор внутри: логирование без блокировки основного потока.
Готов идти дальше?
Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.
🚀 Перейти к Уроку 21Сначала пройди все секции и выбери хотя бы 1 вопрос
⬅️ Назад к Уроку 19