我正在用spring boot开发REST API。我需要记录所有的请求与输入参数(与方法,例如。GET, POST等),请求路径,查询字符串,此请求对应的类方法,以及此操作的响应,包括成功和错误。例如:

成功的要求:

http://example.com/api/users/1

Log应该是这样的:

{
   HttpStatus: 200,
   path: "api/users/1",
   method: "GET",
   clientIp: "0.0.0.0",
   accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
   method: "UsersController.getUser",
   arguments: {
     id: 1 
   },
   response: {
      user: {
        id: 1,
        username: "user123",
        email: "user123@example.com"   
      }
   },
   exceptions: []       
}

或请求错误:

http://example.com/api/users/9999

Log应该是这样的:

{
   HttpStatus: 404,
   errorCode: 101,                 
   path: "api/users/9999",
   method: "GET",
   clientIp: "0.0.0.0",
   accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
   method: "UsersController.getUser",
   arguments: {
     id: 9999 
   },
   returns: {            
   },
   exceptions: [
     {
       exception: "UserNotFoundException",
       message: "User with id 9999 not found",
       exceptionId: "adhaskldjaso98d7324kjh989",
       stacktrace: ...................    
   ]       
}

我希望Request/Response是一个单独的实体,在成功和错误的情况下都具有与该实体相关的自定义信息。

春季实现这一目标的最佳做法是什么,可能是使用过滤器吗?如果是,能否提供具体的例子?

我使用过@ControllerAdvice和@ExceptionHandler,但正如我提到的,我需要在一个地方(和单个日志)处理所有成功和错误请求。


当前回答

如果不需要记录已执行的java方法,则可以使用javax.servlet.Filter。

但是有了这个要求,您必须访问存储在DispatcherServlet的handlerMapping中的信息。也就是说,您可以重写DispatcherServlet来完成请求/响应对的日志记录。

下面是一个想法的例子,可以进一步加强和采用您的需要。

public class LoggableDispatcherServlet extends DispatcherServlet {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        HandlerExecutionChain handler = getHandler(request);

        try {
            super.doDispatch(request, response);
        } finally {
            log(request, response, handler);
            updateResponse(response);
        }
    }

    private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, HandlerExecutionChain handler) {
        LogMessage log = new LogMessage();
        log.setHttpStatus(responseToCache.getStatus());
        log.setHttpMethod(requestToCache.getMethod());
        log.setPath(requestToCache.getRequestURI());
        log.setClientIp(requestToCache.getRemoteAddr());
        log.setJavaMethod(handler.toString());
        log.setResponse(getResponsePayload(responseToCache));
        logger.info(log);
    }

    private String getResponsePayload(HttpServletResponse response) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {

            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                int length = Math.min(buf.length, 5120);
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                }
                catch (UnsupportedEncodingException ex) {
                    // NOOP
                }
            }
        }
        return "[unknown]";
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse();
    }

}

HandlerExecutionChain—包含关于请求处理程序的信息。

然后你可以像下面这样注册这个dispatcher:

    @Bean
    public ServletRegistrationBean dispatcherRegistration() {
        return new ServletRegistrationBean(dispatcherServlet());
    }

    @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet() {
        return new LoggableDispatcherServlet();
    }

下面是log的例子:

http http://localhost:8090/settings/test
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=500, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [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)] and 3 interceptors', arguments=null, response='{"timestamp":1472475814077,"status":500,"error":"Internal Server Error","exception":"java.lang.RuntimeException","message":"org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException","path":"/settings/test"}'}

http http://localhost:8090/settings/params
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=200, path='/settings/httpParams', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public x.y.z.DTO x.y.z.Controller.params()] and 3 interceptors', arguments=null, response='{}'}

http http://localhost:8090/123
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=404, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [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)] and 3 interceptors', arguments=null, response='{"timestamp":1472475840592,"status":404,"error":"Not Found","message":"Not Found","path":"/123"}'}

更新

如果出现错误,Spring会自动进行错误处理。因此,basicerrorcontroller# error被显示为请求处理程序。如果你想保留原始的请求处理程序,那么你可以在#processDispatchResult被调用之前在spring-webmvc- 4.5.release -sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971中覆盖这个行为来缓存原始的处理程序。

其他回答

如果你在你的引导应用程序中使用Tomcat,这里是org.apache.catalina.filters.RequestDumperFilter的类路径。(但它不会为你提供“单一位置的例外”)。

