Spring Boot -如何记录所有的请求和响应与异常在单一的地方?

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

565014 次浏览

这段代码适用于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 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建议在匹配的方法执行返回时运行 正常。< / p >

@ afterthrows通知在匹配的方法执行由退出时运行

如果你想详细阅读,请阅读这篇文章。 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html < / p >

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

但是有了这个要求,你必须访问存储在DispatcherServlethandlerMapping中的信息。也就是说,你可以重写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.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971处覆盖这个行为,以缓存原始的处理程序。

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

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

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

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

@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);
}
}
下面是我如何在春季数据休息 通过使用 org.springframework.web.util.ContentCachingRequestWrapperorg.springframework.web.util.ContentCachingResponseWrapper < / p >
/**
* Doogies very cool HTTP request logging
*
* There is also {@link org.springframework.web.filter.CommonsRequestLoggingFilter}  but it cannot log request method
* And it cannot easily be extended.
*
* https://mdeinum.wordpress.com/2015/07/01/spring-framework-hidden-gems/
* http://stackoverflow.com/questions/8933054/how-to-read-and-copy-the-http-servlet-response-output-stream-content-for-logging
*/
public class DoogiesRequestLogger extends OncePerRequestFilter {


private boolean includeResponsePayload = true;
private int maxPayloadLength = 1000;


private String getContentAsString(byte[] buf, int maxLength, String charsetName) {
if (buf == null || buf.length == 0) return "";
int length = Math.min(buf.length, this.maxPayloadLength);
try {
return new String(buf, 0, length, charsetName);
} catch (UnsupportedEncodingException ex) {
return "Unsupported Encoding";
}
}


/**
* Log each request and respponse with full Request URI, content payload and duration of the request in ms.
* @param request the request
* @param response the response
* @param filterChain chain of filters
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


long startTime = System.currentTimeMillis();
StringBuffer reqInfo = new StringBuffer()
.append("[")
.append(startTime % 10000)  // request ID
.append("] ")
.append(request.getMethod())
.append(" ")
.append(request.getRequestURL());


String queryString = request.getQueryString();
if (queryString != null) {
reqInfo.append("?").append(queryString);
}


if (request.getAuthType() != null) {
reqInfo.append(", authType=")
.append(request.getAuthType());
}
if (request.getUserPrincipal() != null) {
reqInfo.append(", principalName=")
.append(request.getUserPrincipal().getName());
}


this.logger.debug("=> " + reqInfo);


// ========= Log request and response payload ("body") ========
// We CANNOT simply read the request payload here, because then the InputStream would be consumed and cannot be read again by the actual processing/server.
//    String reqBody = DoogiesUtil._stream2String(request.getInputStream());   // THIS WOULD NOT WORK!
// So we need to apply some stronger magic here :-)
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);


filterChain.doFilter(wrappedRequest, wrappedResponse);     // ======== This performs the actual request!
long duration = System.currentTimeMillis() - startTime;


// I can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work.
String requestBody = this.getContentAsString(wrappedRequest.getContentAsByteArray(), this.maxPayloadLength, request.getCharacterEncoding());
if (requestBody.length() > 0) {
this.logger.debug("   Request body:\n" +requestBody);
}


this.logger.debug("<= " + reqInfo + ": returned status=" + response.getStatus() + " in "+duration + "ms");
if (includeResponsePayload) {
byte[] buf = wrappedResponse.getContentAsByteArray();
this.logger.debug("   Response body:\n"+getContentAsString(buf, this.maxPayloadLength, response.getCharacterEncoding()));
}


wrappedResponse.copyBodyToResponse();  // IMPORTANT: copy content of response back into original response


}




}

在将致动器添加到基于spring引导的应用程序后,您就有了带有最新请求信息的/trace端点。该端点基于TraceRepository工作,默认实现是InMemoryTraceRepository,保存最近100次调用。您可以通过自己实现该接口并使其作为Spring bean可用来改变这一点。例如,将所有请求记录到日志(并且仍然使用默认实现作为在/trace端点上提供信息的基本存储),我使用这种实现:

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

Spring已经提供了一个过滤器来完成这项工作。将以下bean添加到配置中

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

不要忘记将日志级别org.springframework.web.filter.CommonsRequestLoggingFilter更改为DEBUG

我在application.properties中定义了日志级别,以便在日志文件中打印请求/响应,方法url

logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=INFO
logging.file=D:/log/myapp.log

我用的是Spring Boot。

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

日志库专门用于记录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 HttpTrace执行器实现。但正如他们告诉上面的那样,它不会把尸体弄沉。

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.stereotype.Repository;


@Slf4j
@Repository
public class LoggingInMemoryHttpTraceRepository extends InMemoryHttpTraceRepository {
public void add(HttpTrace trace) {
super.add(trace);
log.info("Trace:" + ToStringBuilder.reflectionToString(trace));
log.info("Request:" + ToStringBuilder.reflectionToString(trace.getRequest()));
log.info("Response:" + ToStringBuilder.reflectionToString(trace.getResponse()));
}
}

为了只记录结果为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);
}
}


}

你也可以配置一个自定义的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");
}


}

< em >注意: 就像@Robert说的那样,你需要注意你的应用程序正在使用的HttpServletRequestHttpServletResponse的特定实现

例如,对于使用ShallowEtagHeaderFilter的应用程序,响应实现将是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";
}
}


}

如果你只看到请求有效负载的一部分,你需要调用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;
}

实际答案请参考下面的链接 https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba < / p >

对上面提到的解决方案做了一些更改,如果记录器级别为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 Boot拥有用于获取请求和响应日志的执行器特性。

但是您也可以使用Aspect(AOP)获取日志。

Aspect为您提供了诸如@Before@AfterReturning@AfterThrowing等注释。

@Before记录请求,@AfterReturning记录响应,@AfterThrowing记录错误消息, 你可能不需要所有端点的日志,所以你可以在包上应用一些过滤器

下面是一些例子:

请求:

@Before("within(your.package.where.endpoints.are..*)")
public void endpointBefore(JoinPoint p) {
if (log.isTraceEnabled()) {
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
Object[] signatureArgs = p.getArgs();




ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {


if (signatureArgs[0] != null) {
log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
}
} catch (JsonProcessingException e) {
}
}
}

这里@Before("within(your.package.where.endpoints.are..*)")有包路径。这个包中的所有端点都将生成日志。

响应:

@AfterReturning(value = ("within(your.package.where.endpoints.are..*)"),
returning = "returnValue")
public void endpointAfterReturning(JoinPoint p, Object returnValue) {
if (log.isTraceEnabled()) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
}
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
}
}

这里@AfterReturning("within(your.package.where.endpoints.are..*)")有包路径。这个包中的所有端点都将生成日志。Object returnValue也包含响应。

例外:

@AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws DmoneyException {
if (log.isTraceEnabled()) {
System.out.println(e.getMessage());


e.printStackTrace();




log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
}
}

这里@AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")有包路径。这个包中的所有端点都将生成日志。而且Exception e包含错误响应。

以下是完整的代码:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Aspect
@Order(1)
@Component
@ConditionalOnExpression("${endpoint.aspect.enabled:true}")
public class EndpointAspect {
static Logger log = Logger.getLogger(EndpointAspect.class);


@Before("within(your.package.where.is.endpoint..*)")
public void endpointBefore(JoinPoint p) {
if (log.isTraceEnabled()) {
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
Object[] signatureArgs = p.getArgs();




ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {


if (signatureArgs[0] != null) {
log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
}
} catch (JsonProcessingException e) {
}
}
}


@AfterReturning(value = ("within(your.package.where.is.endpoint..*)"),
returning = "returnValue")
public void endpointAfterReturning(JoinPoint p, Object returnValue) {
if (log.isTraceEnabled()) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
}
log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
}
}




@AfterThrowing(pointcut = ("within(your.package.where.is.endpoint..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws Exception {
if (log.isTraceEnabled()) {
System.out.println(e.getMessage());


e.printStackTrace();




log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
}
}
}

在这里,使用@ConditionalOnExpression("${endpoint.aspect.enabled:true}")可以启用/禁用日志。只需将endpoint.aspect.enabled:true添加到application.property并控制日志

更多关于AOP访问的信息:

Spring docs about AOP

关于AOP的示例文章

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

添加maven依赖:

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

编辑application.properties并添加以下行:

management.endpoints.web.exposure.include=*
一旦你的spring boot应用程序启动,你可以通过调用这个url来跟踪最新的100个http请求: http://localhost:8070/actuator/httptrace < / em >

下面粘贴的代码与我的测试一起工作,可以从我的[github项目][1]下载,在应用一个基于生产项目的解决方案后共享。

@Configuration
public class LoggingFilter extends GenericFilterBean {


/**
* It's important that you actually register your filter this way rather then just annotating it
* as @Component as you need to be able to set for which "DispatcherType"s to enable the filter
* (see point *1*)
*
* @return
*/
@Bean
public FilterRegistrationBean<LoggingFilter> initFilter() {
FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LoggingFilter());


// *1* make sure you sett all dispatcher types if you want the filter to log upon
registrationBean.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));


// *2* this should put your filter above any other filter
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);


return registrationBean;
}


@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {


ContentCachingRequestWrapper wreq =
new ContentCachingRequestWrapper(
(HttpServletRequest) request);


ContentCachingResponseWrapper wres =
new ContentCachingResponseWrapper(
(HttpServletResponse) response);


try {


// let it be ...
chain.doFilter(wreq, wres);


// makes sure that the input is read (e.g. in 404 it may not be)
while (wreq.getInputStream().read() >= 0);


System.out.printf("=== REQUEST%n%s%n=== end request%n",
new String(wreq.getContentAsByteArray()));


// Do whatever logging you wish here, in this case I'm writing request
// and response to system out which is probably not what you wish to do
System.out.printf("=== RESPONSE%n%s%n=== end response%n",
new String(wres.getContentAsByteArray()));


// this is specific of the "ContentCachingResponseWrapper" we are relying on,
// make sure you call it after you read the content from the response
wres.copyBodyToResponse();


// One more point, in case of redirect this will be called twice! beware to handle that
// somewhat


} catch (Throwable t) {
// Do whatever logging you whish here, too
// here you should also be logging the error!!!
throw t;
}


}
}
为了记录所有带有输入参数和主体的请求,我们可以使用过滤器拦截器。但是在使用过滤器或拦截器时,我们不能多次打印请求体。 更好的方法是使用spring-AOP。通过使用这个,我们可以将日志机制从应用程序中分离出来。AOP可用于记录应用程序中每个方法输入与输出

我的解决方案是:

 import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Aspect
@Component
public class LoggingAdvice {
private static final Logger logger =
LoggerFactory.getLogger(LoggingAdvice.class);


//here we can provide any methodName, packageName, className
@Pointcut(value = "execution(* com.package.name.*.*.*(..) )")
public void myPointcut() {


}


@Around("myPointcut()")
public Object applicationLogger(ProceedingJoinPoint pjt) throws Throwable {
ObjectMapper mapper = new ObjectMapper();
String methodName = pjt.getSignature().getName();
String className = pjt.getTarget().getClass().toString();
String inputParams = this.getInputArgs(pjt ,mapper);
logger.info("method invoked from " + className + " : " + methodName + "--Request Payload::::"+inputParams);
Object object = pjt.proceed();
try {
logger.info("Response Object---" + mapper.writeValueAsString(object));
} catch (Exception e) {
}
return object;
}


private String getInputArgs(ProceedingJoinPoint pjt,ObjectMapper mapper) {
Object[] array = pjt.getArgs();
CodeSignature signature = (CodeSignature) pjt.getSignature();


StringBuilder sb = new StringBuilder();
sb.append("{");
int i = 0;
String[] parameterNames = signature.getParameterNames();
int maxArgs = parameterNames.length;
for (String name : signature.getParameterNames()) {
sb.append("[").append(name).append(":");
try {
sb.append(mapper.writeValueAsString(array[i])).append("]");
if(i != maxArgs -1 ) {
sb.append(",");
}
} catch (Exception e) {
sb.append("],");
}
i++;
}
return sb.append("}").toString();
}

如果你已经配置了Spring引导配置服务器,那么只需要为类启用Debug记录器:

Http11InputBuffer.Http11InputBuffer.java

调试将记录每个请求的所有请求和响应

自从最初的问题发布以来,执行器HTTP跟踪是否有任何发展,即有一种方法来丰富它与响应体?

用来自MDC、Spring-Sleuth或Zipkin的自定义元数据(如traceId和spanId)来丰富它怎么样?

对我来说,执行器HTTP跟踪不工作Spring Boot 2.2.3,我在这里找到了修复:https://juplo.de/actuator-httptrace-does-not-work-with-spring-boot-2-2/

pom.xml

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

application.properties

management.endpoints.web.exposure.include=httptrace

解决办法:

解决这个问题的简单方法是,添加一个@Bean类型 InMemoryHttpTraceRepository到@Configuration-class:

@Bean
public HttpTraceRepository htttpTraceRepository()
{
return new InMemoryHttpTraceRepository();
}

解释:

这个问题的原因不是一个bug,而是一个合法的更改 默认配置。不幸的是,此更改没有在 根据文件部分。相反,它被埋在 Spring Boot 2.2的升级说明

默认实现在内存中存储捕获的数据。因此, 它会在用户不知情的情况下消耗大量内存,甚至更糟: 需要它。这在集群环境中尤其不可取, 那里的记忆是珍贵的好东西。记住:Spring Boot是 发明来简化集群部署!< / p >

也就是说,为什么这个功能现在默认是关闭的,而且必须关闭 如果需要,由用户显式打开

作为建议之前日志对于这一点来说非常完美,但在使用Java模块时,由于logbook-apilogbook-core之间的分割包,我确实在设置它时遇到了一些麻烦。

对于我的Gradle + Spring Boot项目,我需要

build.gradle

dependencies {
compileOnly group: 'org.zalando', name: 'logbook-api', version: '2.4.1'
runtimeOnly group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '2.4.1'
//...
}


logback-spring.xml

<configuration>
<!-- HTTP Requests and Responses -->
<logger name="org.zalando.logbook" level="trace" />
</configuration>

请注意

 @Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
...
}

方法不适用于弹簧安全过滤器链。 你必须手动添加CommonsRequestLoggingFilter,如

protected void configure(HttpSecurity http) throws Exception {
HttpSecurity filter = http
.cors().and().addFilterBefore(new CommonsRequestLoggingFilter(), CorsFilter.class);
}

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

我创建了一个名为LoggingConfig.java的文件,内容如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;


@Configuration
public class LoggingConfig {


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


在应用程序中。我添加的属性:

logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG

日志请求+自定义格式的有效载荷:

对于自定义格式,只需覆盖Spring日志记录器Bean的超级实现- org/springframework/web/filter/AbstractRequestLoggingFilter.java

假设我们希望跳过GET请求,只跟踪INFO日志级别的写请求(PUT, PATCH, DELETE等):

@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter logFilter = new CommonsRequestLoggingFilter() {


@Override
protected boolean shouldLog(HttpServletRequest request) {
return logger.isInfoEnabled() && !Objects.equals(request.getMethod(), "GET");
}




@Override
protected void beforeRequest(HttpServletRequest request, String message) {
// Do nothing if you need logging payload.
// As, Before the Request, the payload is not read from the input-stream, yet.
}




@Override
protected void afterRequest(HttpServletRequest request, String message) {
logger.info(message); // Or log to a file here, as OP asks.
}




@Override
protected @NonNull String createMessage(HttpServletRequest request, @NonNull String prefix, @NonNull String suffix) {
// Output: [PUT][/api/my-entity], user:[my-loging], payload was:[{ "id": 33, "value": 777.00}]
StringBuilder msg = new StringBuilder()
.append(prefix)
.append("[").append(request.getMethod()).append("]")
.append("[").append(request.getRequestURI()).append("]");


String user = request.getRemoteUser();
msg.append(", user:[").append(null == user ? "" : user).append("]");


String payload = getMessagePayload(request);
if (payload != null) {
// It's not null on After event. As, on Before event, the Input stream was not read, yet.
msg.append(", payload was:[").append(payload.replace("\n", "")).append("]");  // Remove /n to be compliant with elastic search readers.
}


msg.append(suffix);
return msg.toString();
}
};
logFilter.setBeforeMessagePrefix("Incoming REST call: -->>>[");
logFilter.setBeforeMessageSuffix("]...");
logFilter.setAfterMessagePrefix("REST call processed: -<<<[");
logFilter.setAfterMessageSuffix("]");
logFilter.setIncludePayload(true);
logFilter.setMaxPayloadLength(64000);
return logFilter;
}

日志请求+响应/状态:

看到https://www.baeldung.com/spring-http-logging#custom-request-logging

(如果答案得到需求/达到50+赞,我可以在这里添加准确的代码示例)

您可以在Spring Boot中使用驱动器。
它记录请求和响应以及有关servlet和系统操作的更多信息。
如果需要记录更多细节,只需将其添加为项目和配置的依赖项。
看一下这个例子:
Spring Boot示例中的执行器 < / p >