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


当前回答

为了只记录结果为400的请求:

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.AbstractRequestLoggingFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;

/**
 * Implementation is partially copied from {@link AbstractRequestLoggingFilter} and modified to output request information only if request resulted in 400.
 * Unfortunately {@link AbstractRequestLoggingFilter} is not smart enough to expose {@link HttpServletResponse} value in afterRequest() method.
 */
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {

    public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";

    public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";

    private final boolean includeQueryString = true;
    private final boolean includeClientInfo = true;
    private final boolean includeHeaders = true;
    private final boolean includePayload = true;

    private final int maxPayloadLength = (int) (2 * FileUtils.ONE_MB);

    private final String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;

    private final String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;

    /**
     * The default value is "false" so that the filter may log a "before" message
     * at the start of request processing and an "after" message at the end from
     * when the last asynchronously dispatched thread is exiting.
     */
    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException {

        final boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestToUse = request;

        if (includePayload && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestToUse = new ContentCachingRequestWrapper(request, maxPayloadLength);
        }

        final boolean shouldLog = shouldLog(requestToUse);

        try {
            filterChain.doFilter(requestToUse, response);
        } finally {
            if (shouldLog && !isAsyncStarted(requestToUse)) {
                afterRequest(requestToUse, response, getAfterMessage(requestToUse));
            }
        }
    }

    private String getAfterMessage(final HttpServletRequest request) {
        return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
    }

    private String createMessage(final HttpServletRequest request, final String prefix, final String suffix) {
        final StringBuilder msg = new StringBuilder();
        msg.append(prefix);
        msg.append("uri=").append(request.getRequestURI());

        if (includeQueryString) {
            final String queryString = request.getQueryString();
            if (queryString != null) {
                msg.append('?').append(queryString);
            }
        }

        if (includeClientInfo) {
            final String client = request.getRemoteAddr();
            if (StringUtils.hasLength(client)) {
                msg.append(";client=").append(client);
            }
            final HttpSession session = request.getSession(false);
            if (session != null) {
                msg.append(";session=").append(session.getId());
            }
            final String user = request.getRemoteUser();
            if (user != null) {
                msg.append(";user=").append(user);
            }
        }

        if (includeHeaders) {
            msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
        }

        if (includeHeaders) {
            final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
            if (wrapper != null) {
                final byte[] buf = wrapper.getContentAsByteArray();
                if (buf.length > 0) {
                    final int length = Math.min(buf.length, maxPayloadLength);
                    String payload;
                    try {
                        payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
                    } catch (final UnsupportedEncodingException ex) {
                        payload = "[unknown]";
                    }
                    msg.append(";payload=").append(payload);
                }
            }
        }
        msg.append(suffix);
        return msg.toString();
    }

    private boolean shouldLog(final HttpServletRequest request) {
        return true;
    }

    private void afterRequest(final HttpServletRequest request, final HttpServletResponse response, final String message) {
        if (response.getStatus() == HttpStatus.BAD_REQUEST.value()) {
            logger.warn(message);
        }
    }

}

其他回答

日志库是专门为记录HTTP请求和响应而设计的。它使用一个特殊的启动器库支持Spring Boot。

要在Spring Boot中启用日志,您所需要做的就是将库添加到项目的依赖项中。例如,假设您正在使用Maven:

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>logbook-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

默认情况下,日志输出如下所示:

{
  "origin" : "local",
  "correlation" : "52e19498-890c-4f75-a06c-06ddcf20836e",
  "status" : 200,
  "headers" : {
    "X-Application-Context" : [
      "application:8088"
    ],
    "Content-Type" : [
      "application/json;charset=UTF-8"
    ],
    "Transfer-Encoding" : [
      "chunked"
    ],
    "Date" : [
      "Sun, 24 Dec 2017 13:10:45 GMT"
    ]
  },
  "body" : {
    "thekey" : "some_example"
  },
  "duration" : 105,
  "protocol" : "HTTP/1.1",
  "type" : "response"
}

但是它不输出处理请求的类名。该库确实提供了一些用于编写自定义记录器的接口。

笔记

与此同时,库已经有了显著的发展,目前的版本是2.4.1,请参阅https://github.com/zalando/logbook/releases。例如,默认输出格式已经改变,可以配置,过滤等。

不要忘记将日志级别设置为TRACE,否则你将看不到任何东西:

logging:
  level:
    org.zalando.logbook: TRACE

这段代码适用于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();
            }

        }
    }

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

你可以使用面向方面的编程在一个地方处理所有这些。

After adding Actuators to the spring boot bassed application you have /trace endpoint available with latest requests informations. This endpoint is working based on TraceRepository and default implementation is InMemoryTraceRepository that saves last 100 calls. You can change this by implementing this interface by yourself and make it available as a Spring bean. For example to log all requests to log (and still use default implementation as a basic storage for serving info on /trace endpoint) I'm using this kind of implementation:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.Trace;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;


@Component
public class LoggingTraceRepository implements TraceRepository {

  private static final Logger LOG = LoggerFactory.getLogger(LoggingTraceRepository.class);
  private final TraceRepository delegate = new InMemoryTraceRepository();

  @Override
  public List<Trace> findAll() {
    return delegate.findAll();
  }

  @Override
  public void add(Map<String, Object> traceInfo) {
    LOG.info(traceInfo.toString());
    this.delegate.add(traceInfo);
  }
}

这个traceInfo映射以这种形式包含了请求和响应的基本信息: {method=GET, path=/api/hello/John, headers={request={host=localhost:8080, user-agent=curl/7.51.0, accept=*/*}, response={X-Application-Context=application, Content-Type=text/plain;charset=UTF-8, Content-Length=10, Date=Wed, 29 Mar 2017 20:41:21 GMT, status=200}}}。这里没有响应内容。

编辑!测井POST数据

你可以通过覆盖WebRequestTraceFilter来访问POST数据,但不认为这是一个好主意(例如,所有上传的文件内容都会去日志) 下面是示例代码,但不要使用它:

package info.fingo.nuntius.acuate.trace;

import org.apache.commons.io.IOUtils;
import org.springframework.boot.actuate.trace.TraceProperties;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class CustomWebTraceFilter extends WebRequestTraceFilter {

  public CustomWebTraceFilter(TraceRepository repository, TraceProperties properties) {
    super(repository, properties);
}

  @Override
  protected Map<String, Object> getTrace(HttpServletRequest request) {
    Map<String, Object> trace = super.getTrace(request);
    String multipartHeader = request.getHeader("content-type");
    if (multipartHeader != null && multipartHeader.startsWith("multipart/form-data")) {
        Map<String, Object> parts = new LinkedHashMap<>();
        try {
            request.getParts().forEach(
                    part -> {
                        try {
                            parts.put(part.getName(), IOUtils.toString(part.getInputStream(), Charset.forName("UTF-8")));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
            );
        } catch (IOException | ServletException e) {
            e.printStackTrace();
        }
        if (!parts.isEmpty()) {
            trace.put("multipart-content-map", parts);
        }
    }
    return trace;
  }
}