УРОК 20 / 30 0%

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. 1
    Зависимости
    
    dependencies {
        implementation("net.logstash.logback:logstash-logback-encoder:8.0")
    }
    
  2. 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. 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. 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. 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. 6
    Маскирование чувствительных данных
    
    
        
            
                {"password":"***","secret":"***"}
            
        
    
    

    Или через MessageConverter для конкретных классов.

  7. 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 — легко найти все записи одного запроса.

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

Поиск по userId: «что делала Alice за последний час?» — level=INFO AND userId=alice.
Debugging по requestId: ошибка у юзера? Просим прислать X-Request-Id, грепаем логи.
Distributed tracing: requestId = traceId → связь между сервисами (при использовании OpenTelemetry).
Маскирование паролей/токенов: обязательно для PCI DSS / GDPR compliance.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
SLF4J vs Logback — кто за что отвечает?

Разбор внутри: API логирования и реализация по умолчанию.

2
TRACE, DEBUG, INFO, WARN, ERROR — как выбрать уровень?

Разбор внутри: фильтр важности и правильная детализация логов.

3
Зачем нужен MDC в structured logging?

Разбор внутри: requestId/userId как контекст каждого лога.

4
Какие данные нельзя логировать?

Разбор внутри: пароли, токены, PII и маскирование.

5
Зачем нужны async appenders в Logback?

Разбор внутри: логирование без блокировки основного потока.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 21

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 19
🎉
Новый тир
Новый тир достигнут!