Опирается на правила: R-OBS-CTX-1R-OBS-CTX-4 и R-OBS-CTX-X1R-OBS-CTX-X3 из Observability Style Guide → раздел 6. Context propagation (MDC).

Важно знать

  • MdcFilter с @Order(HIGHEST_PRECEDENCE) populates requestId на каждом incoming HTTP request.
  • MDC.clear() в finally — обязательно. Иначе контекст утекает между requests в thread pool, и чужой userId попадает в чужие логи.
  • traceId/spanId автоматически через OTel Logback appender. Не добавлять руками.
  • userId populates после JWT-валидации в Security filter chain.
  • Async / CompletableFuture / @AsyncTaskDecorator с copyOfContextMapsetContextMapclear. Без декоратора traces разрываются.
  • MDC.put только в filter/interceptor, не в handler/service. Иначе clear-логика неочевидна.
  • Утечка MDC — compliance incident: чужой userId в чужих логах под чужими действиями.

MDC (Mapped Diagnostic Context) — потоково-локальное хранилище в Slf4j/Logback. Каждый log-entry автоматически содержит текущий MDC. Это даёт requestId/traceId/userId в каждом логе без явного передавания через все методы. Цена — дисциплина в lifecycle: populated в правильном месте, cleared в правильном месте.

MdcFilter

R-OBS-CTX-1: один filter на entry-point.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MdcFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
                                     FilterChain chain) throws ServletException, IOException {
        var requestId = Optional.ofNullable(req.getHeader("X-Request-Id"))
            .orElseGet(() -> UUID.randomUUID().toString());
        MDC.put("requestId", requestId);
        resp.setHeader("X-Request-Id", requestId);
        try {
            chain.doFilter(req, resp);
        } finally {
            MDC.clear();
        }
    }
}

Что делает:

  • Берёт X-Request-Id из header, если есть; иначе генерирует UUID.
  • Возвращает тот же X-Request-Id в response — клиент может его залогировать и предоставить при инциденте.
  • Кладёт в MDC, и все log-entries в этом request будут с этим requestId.
  • В finally чистит MDC.

@Order(HIGHEST_PRECEDENCE) — filter работает до Security filter chain. Значит ошибки в Security уже залогируются с requestId. Без этого инциденты с auth-фейлами расследуются вслепую.

TraceId/spanId автоматически

R-OBS-CTX-2: подключаем opentelemetry-logback-mdc-1.0 (см. Tracing).

implementation("io.opentelemetry.instrumentation:opentelemetry-logback-mdc-1.0")

OTel сам populates traceId, spanId в MDC на каждое incoming HTTP request. Руками MDC.put("traceId", ...) не делать — конфликт с авто.

TaskDecorator для @Async

R-OBS-CTX-3: проблема — @Async и CompletableFuture.runAsync(...) выполняются на отдельном thread, у которого MDC пустой.

@Bean
public TaskDecorator mdcTaskDecorator() {
    return runnable -> {
        var contextMap = MDC.getCopyOfContextMap();
        return () -> {
            try {
                if (contextMap != null) {
                    MDC.setContextMap(contextMap);
                }
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    };
}

@Bean("taskExecutor")
public TaskExecutor taskExecutor(TaskDecorator decorator) {
    var executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(50);
    executor.setQueueCapacity(100);
    executor.setTaskDecorator(decorator);
    executor.setThreadNamePrefix("async-");
    executor.initialize();
    return executor;
}

Что делает декоратор:

  1. На submitting thread — копирует текущий MDC (getCopyOfContextMap).
  2. На executing thread — устанавливает скопированный context (setContextMap).
  3. После выполнения — clear, чтобы не загрязнять следующую задачу на том же thread.

Без декоратора:

@Async
public CompletableFuture<Void> sendEmail(Long userId) {
    log.info("Sending email to user");  // нет requestId, нет traceId — лог сирота
    emailClient.send(userId);
    return CompletableFuture.completedFuture(null);
}

С декоратором — requestId, traceId пропагируются. Trace в Tempo показывает span от incoming request → @Async branch без разрыва.

Для OTel context (нужен для traceparent propagation в outgoing HTTP внутри @Async) — отдельный механизм через Context.taskWrapping. OTel java agent делает это автоматически, без javaagent нужно явное оборачивание.

userId после JWT

R-OBS-CTX-4: populates после auth, не в MdcFilter.

@Component
public class UserIdMdcFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
                                     FilterChain chain) throws ServletException, IOException {
        try {
            var auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth != null && auth.isAuthenticated()) {
                MDC.put("userId", auth.getName());
            }
            chain.doFilter(req, resp);
        } finally {
            MDC.remove("userId");
        }
    }
}

Этот filter порядок — после Spring Security filter chain (чтобы SecurityContextHolder уже заполнен). В Spring Boot 3 — @Order(SecurityFilterChain.DEFAULT_ORDER + 10).

MDC.remove("userId") вместо полного MDC.clear()requestId/traceId ставит MdcFilter/OTel и они должны жить до конца request. Здесь снимаем только userId.

Что запрещено

MDC без MDC.clear() в finally

R-OBS-CTX-X1 — главная ошибка.

// КАТАСТРОФА — MDC утекает в thread pool
@Component
public class BadMdcFilter extends OncePerRequestFilter {
    protected void doFilterInternal(...) {
        MDC.put("requestId", UUID.randomUUID().toString());
        chain.doFilter(req, resp);
        MDC.clear();   // НЕ выполнится при exception
    }
}

Сценарий: handler бросает IllegalStateException, filter не выполняет MDC.clear(). Tomcat возвращает thread в pool. Через 5ms тот же thread обслуживает request другого пользователя — но MDC.requestId остался от старого. Все логи нового пользователя помечены userId=user-of-previous-request (если userId тоже не cleared).

В compliance audit это incident: чужой userId в чужих логах. Расследование — недели.

Всегда try { ... } finally { MDC.clear(); }.

MDC.put в произвольных местах

R-OBS-CTX-X2: MDC.put внутри handler-а или service — нарушает дисциплину.

// ПЛОХО
@Service
public class OrderService {
    public void process(Order order) {
        MDC.put("orderId", String.valueOf(order.id()));
        // ... обработка
        // MDC.remove("orderId") где?
    }
}

Если забыть MDC.remove("orderId")orderId утекает в логи следующих операций на том же thread. Дисциплина — MDC.put только в filter/interceptor с явным MDC.clear() в finally. Внутри handler-а используем log-параметры log.info("...orderId={}", order.id()), не MDC.

Если действительно нужна business-метка на короткий период — MDC.MDCCloseable:

try (var ignored = MDC.putCloseable("orderId", order.id().toString())) {
    process(order);
}

Автозакрытие try-with-resources снимает риск утечки.

@Async без TaskDecorator

R-OBS-CTX-X3: см. секцию выше. Traces разрываются на границе thread, requestId исчезает из логов async-операций.

Что запрещено — таблица

АнтипаттернПравилоЧто взамен
MDC.put без MDC.clear() в finallyR-OBS-CTX-X1try/finally или MDCCloseable
MDC.put в handler/service напрямуюR-OBS-CTX-X2только в filter; для коротких — MDCCloseable
@Async без TaskDecoratorR-OBS-CTX-X3TaskDecorator с copy → setContextMap → clear
MDC.put("traceId", ...) рукамиR-OBS-CTX-2opentelemetry-logback-mdc-1.0
userId в MdcFilter до SecurityR-OBS-CTX-4отдельный filter после Security
Один filter для request+user без четкого orderR-OBS-CTX-1, R-OBS-CTX-4два filter с явным @Order
MDC.clear() в обычном flow (не в finally)R-OBS-CTX-X1всегда в finally — exception-safe

Куда дальше

  • Observability → раздел 6. Context propagation — нормативные формулировки.
  • Logging — как MDC поля попадают в JSON.
  • Tracing — traceId/spanId автоматически в MDC.
  • Configuration — <includeMdcKeyName> в LogstashEncoder.
  • Auth → AUTH-16 — почему утечка userId в логах — compliance incident.
  • Resilience → async polling — TaskDecorator для async-обработки.