简体   繁体   English

在 Spring Boot 中的异常处理期间保留自定义 MDC 属性

[英]Preserve custom MDC attributes during exception-handling in Spring Boot

Short Version (With Enough Details)简短版本(有足够的细节)

How to preserve the MDC attribute added in a doFilter() method of a javax.servlet.Filter implementation ...如何保留在javax.servlet.Filter实现的doFilter()方法中添加的 MDC 属性...

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        MDC.put("token", MyToken.random()); // add the MDC attribute related to the current request processing
        chain.doFilter(request, response); // send the request to other filters and the Controller
    } finally {
        MDC.clear(); // MDC attribute must be removed so future tasks executed on the same thread would not log invalid information
    }
}

... during exception handling if an exception occurs in another filter or in the Controller (the call to chain.doFilter(...) ). ... 在异常处理期间,如果异常发生在另一个过滤器或控制器中(调用chain.doFilter(...) )。

Currently, if an exception occurs: the finally block would be executed, clearing the MDC, and then the exception would be thrown out of the filter.当前,如果发生异常:将执行 finally 块,清除 MDC,然后将异常抛出过滤器。 All the logs during the exception handling will not contain the MDC attribute.异常处理期间的所有日志都不会包含 MDC 属性。

Long Version (Overly Detailed)长版(过于详细)

I have a simple Filter implementation to intercept all requests.我有一个简单的Filter实现来拦截所有请求。 It only creates a random string of characters (a token) to be included in all logs related to processing the request.它只创建一个随机字符串(令牌)以包含在与处理请求相关的所有日志中。

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            MDC.put("token", MyToken.random());
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }

    @Override
    public void destroy() {
    }
}

The sequence of events would be:事件的顺序是:

  1. The request is received.收到请求。
  2. My doFilter() is called, adding the random token to MDC.我的doFilter()被调用,将随机标记添加到 MDC。
  3. The request is processed by calling chain.doFilter() .通过调用chain.doFilter()处理请求。
  4. Whatever happens (processing finished, error occurred), the MDC is cleared of the random token in the finally block.无论发生什么(处理完成,发生错误),MDC 都会清除finally块中的随机令牌。

The problem is that if an error occurs and it is processed (eg by a custom ErrorController implementation), the related logs do not include the token:问题是,如果发生错误并对其进行处理(例如通过自定义ErrorController实现),相关日志不包含令牌:

[2019.03.13 15:00:14.535] token:308...8bf [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : GET "/resource", parameters={}
[2019.03.13 15:00:14.551] token:308...8bf [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : Completed 400 BAD_REQUEST
[2019.03.13 15:00:14.551] token:          [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : "ERROR" dispatch for GET "/error", parameters={}
[2019.03.13 15:00:14.551] token:          [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity myproj.CustomErrorController.handleError(javax.servlet.http.HttpServletRequest)
[2019.03.13 15:00:14.551] token:          [ERROR] 8124 [https-jsse-nio-8443-exec-7] m.CustomErrorController                    : HTTP Error: Bad Request (400)
[2019.03.13 15:00:14.551] token:          [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : Exiting from "ERROR" dispatch, status 400

The finally block is executed when the exception is thrown from the Controller which handles it, resulting in clearing the MDC. finally块在处理它的Controller抛出异常时执行,导致清除 MDC。

Error handling (including the custom ErrorController ) is executed after this, which means there is no more token in the related logs.在此之后执行错误处理(包括自定义ErrorController ),这意味着相关日志中没有更多令牌。

How can I add a custom token to all the logs related to the entire processing of the request from receiving it until sending a response, including error handling?如何将自定义令牌添加到与从接收请求到发送响应的整个请求处理相关的所有日志中,包括错误处理? I want the MDC to be cleared after a response has been sent by the thread, as the last action.我希望在线程发送响应后清除 MDC,作为最后一个操作。 The MDC should be cleared regardless of what happens (successful response, exception thrown during error handling, etc).无论发生什么(成功响应、错误处理期间抛出的异常等),都应该清除 MDC。

With multiple clients using the Rest service simultaneously, logs can get really messed up.由于多个客户端同时使用 Rest 服务,日志可能会变得非常混乱。 Having a unique token attached to each log produced during the entire handling process of a certain request would greatly simplify debugging.在某个请求的整个处理过程中产生的每个日志上附加一个唯一的令牌将大大简化调试。

It seems to work well that clearing MDC on ServletRequestListener#requestDestroyed() , instead of on Filter .ServletRequestListener#requestDestroyed()而不是在Filter上清除 MDC 似乎效果很好。

( Here is concrete example.) 这里是具体的例子。)

There are two ways to define mechanism for generating token for requests.有两种方法可以定义为请求生成token机制。

The first way is to define a filter and wrap the DispatcherServlet like this:第一种方法是定义一个过滤器并像这样包装DispatcherServlet

import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

@Component
@WebFilter
public class RequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            MDC.put("token", UUID.randomUUID().toString());
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }

}

and change mapping url for DispatcherServlet in application.properties并在application.properties更改DispatcherServlet映射 url

server.servlet.context-path=/api
spring.mvc.servlet.path=/

That way is applicable if you can change url mapping for DispatcherServlet , and you should have default exception handler definition like this:如果您可以更改DispatcherServlet url 映射,那么这种方式是适用的,并且您应该具有这样的默认异常处理程序定义:

@ExceptionHandler({ Exception.class })
public void handleException(Exception e) {
   log.error("Error: ", e);
}

otherwise can be logs without token in console.否则可以是控制台中没有令牌的日志。 If the above conditions do not apply, use the second method.如果上述条件不适用,请使用第二种方法。

The second way is to use the Interceptor , the configuration is as follows:第二种方式是使用Interceptor ,配置如下:

public class MDCInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MDC.put("token", UUID.randomUUID().toString());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        MDC.clear();
    }
}

And add interceptor in configuration并在配置中添加拦截器

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MDCInterceptor());
    }
}

Above configuration has MDC.clear() in postHandle method, because after exception the afterCompletion method is executed immediately and will clear the MDC.上面的配置在postHandle方法中有MDC.clear() ,因为在异常之后afterCompletion方法会立即执行并将清除 MDC。 The second method covers all cases of adding a token to log messages.第二种方法涵盖了将令牌添加到日志消息的所有情况。

you can use ServletRequestListener instead,你可以使用 ServletRequestListener 代替,

for example:例如:

import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.util.UUID;

@Component
public class MyServletRequestListener implements ServletRequestListener {

    @Override
    public void requestInitialized(ServletRequestEvent requestEvent) {
        MDC.put("token", UUID.randomUUID().toString());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent requestEvent) {
        MDC.clear();
    }
}

A standard servlet filter is executed around any servlet, including Spring's DispatcherServlet (see, for example, here ) but your filter is a Spring component.标准servlet 过滤器围绕任何 servlet 执行,包括 Spring 的DispatcherServlet (例如,参见此处),但您的过滤器是一个 Spring 组件。 Since it doesn't use any Spring bean you can easily convert it to a plain filter, that is a filter configured in the web.xml as described in the page I linked.由于它不使用任何 Spring bean,您可以轻松地将其转换为普通过滤器,即在web.xml配置的过滤器,如我链接的页面中所述。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM