Spring 启动 - 每 4xx 和 5xx 记录一次,包括请求

Spring boot - Log every 4xx and 5xx including the request

我正在尝试每 4xx 和 5xx 记录一次(slf4j 的警告和错误)以及来自客户端的请求,包括 headers 和有效负载。

我还想记录我的应用程序响应的响应,无论它是由 Spring 本身生成的异常消息,还是我从控制器返回的自定义消息。

这些是我用于测试的控制器:

@RequestMapping(path = "/throw", method = RequestMethod.GET)
public String Fail(){
    String nul = null;
    nul.toCharArray();
    return "Hello World";
}

@RequestMapping(path = "/null", method = RequestMethod.GET)
public ResponseEntity Custom() {
    return ResponseEntity.notFound().build();
}

我试过以下方法:

ControllerAdvice
发现这只是为了处理异常。我需要处理从我的控制器返回的任何 4xx 和 5xx 响应。

使用过滤器
通过使用 CommonsRequestLoggingFilter,我可以记录请求,包括负载。但是,当抛出异常(由 Spring 处理)时,这不会记录。

使用拦截器
使用拦截器,我应该能够使用以下代码拦截传入和传出数据:

private static final Logger log = LoggerFactory.getLogger(RequestInterceptor.class);

class RequestLog {

    public String requestMethod;
    public String requestUri;
    public String requestPayload;
    public String handlerName;
    public String requestParams;

    RequestLog(String requestMethod, String requestUri, String requestPayload, String handlerName, Enumeration<String> requestParams) {
        this.requestMethod = requestMethod;
        this.requestUri = requestUri;
        this.requestPayload = requestPayload;
        this.handlerName = handlerName;

        StringBuilder stringBuilder = new StringBuilder();

        while (requestParams.hasMoreElements()) {
            stringBuilder
                    .append(";")
                    .append(requestParams.nextElement());
        }

        this.requestParams = stringBuilder.toString();
    }
}

class ResponseLog {
    public int responseStatus;
    public String responsePayload;

    public ResponseLog(int responseStatus, String responsePayload) {
        this.responseStatus = responseStatus;
        this.responsePayload = responsePayload;
    }
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String requestUri = request.getRequestURI();

    String requestPayload = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
    Enumeration<String> requestParams = request.getParameterNames();
    String requestMethod = request.getMethod();
    String handlerName = handler.toString();

    RequestLog requestLog = new RequestLog(requestMethod, requestUri, requestPayload, handlerName, requestParams);
    String serialized = new ObjectMapper().writeValueAsString(requestLog);

    log.info("Incoming request:" + serialized);

    return super.preHandle(request, response, handler);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws IOException {
    int responseStatus = response.getStatus();

    boolean is4xx = String.valueOf(responseStatus).startsWith("4");
    boolean is5xx = String.valueOf(responseStatus).startsWith("5");

    if (is4xx || is5xx || ex != null) {
        String responseBody = getResponseBody(response);
        ResponseLog responseLog = new ResponseLog(responseStatus, responseBody);

        String serialized = new ObjectMapper().writeValueAsString(responseLog);
        log.warn("Response to last request:" + serialized);
    }
}

private String getResponseBody(HttpServletResponse response) throws UnsupportedEncodingException {
    String responsePayload = "";
    ContentCachingResponseWrapper wrappedRequest = new ContentCachingResponseWrapper(response);

    byte[] responseBuffer = wrappedRequest.getContentAsByteArray();

    if (responseBuffer.length > 0) {
            responsePayload = new String(responseBuffer, 0, responseBuffer.length, wrappedRequest.getCharacterEncoding());
    }

    return responsePayload;
}

当请求 /throw 时,我从拦截器得到以下日志:

2017-12-11 21:40:15.619  INFO 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor  : Incoming request:{"requestMethod":"GET","requestUri":"/throw","requestPayload":"","handlerName":"public java.lang.String com.example.demo.controllers.IndexController.Fail()","requestParams":""}
2017-12-11 21:40:15.635  WARN 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor  : Response to last request:{"responseStatus":200,"responsePayload":""}

*stackTrace because of nullpointer...*

2017-12-11 21:40:15.654  INFO 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor  : Incoming request:{"requestMethod":"GET","requestUri":"/error","requestPayload":"","handlerName":"public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)","requestParams":""}
2017-12-11 21:40:15.675  WARN 12220 --- [nio-8080-exec-1] c.e.demo.interceptor.RequestInterceptor  : Response to last request:{"responseStatus":500,"responsePayload":""}

请求 /null:

2017-12-11 21:48:14.815  INFO 12220 --- [nio-8080-exec-3] c.e.demo.interceptor.RequestInterceptor  : Incoming request:{"requestMethod":"GET","requestUri":"/null","requestPayload":"","handlerName":"public org.springframework.http.ResponseEntity com.example.demo.controllers.IndexController.Custom()","requestParams":""}
2017-12-11 21:48:14.817  WARN 12220 --- [nio-8080-exec-3] c.e.demo.interceptor.RequestInterceptor  : Response to last request:{"responseStatus":404,"responsePayload":""}

这里有两个问题:

TL;DR:我需要将请求记录到我的 Spring 应用程序并将响应(包括有效负载)记录到客户端。我怎么解决这个问题?

同时使用 Filter 和 ControllerAdvice 的可能解决方案:

过滤器:

@Component
public class LogFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(LogFilter.class);

    private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 1000;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        logRequest(request);
        filterChain.doFilter(requestWrapper, responseWrapper);
        logResponse(responseWrapper);
    }

    private void logResponse(ContentCachingResponseWrapper responseWrapper) {
            String body = "None";
            byte[] buf = responseWrapper.getContentAsByteArray();

            if (buf.length > 0) {
                int length = Math.min(buf.length, DEFAULT_MAX_PAYLOAD_LENGTH);
                try {
                    body = new String(buf, 0, length, responseWrapper.getCharacterEncoding());
                    responseWrapper.copyBodyToResponse();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        int responseStatus = responseWrapper.getStatusCode();

        boolean is4xx = String.valueOf(responseStatus).startsWith("4");
        boolean is5xx = String.valueOf(responseStatus).startsWith("5");

        if(is4xx) logger.warn("Response: statusCode: {}, body: {}", responseStatus, body);
        else if (is5xx) logger.error("Response: statusCode: {}, body: {}", responseStatus, body);
    }

    private void logRequest(HttpServletRequest request) {
        String body = "None";
        try {
            body = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        logger.warn("Incoming request {}: {}", request.getRequestURI() , body);
    }

}

控制器建议:

@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        CustomException customException = new CustomException(NOT_FOUND, ex.getMessage(), ex.getLocalizedMessage(), ex);
        ex.printStackTrace();
        return new ResponseEntity<>(customException, customException.getStatus());
    }


    @ResponseBody
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<Object> handleSpringExceptions(HttpServletRequest request, Exception ex) {
        CustomException customException = new CustomException(INTERNAL_SERVER_ERROR, ex.getMessage(), ex.getLocalizedMessage(), ex);
        ex.printStackTrace();
        return new ResponseEntity<>(customException, customException.getStatus());

    }


    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        CustomException customException = new CustomException(INTERNAL_SERVER_ERROR, ex.getMessage(),ex.getLocalizedMessage(), ex);
        ex.printStackTrace();
        return new ResponseEntity<>(customException, customException.getStatus());
    }
}

过滤器可以记录我们在控制器内部处理的任何请求和响应,但是当抛出异常时响应负载似乎总是空的(因为 Spring 处理它并创建自定义消息) .我不确定这是如何工作的,但我通过另外使用 ControllerAdvice 设法克服了这个问题(响应通过过滤器传递......)。现在我可以正确记录任何 4xx 和 5xx。如果有人有更好的解决方案,我会接受。

注意:CustomException 只是一个 class,其中包含我要发送给客户端的字段。

public class CustomException{

    public String timestamp;
    public HttpStatus status;
    public String exceptionMessage;
    public String exceptionType;
    public String messageEn;
    public String messageNo;

    ...
}