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

12

简短版本(含足够细节)

如何保留在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
    }
}

如果在另一个过滤器或控制器(对chain.doFilter(...)的调用)中发生异常,那么在异常处理期间将清除MDC,并且异常将从过滤器中抛出。当前,如果出现异常:finally块将被执行以清除MDC,然后异常将被抛出筛选器之外。在异常处理期间的所有日志都不会包含MDC属性。

详细版本

我有一个简单的Filter实现来拦截所有请求。它只创建一串随机字符(令牌),以便包含在与处理请求相关的所有日志中。

@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() {
    }
}

事件的顺序将会是:

  1. 请求被接收。
  2. 我的 doFilter() 被调用,将随机令牌添加到 MDC 中。
  3. 通过调用 chain.doFilter() 来处理请求。
  4. 无论发生什么 (处理完成、出现错误),MDC 中的随机令牌都会在 finally 块中被清除。

问题在于,如果发生了错误并且被处理(例如由自定义 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
finally 块在由处理它的 Controller 抛出异常时执行,导致清除 MDC。
此后执行错误处理(包括自定义的 ErrorController),这意味着相关日志中没有更多的令牌。 我该如何将自定义标记添加到与请求的整个处理过程(包括错误处理)相关的所有日志中,从接收请求到发送响应为止? 我想要在线程发送响应之后清除 MDC,作为最后一步操作。无论发生什么情况(成功响应、在错误处理期间抛出异常等),MDC 都应该被清除。
使用 Rest 服务的多个客户端同时访问,日志可能会变得非常混乱。将唯一标识符附加到某个请求的整个处理过程中产生的每个日志记录中将极大地简化调试。

你的代码看起来没问题。根据你的过滤器配置,在所有操作完成后,你的MDC会被清除。 所有的调度程序处理都发生在你的“finally”块执行之前。因此,逻辑上你的trace-id应该在所有日志中打印出来。 请在“finally”块中添加一个日志行,以了解MDC的清除顺序。如果我没错的话,你的日志行将在最后打印,指示在你的代码的其他地方也清除了MDC。 - Mukul Bansal
@MukulBansal 我今天稍后会研究一下并更新问题。我现在相信,通过向Spring Security添加自定义过滤器并在配置中指定显式顺序,可以实现上述目标。然后,MDC由一个更接近执行链开始的过滤器设置和清除,可能还包装了异常处理。但我不确定,也许效果完全相同,异常处理总是在其他过滤器退出后最后进行。 - Snackoverflow
查看FilterChainProxy的源代码,你的过滤器可能已经被添加到了安全过滤器链(一个虚拟链)中。这可能解释了日志输出。 - Ortwin Angermeier
4个回答

2

0

你可以使用ServletRequestListener,

例如:

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();
    }
}

0

定义生成请求token机制有两种方法。

第一种方法是定义一个过滤器并像这样包装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();
        }
    }

}

并在 application.properties 中更改 DispatcherServlet 的映射 URL

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

如果您可以更改DispatcherServlet的URL映射,并且应该具有以下默认异常处理程序定义,则可以使用该方法:

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

否则,控制台中可能会出现没有令牌的日志。

如果上述条件不适用,则使用第二种方法。

第二种方法是使用拦截器,配置如下:

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();
    }
}

在配置中添加拦截器

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

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

上述配置在postHandle方法中使用了MDC.clear(),因为在异常之后,afterCompletion方法会立即执行并清除MDC。第二种方法涵盖了所有添加日志消息令牌的情况。

-1
一个标准的servlet过滤器会在任何servlet周围执行,包括Spring的DispatcherServlet(例如,请参见here),但您的过滤器是一个Spring组件。由于它不使用任何Spring bean,因此您可以轻松地将其转换为普通过滤器,即在web.xml中配置的过滤器,如我链接的页面所述。

一个 HandlerInterceptor 是否更简单或者可能性更大?在 preHandle() 中添加 MDC 属性,在 afterCompletion() 中移除它? - Snackoverflow
很抱歉,任何Spring MVC组件都是由DispatcherServlet管理(即包装),但您的用例需要包装servlet的东西,也就是标准过滤器。 - Pino
@anddero:有任何反馈吗?你尝试过标准的过滤解决方案了吗? - Pino
@Pino,你能提供一个“标准过滤器”示例或文档的更新链接吗?你在回答中提供的链接对我来说似乎无法使用。我也试图完成OP所做的事情,但我不确定我是否正在实现正确类型的过滤器。 - Dave C
1
@DaveC,正如我之前所说:“恐怕任何Spring MVC组件都是由DispatcherServlet管理(即包装)的,但您的用例需要一些包装servlet的东西,也就是标准过滤器”。OP没有再对这个建议给出反馈,所以您可以尝试一下。 - Pino
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接