簡體   English   中英

如何在spring-webflux WebFilter中正確使用slf4j MDC

[英]How to correctly use slf4j MDC in spring-webflux WebFilter

我參考了博客文章Contextual Logging with Reactor Context and MDC但我不知道如何在 WebFilter 中訪問反應器上下文。

@Component
public class RequestIdFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        List<String> myHeader =  exchange.getRequest().getHeaders().get("X-My-Header");

        if (myHeader != null && !myHeader.isEmpty()) {
            MDC.put("myHeader", myHeader.get(0));
        }

        return chain.filter(exchange);
    }
}

你可以做類似下面的事情,你可以用你喜歡的任何類設置context ,在這個例子中,我只使用了標題 - 但自定義類就可以了。 如果您在此處設置它,那么任何帶有處理程序等的日志記錄也將可以訪問context
下面的logWithContext設置 MDC 並在之后清除它。 顯然,這可以用你喜歡的任何東西代替。

public class RequestIdFilter  implements WebFilter {

    private Logger LOG = LoggerFactory.getLogger(RequestIdFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        HttpHeaders headers = exchange.getRequest().getHeaders();
        return chain.filter(exchange)
                .doAfterSuccessOrError((r, t) -> logWithContext(headers, httpHeaders -> LOG.info("Some message with MDC set")))
                .subscriberContext(Context.of(HttpHeaders.class, headers));
    }

    static void logWithContext(HttpHeaders headers, Consumer<HttpHeaders> logAction) {
        try {
            headers.forEach((name, values) -> MDC.put(name, values.get(0)));
            logAction.accept(headers);
        } finally {
            headers.keySet().forEach(MDC::remove);
        }

    }

}

這是一種基於最新方法的解決方案,截至2021 年 5 月,取自官方文檔

import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Signal;
import reactor.util.context.Context;

@Slf4j
@Configuration
public class RequestIdFilter implements WebFilter {

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    String requestId = getRequestId(request.getHeaders());
    return chain
        .filter(exchange)
        .doOnEach(logOnEach(r -> log.info("{} {}", request.getMethod(), request.getURI())))
        .contextWrite(Context.of("CONTEXT_KEY", requestId));
  }

  private String getRequestId(HttpHeaders headers) {
    List<String> requestIdHeaders = headers.get("X-Request-ID");
    return requestIdHeaders == null || requestIdHeaders.isEmpty()
        ? UUID.randomUUID().toString()
        : requestIdHeaders.get(0);
  }

  public static <T> Consumer<Signal<T>> logOnEach(Consumer<T> logStatement) {
    return signal -> {
      String contextValue = signal.getContextView().get("CONTEXT_KEY");
      try (MDC.MDCCloseable cMdc = MDC.putCloseable("MDC_KEY", contextValue)) {
        logStatement.accept(signal.get());
      }
    };
  }

  public static <T> Consumer<Signal<T>> logOnNext(Consumer<T> logStatement) {
    return signal -> {
      if (!signal.isOnNext()) return;
      String contextValue = signal.getContextView().get("CONTEXT_KEY");
      try (MDC.MDCCloseable cMdc = MDC.putCloseable("MDC_KEY", contextValue)) {
        logStatement.accept(signal.get());
      }
    };
  }
}

鑒於您的application.properties有以下行:

logging.pattern.level=[%X{MDC_KEY}] %5p

那么每次調用端點時,您的服務器日志都會包含這樣的日志:

2021-05-06 17:07:41.852 [60b38305-7005-4a05-bac7-ab2636e74d94]  INFO 20158 --- [or-http-epoll-6] my.package.RequestIdFilter    : GET http://localhost:12345/my-endpoint/444444/

每次您想在反應式上下文中手動記錄某些內容時,您都必須將以下內容添加到您的反應式鏈中:

.doOnEach(logOnNext(r -> log.info("Something")))

如果您希望將X-Request-ID傳播到其他服務以進行分布式跟蹤,您需要從響應式上下文(而不是從 MDC)讀取它並使用以下內容包裝您的WebClient代碼:

Mono.deferContextual(
    ctx -> {
      RequestHeadersSpec<?> request = webClient.get().uri(uri);
      request = request.header("X-Request-ID", ctx.get("CONTEXT_KEY"));
      // The rest of your request logic...
    });

從 Spring Boot 2.2 開始,有 Schedulers.onScheduleHook 可以讓您處理 MDC:

Schedulers.onScheduleHook("mdc", runnable -> {
    Map<String, String> map = MDC.getCopyOfContextMap();
    return () -> {
        if (map != null) {
            MDC.setContextMap(map);
        }
        try {
            runnable.run();
        } finally {
            MDC.clear();
        }
    };
});

或者, Hooks.onEachOperator 可用於通過訂閱者上下文傳遞 MDC 值。

http://ttddyy.github.io/mdc-with-webclient-in-webmvc/

這不是完整的 MDC 解決方案,例如,在我的情況下,我無法清除 R2DBC 線程中的 MDC 值。

更新:這篇文章真的解決了我的 MDC 問題: https : //www.novatec-gmbh.de/en/blog/how-can-the-mdc-context-be-used-in-the-reactive-spring-applications/

它提供了基於訂閱者上下文更新 MDC 的正確方法。

將它與由AuthenticationWebFilter填充的SecurityContext::class.java鍵結合起來,您將能夠將用戶登錄放入您的日志中。

我的解決方案基於Reactor 3 Reference Guide 方法,但使用 doOnSuccess 而不是 doOnEach。

主要思想是通過以下方式使用Context進行 MDC 傳播:

  1. 使用來自上游流的 MDC state 填充下游上下文(將由派生線程使用)(可以通過.contextWrite(context -> Context.of(MDC.getCopyOfContextMap()))
  2. 在派生線程中訪問下游上下文並使用下游上下文中的值填充派生線程中的 MDC(主要挑戰
  3. 清除下游上下文中的 MDC(可以通過.doFinally(signalType -> MDC.clear())完成)

主要問題是在派生線程中訪問下游上下文。 您可以使用最方便的方法實施第 2 步)。 但這是我的解決方案:

webclient.post()
   .bodyValue(someRequestData)
   .retrieve()
   .bodyToMono(String.class)
// By this action we wrap our response with a new Mono and also 
// in parallel fill MDC with values from a downstream Context because
// we have an access to it
   .flatMap(wrapWithFilledMDC())
   .doOnSuccess(response -> someActionWhichRequiresFilledMdc(response)))
// Fill a downstream context with the current MDC state
   .contextWrite(context -> Context.of(MDC.getCopyOfContextMap())) 
// Allows us to clear MDC from derived threads
   .doFinally(signalType -> MDC.clear())
   .block();
// Function which implements second step from the above main idea
public static <T> Function<T, Mono<T>> wrapWithFilledMDC() {
// Using deferContextual we have an access to downstream Context, so
// we can just fill MDC in derived threads with 
// values from the downstream Context
   return item -> Mono.deferContextual(contextView -> {
// Function for filling MDC with Context values 
// (you can apply your action)
      fillMdcWithContextView(contextView);
      return Mono.just(item);
   });
}
public static void fillMdcWithContextValues(ContextView contextView) {
   contextView.forEach(
      (key, value) -> {
          if (key instanceof String keyStr && value instanceof String valueStr) {
             MDC.put(keyStr, valueStr);
          }
   });
}

這種方法也可以應用於 doOnError 和 onErrorResume 方法,因為主要思想是相同的。

使用的版本:

  • spring-boot:2.7.3
  • spring-webflux:5.3.22(來自 spring-boot)
  • 反應堆核心:3.4.22(來自 spring-webflux)
  • reactor.netty:1.0.22(來自 spring-webflux)

我通過以下方式實現了這一目標:-

package com.nks.app.filter;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

/**
 * @author nks
 */
@Component
@Slf4j
public class SessionIDFilter implements WebFilter {
    
    private static final String APP_SESSION_ID = "app-session-id";
    
    /**
     * Process the Web request and (optionally) delegate to the next
     * {@code WebFilter} through the given {@link WebFilterChain}.
     *
     * @param serverWebExchange the current server exchange
     * @param webFilterChain    provides a way to delegate to the next filter
     * @return {@code Mono<Void>} to indicate when request processing is complete
     */
    @Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
        serverWebExchange.getResponse()
                .getHeaders().add(APP_SESSION_ID, serverWebExchange.getRequest().getHeaders().getFirst(APP_SESSION_ID));
        MDC.put(APP_SESSION_ID, serverWebExchange.getRequest().getHeaders().getFirst(APP_SESSION_ID));
        log.info("[{}] : Inside filter of SessionIDFilter, ADDED app-session-id in MDC Logs", MDC.get(APP_SESSION_ID));
        return webFilterChain.filter(serverWebExchange);
    }
}

並且,可以記錄與線程的app-session-id關聯的值。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM