我正在用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,但正如我提到的,我需要在一个地方(和单个日志)处理所有成功和错误请求。


当前回答

如果您只看到请求有效负载的一部分,则需要调用setMaxPayloadLength函数,因为它默认只在请求正文中显示50个字符。另外,如果你不想记录你的认证头,设置setIncludeHeaders为false也是一个好主意!

@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
    CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
    loggingFilter.setIncludeClientInfo(false);
    loggingFilter.setIncludeQueryString(false);
    loggingFilter.setIncludePayload(true);
    loggingFilter.setIncludeHeaders(false);
    loggingFilter.setMaxPayloadLength(500);
    return loggingFilter;
}

其他回答

这段代码适用于Spring Boot应用程序-只需将其注册为过滤器

    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.util.Collection;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Locale;
    import java.util.Map;
    import javax.servlet.*;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import javax.servlet.http.HttpServletResponse;
    import org.apache.commons.io.output.TeeOutputStream;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;

    @Component
    public class HttpLoggingFilter implements Filter {

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

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain chain) throws IOException, ServletException {
            try {
                HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;

                Map<String, String> requestMap = this
                        .getTypesafeRequestMap(httpServletRequest);
                BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
                        httpServletRequest);
                BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
                        httpServletResponse);

                final StringBuilder logMessage = new StringBuilder(
                        "REST Request - ").append("[HTTP METHOD:")
                        .append(httpServletRequest.getMethod())
                        .append("] [PATH INFO:")
                        .append(httpServletRequest.getServletPath())
                        .append("] [REQUEST PARAMETERS:").append(requestMap)
                        .append("] [REQUEST BODY:")
                        .append(bufferedRequest.getRequestBody())
                        .append("] [REMOTE ADDRESS:")
                        .append(httpServletRequest.getRemoteAddr()).append("]");

                chain.doFilter(bufferedRequest, bufferedResponse);
                logMessage.append(" [RESPONSE:")
                        .append(bufferedResponse.getContent()).append("]");
                log.debug(logMessage.toString());
            } catch (Throwable a) {
                log.error(a.getMessage());
            }
        }

        private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
            Map<String, String> typesafeRequestMap = new HashMap<String, String>();
            Enumeration<?> requestParamNames = request.getParameterNames();
            while (requestParamNames.hasMoreElements()) {
                String requestParamName = (String) requestParamNames.nextElement();
                String requestParamValue;
                if (requestParamName.equalsIgnoreCase("password")) {
                    requestParamValue = "********";
                } else {
                    requestParamValue = request.getParameter(requestParamName);
                }
                typesafeRequestMap.put(requestParamName, requestParamValue);
            }
            return typesafeRequestMap;
        }

        @Override
        public void destroy() {
        }

        private static final class BufferedRequestWrapper extends
                HttpServletRequestWrapper {

            private ByteArrayInputStream bais = null;
            private ByteArrayOutputStream baos = null;
            private BufferedServletInputStream bsis = null;
            private byte[] buffer = null;

            public BufferedRequestWrapper(HttpServletRequest req)
                    throws IOException {
                super(req);
                // Read InputStream and store its content in a buffer.
                InputStream is = req.getInputStream();
                this.baos = new ByteArrayOutputStream();
                byte buf[] = new byte[1024];
                int read;
                while ((read = is.read(buf)) > 0) {
                    this.baos.write(buf, 0, read);
                }
                this.buffer = this.baos.toByteArray();
            }

            @Override
            public ServletInputStream getInputStream() {
                this.bais = new ByteArrayInputStream(this.buffer);
                this.bsis = new BufferedServletInputStream(this.bais);
                return this.bsis;
            }

            String getRequestBody() throws IOException {
                BufferedReader reader = new BufferedReader(new InputStreamReader(
                        this.getInputStream()));
                String line = null;
                StringBuilder inputBuffer = new StringBuilder();
                do {
                    line = reader.readLine();
                    if (null != line) {
                        inputBuffer.append(line.trim());
                    }
                } while (line != null);
                reader.close();
                return inputBuffer.toString().trim();
            }

        }

        private static final class BufferedServletInputStream extends
                ServletInputStream {

            private ByteArrayInputStream bais;

            public BufferedServletInputStream(ByteArrayInputStream bais) {
                this.bais = bais;
            }

            @Override
            public int available() {
                return this.bais.available();
            }

            @Override
            public int read() {
                return this.bais.read();
            }

            @Override
            public int read(byte[] buf, int off, int len) {
                return this.bais.read(buf, off, len);
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        }

        public class TeeServletOutputStream extends ServletOutputStream {

            private final TeeOutputStream targetStream;

            public TeeServletOutputStream(OutputStream one, OutputStream two) {
                targetStream = new TeeOutputStream(one, two);
            }

            @Override
            public void write(int arg0) throws IOException {
                this.targetStream.write(arg0);
            }

            public void flush() throws IOException {
                super.flush();
                this.targetStream.flush();
            }

            public void close() throws IOException {
                super.close();
                this.targetStream.close();
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setWriteListener(WriteListener writeListener) {

            }
        }

        public class BufferedResponseWrapper implements HttpServletResponse {

            HttpServletResponse original;
            TeeServletOutputStream tee;
            ByteArrayOutputStream bos;

            public BufferedResponseWrapper(HttpServletResponse response) {
                original = response;
            }

            public String getContent() {
                return bos.toString();
            }

            public PrintWriter getWriter() throws IOException {
                return original.getWriter();
            }

            public ServletOutputStream getOutputStream() throws IOException {
                if (tee == null) {
                    bos = new ByteArrayOutputStream();
                    tee = new TeeServletOutputStream(original.getOutputStream(),
                            bos);
                }
                return tee;

            }

            @Override
            public String getCharacterEncoding() {
                return original.getCharacterEncoding();
            }

            @Override
            public String getContentType() {
                return original.getContentType();
            }

            @Override
            public void setCharacterEncoding(String charset) {
                original.setCharacterEncoding(charset);
            }

            @Override
            public void setContentLength(int len) {
                original.setContentLength(len);
            }

            @Override
            public void setContentLengthLong(long l) {
                original.setContentLengthLong(l);
            }

            @Override
            public void setContentType(String type) {
                original.setContentType(type);
            }

            @Override
            public void setBufferSize(int size) {
                original.setBufferSize(size);
            }

            @Override
            public int getBufferSize() {
                return original.getBufferSize();
            }

            @Override
            public void flushBuffer() throws IOException {
                tee.flush();
            }

            @Override
            public void resetBuffer() {
                original.resetBuffer();
            }

            @Override
            public boolean isCommitted() {
                return original.isCommitted();
            }

            @Override
            public void reset() {
                original.reset();
            }

            @Override
            public void setLocale(Locale loc) {
                original.setLocale(loc);
            }

            @Override
            public Locale getLocale() {
                return original.getLocale();
            }

            @Override
            public void addCookie(Cookie cookie) {
                original.addCookie(cookie);
            }

            @Override
            public boolean containsHeader(String name) {
                return original.containsHeader(name);
            }

            @Override
            public String encodeURL(String url) {
                return original.encodeURL(url);
            }

            @Override
            public String encodeRedirectURL(String url) {
                return original.encodeRedirectURL(url);
            }

            @SuppressWarnings("deprecation")
            @Override
            public String encodeUrl(String url) {
                return original.encodeUrl(url);
            }

            @SuppressWarnings("deprecation")
            @Override
            public String encodeRedirectUrl(String url) {
                return original.encodeRedirectUrl(url);
            }

            @Override
            public void sendError(int sc, String msg) throws IOException {
                original.sendError(sc, msg);
            }

            @Override
            public void sendError(int sc) throws IOException {
                original.sendError(sc);
            }

            @Override
            public void sendRedirect(String location) throws IOException {
                original.sendRedirect(location);
            }

            @Override
            public void setDateHeader(String name, long date) {
                original.setDateHeader(name, date);
            }

            @Override
            public void addDateHeader(String name, long date) {
                original.addDateHeader(name, date);
            }

            @Override
            public void setHeader(String name, String value) {
                original.setHeader(name, value);
            }

            @Override
            public void addHeader(String name, String value) {
                original.addHeader(name, value);
            }

            @Override
            public void setIntHeader(String name, int value) {
                original.setIntHeader(name, value);
            }

            @Override
            public void addIntHeader(String name, int value) {
                original.addIntHeader(name, value);
            }

            @Override
            public void setStatus(int sc) {
                original.setStatus(sc);
            }

            @SuppressWarnings("deprecation")
            @Override
            public void setStatus(int sc, String sm) {
                original.setStatus(sc, sm);
            }

            @Override
            public String getHeader(String arg0) {
                return original.getHeader(arg0);
            }

            @Override
            public Collection<String> getHeaderNames() {
                return original.getHeaderNames();
            }

            @Override
            public Collection<String> getHeaders(String arg0) {
                return original.getHeaders(arg0);
            }

            @Override
            public int getStatus() {
                return original.getStatus();
            }

        }
    }

你也可以配置一个自定义的Spring拦截器HandlerInterceptorAdapter来简化前置/后置拦截器的实现:

@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle (final HttpServletRequest request, final HttpServletResponse response,
            final Object handler)
            throws Exception {

        // Logs here

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

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final Exception ex) {
        // Logs here
    }
}

然后,你可以注册尽可能多的拦截器:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    CustomHttpInterceptor customHttpInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(customHttpInterceptor).addPathPatterns("/endpoints");
    }

}

注意:就像@Robert说的,你需要注意你的应用程序正在使用的HttpServletRequest和HttpServletResponse的具体实现。

例如,对于使用shaallowetagheaderfilter的应用程序,响应实现将是一个ContentCachingResponseWrapper,所以你会有:

@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomHttpInterceptor.class);

    private static final int MAX_PAYLOAD_LENGTH = 1000;

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final Exception ex) {
        final byte[] contentAsByteArray = ((ContentCachingResponseWrapper) response).getContentAsByteArray();

        LOGGER.info("Request body:\n" + getContentAsString(contentAsByteArray, response.getCharacterEncoding()));
    }

    private String getContentAsString(byte[] buf, String charsetName) {
        if (buf == null || buf.length == 0) {
            return "";
        }

        try {
            int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);

            return new String(buf, 0, length, charsetName);
        } catch (UnsupportedEncodingException ex) {
            return "Unsupported Encoding";
        }
    }

}

如果不需要记录已执行的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中覆盖这个行为来缓存原始的处理程序。

@hahn的回答需要一些修改才能为我工作,但这是迄今为止我能得到的最可定制的东西。

它对我不起作用,可能是因为我也有一个HandlerInterceptorAdapter[?? ?但是我一直从那个版本的服务器得到不好的响应。这是我对它的修改。

public class LoggableDispatcherServlet extends DispatcherServlet {

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

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

        long startTime = System.currentTimeMillis();
        try {
            super.doDispatch(request, response);
        } finally {
            log(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response),
                    System.currentTimeMillis() - startTime);
        }
    }

    private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, long timeTaken) {
        int status = responseToCache.getStatus();
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("httpStatus", status);
        jsonObject.addProperty("path", requestToCache.getRequestURI());
        jsonObject.addProperty("httpMethod", requestToCache.getMethod());
        jsonObject.addProperty("timeTakenMs", timeTaken);
        jsonObject.addProperty("clientIP", requestToCache.getRemoteAddr());
        if (status > 299) {
            String requestBody = null;
            try {
                requestBody = requestToCache.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
            } catch (IOException e) {
                e.printStackTrace();
            }
            jsonObject.addProperty("requestBody", requestBody);
            jsonObject.addProperty("requestParams", requestToCache.getQueryString());
            jsonObject.addProperty("tokenExpiringHeader",
                    responseToCache.getHeader(ResponseHeaderModifierInterceptor.HEADER_TOKEN_EXPIRING));
        }
        logger.info(jsonObject);
    }
}

如果您不介意尝试Spring AOP,这是我一直在探索的日志目的,它对我来说工作得很好。它不会记录未定义的请求和失败的请求尝试。

添加这三个依赖项

spring-aop, aspectjrt, aspectjweaver

将此添加到xml配置文件<aop:aspectj-autoproxy/>

创建一个可以用作切入点的注释

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface EnableLogging {
ActionType actionType();
}

现在注释你想要记录的所有API方法

@EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
@Override
public Response getEmployees(RequestDto req, final String param) {
...
}

现在来看方面。组件—扫描这个类所在的包。

@Aspect
@Component
public class Aspects {

@AfterReturning(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", returning = "result")
public void auditInfo(JoinPoint joinPoint, Object result, EnableLogging enableLogging, Object reqArg, String reqArg1) {

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
            .getRequest();

    if (result instanceof Response) {
        Response responseObj = (Response) result;

    String requestUrl = request.getScheme() + "://" + request.getServerName()
                + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
                + "?" + request.getQueryString();

String clientIp = request.getRemoteAddr();
String clientRequest = reqArg.toString();
int httpResponseStatus = responseObj.getStatus();
responseObj.getEntity();
// Can log whatever stuff from here in a single spot.
}


@AfterThrowing(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", throwing="exception")
public void auditExceptionInfo(JoinPoint joinPoint, Throwable exception, EnableLogging enableLogging, Object reqArg, String reqArg1) {

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
            .getRequest();

    String requestUrl = request.getScheme() + "://" + request.getServerName()
    + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
    + "?" + request.getQueryString();

    exception.getMessage();
    exception.getCause();
    exception.printStackTrace();
    exception.getLocalizedMessage();
    // Can log whatever exceptions, requests, etc from here in a single spot.
    }
}

@AfterReturning建议在匹配的方法执行返回时运行 正常。 @ afterthrows通知在匹配的方法执行由退出时运行 抛出异常。

如果你想详细阅读,请通读这个。 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html