具体答案请参考以下链接 https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba

对上面提到的解决方案做了一些更改,如果记录器级别为info,请求和响应也将登录控制台和文件。我们可以在控制台或文件中打印。

@Component
public class LoggingFilter extends OncePerRequestFilter {

private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
        MediaType.valueOf("text/*"),
        MediaType.APPLICATION_FORM_URLENCODED,
        MediaType.APPLICATION_JSON,
        MediaType.APPLICATION_XML,
        MediaType.valueOf("application/*+json"),
        MediaType.valueOf("application/*+xml"),
        MediaType.MULTIPART_FORM_DATA
        );
Logger log = LoggerFactory.getLogger(ReqAndResLoggingFilter.class);
private static final Path path = Paths.get("/home/ramesh/loggerReq.txt");
private static BufferedWriter writer = null;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    try {
        writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"));
    if (isAsyncDispatch(request)) {
        filterChain.doFilter(request, response);
    } else {
        doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
    }
    }finally {
        writer.close();
    }
}

protected void doFilterWrapped(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain) throws ServletException, IOException {
    try {
        beforeRequest(request, response);
        filterChain.doFilter(request, response);
    }
    finally {
        afterRequest(request, response);
        response.copyBodyToResponse();
    }
}

protected void beforeRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
    if (log.isInfoEnabled()) {
        logRequestHeader(request, request.getRemoteAddr() + "|>");
    }
}

protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
    if (log.isInfoEnabled()) {
        logRequestBody(request, request.getRemoteAddr() + "|>");
        logResponse(response, request.getRemoteAddr() + "|<");
    }
}

private void logRequestHeader(ContentCachingRequestWrapper request, String prefix) throws IOException {
    String queryString = request.getQueryString();
    if (queryString == null) {
        printLines(prefix,request.getMethod(),request.getRequestURI());
        log.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
    } else {
        printLines(prefix,request.getMethod(),request.getRequestURI(),queryString);
        log.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
    }
    Collections.list(request.getHeaderNames()).forEach(headerName ->
    Collections.list(request.getHeaders(headerName)).forEach(headerValue ->
    log.info("{} {}: {}", prefix, headerName, headerValue)));
    printLines(prefix);
    printLines(RequestContextHolder.currentRequestAttributes().getSessionId());
    log.info("{}", prefix);

    log.info(" Session ID: ", RequestContextHolder.currentRequestAttributes().getSessionId());
}

private void printLines(String ...args) throws IOException {

    try {
    for(String varArgs:args) {
            writer.write(varArgs);
            writer.newLine();
    }
        }catch(IOException ex){
            ex.printStackTrace();
    }

}

private void logRequestBody(ContentCachingRequestWrapper request, String prefix) {
    byte[] content = request.getContentAsByteArray();
    if (content.length > 0) {
        logContent(content, request.getContentType(), request.getCharacterEncoding(), prefix);
    }
}

