Spring boot - 每 4xx 和 5xx 记录一次,包括请求
Posted
技术标签:
【中文标题】Spring boot - 每 4xx 和 5xx 记录一次,包括请求【英文标题】:Spring boot - Log every 4xx and 5xx including the request 【发布时间】:2018-05-25 11:26:59 【问题描述】:我正在尝试每 4xx 和 5xx 记录一次(使用 slf4j 的警告和错误)以及来自客户端的请求,包括标头和有效负载。
我还想记录我的应用程序响应的响应,无论它是 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();
我尝试了以下方法:
控制器建议 发现这仅用于处理异常。我需要处理从我的控制器返回的任何 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":""
这里有两个问题:
响应正文始终为空(即使客户端收到来自 Spring 的错误响应)。我该如何解决这个问题?
好像Spring在异常发生时重定向到/error
TL;DR:我需要将请求记录到我的 Spring 应用程序并将响应(包括有效负载)记录到客户端。我怎么解决这个问题?
【问题讨论】:
您的日志代码扩展/实现是什么类/接口?粘贴时好像遗漏了它。 它正在扩展 HandlerInterceptorAdapter。 我测试了CommonsRequestLoggingFilter
。即使 API 抛出异常,它仍然会在请求之前和请求之后记录。
【参考方案1】:
同时使用 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 只是一个包含我想发送给客户端的字段的类。
public class CustomException
public String timestamp;
public HttpStatus status;
public String exceptionMessage;
public String exceptionType;
public String messageEn;
public String messageNo;
...
【讨论】:
以上是关于Spring boot - 每 4xx 和 5xx 记录一次,包括请求的主要内容,如果未能解决你的问题,请参考以下文章
存在 4xx 或 5xx Http 错误代码时在 Javascript 中解析 JSONP 响应