我正在用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,但正如我提到的,我需要在一个地方(和单个日志)处理所有成功和错误请求。
你也可以配置一个自定义的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";
}
}
}
如果您不介意尝试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
日志请求+自定义格式的有效载荷:
对于自定义格式,只需覆盖Spring日志记录器Bean的超级实现
假设我们希望跳过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+赞,我可以在这里添加准确的代码示例)
日志库是专门为记录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
如果不需要记录已执行的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中覆盖这个行为来缓存原始的处理程序。