private void logResponse(ContentCachingResponseWrapper response, String prefix) throws IOException {
    int status = response.getStatus();
    printLines(prefix, String.valueOf(status), HttpStatus.valueOf(status).getReasonPhrase());
    log.info("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
    response.getHeaderNames().forEach(headerName ->
    response.getHeaders(headerName).forEach(headerValue ->
    log.info("{} {}: {}", prefix, headerName, headerValue)));
    printLines(prefix);
    log.info("{}", prefix);
    byte[] content = response.getContentAsByteArray();
    if (content.length > 0) {
        logContent(content, response.getContentType(), response.getCharacterEncoding(), prefix);
    }
}

private void logContent(byte[] content, String contentType, String contentEncoding, String prefix) {
    MediaType mediaType = MediaType.valueOf(contentType);
    boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
    if (visible) {
        try {
            String contentString = new String(content, contentEncoding);
            Stream.of(contentString.split("\r\n|\r|\n")).forEach(line -> {
                try {
                    printLines(line);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
//              log.info("{} {}", prefix, line));
        } catch (UnsupportedEncodingException e) {
            log.info("{} [{} bytes content]", prefix, content.length);
        }
    } else {

        log.info("{} [{} bytes content]", prefix, content.length);
    }
}

private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
    if (request instanceof ContentCachingRequestWrapper) {
        return (ContentCachingRequestWrapper) request;
    } else {
        return new ContentCachingRequestWrapper(request);
    }
}

private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) {
    if (response instanceof ContentCachingResponseWrapper) {
        return (ContentCachingResponseWrapper) response;
    } else {
        return new ContentCachingResponseWrapper(response);
    }
}
} 

文件输出:

127.0.0.1|>
POST
/createUser
127.0.0.1|>
session Id:C0793464532E7F0C7154913CBA018B2B
Request:
{
  "name": "asdasdas",
  "birthDate": "2018-06-21T17:11:15.679+0000"
}
127.0.0.1|<
200
OK
127.0.0.1|<
Response:
{"name":"asdasdas","birthDate":"2018-06-21T17:11:15.679+0000","id":4}

这里是我的解决方案(Spring 2.0.x)

添加maven依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

编辑应用程序。属性,并添加以下行:

management.endpoints.web.exposure.include=* 

一旦你的spring boot应用程序启动,你可以通过调用这个url来跟踪最新的100个http请求: http://localhost:8070/actuator/httptrace

不要编写任何拦截器、过滤器、组件、方面等,这是一个非常常见的问题,并且已经解决了很多次。

Spring Boot有一个名为Actuator的模块,它提供了开箱即用的HTTP请求日志记录。有一个端点映射到/trace (SB1.x)或/actuator/httptrace (SB2.0+),它将显示最近100个HTTP请求。您可以自定义它以记录每个请求,或将其写入DB。

要获得您想要的端点,您需要spring-boot-starter-actuator依赖项,还需要将您正在寻找的端点“白名单”,并可能为其设置或禁用安全性。

另外,这个应用程序将在哪里运行?您将使用PaaS吗?托管提供商,例如Heroku,提供请求日志记录作为他们服务的一部分,你不需要做任何编码。

如果不需要记录已执行的java方法,则可以使用javax.servlet.Filter。

但是有了这个要求,您必须访问存储在DispatcherServlet的handlerMapping中的信息。也就是说,您可以重写DispatcherServlet来完成请求/响应对的日志记录。

下面是一个想法的例子,可以进一步加强和采用您的需要。

public class LoggableDispatcherServlet extends DispatcherServlet {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        HandlerExecutionChain handler = getHandler(request);

        try {
            super.doDispatch(request, response);
        } finally {
            log(request, response, handler);
            updateResponse(response);
        }
    }

    private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, HandlerExecutionChain handler) {
        LogMessage log = new LogMessage();
        log.setHttpStatus(responseToCache.getStatus());
        log.setHttpMethod(requestToCache.getMethod());
        log.setPath(requestToCache.getRequestURI());
        log.setClientIp(requestToCache.getRemoteAddr());
        log.setJavaMethod(handler.toString());
        log.setResponse(getResponsePayload(responseToCache));
        logger.info(log);
    }

    private String getResponsePayload(HttpServletResponse response) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {

            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                int length = Math.min(buf.length, 5120);
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                }
                catch (UnsupportedEncodingException ex) {
                    // NOOP
                }
            }
        }
        return "[unknown]";
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse();
    }

}

HandlerExecutionChain—包含关于请求处理程序的信息。

然后你可以像下面这样注册这个dispatcher:

    @Bean
    public ServletRegistrationBean dispatcherRegistration() {
        return new ServletRegistrationBean(dispatcherServlet());
    }

    @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet() {
        return new LoggableDispatcherServlet();
    }

下面是log的例子:

http http://localhost:8090/settings/test
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=500, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [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)] and 3 interceptors', arguments=null, response='{"timestamp":1472475814077,"status":500,"error":"Internal Server Error","exception":"java.lang.RuntimeException","message":"org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException","path":"/settings/test"}'}

http http://localhost:8090/settings/params
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=200, path='/settings/httpParams', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public x.y.z.DTO x.y.z.Controller.params()] and 3 interceptors', arguments=null, response='{}'}

http http://localhost:8090/123
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=404, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [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)] and 3 interceptors', arguments=null, response='{"timestamp":1472475840592,"status":404,"error":"Not Found","message":"Not Found","path":"/123"}'}

更新

如果出现错误,Spring会自动进行错误处理。因此,basicerrorcontroller# error被显示为请求处理程序。如果你想保留原始的请求处理程序,那么你可以在#processDispatchResult被调用之前在spring-webmvc- 4.5.release -sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971中覆盖这个行为来缓存原始的处理程序。