我正在用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 Boot拥有用于获取请求和响应日志的执行器特性。
但是您也可以使用Aspect(AOP)获取日志。
Aspect为您提供了诸如:@Before, @AfterReturning, @ afterthrows等注释。
@Before记录请求,@AfterReturning记录响应,@ afterthrows记录错误消息,
您可能不需要所有端点的日志,因此可以对包应用一些筛选器。
下面是一些例子:
请求:
@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) {
}
}
}
Here @Before("within(your.package.where.endpoint .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.endpoint .are. *)")有包的路径。这个包中的所有端点都将生成日志。对象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());
}
}
这里@ afterthrows (pointcut = ("within(your.package.where.endpoint .are..*)"), throws = "e")有包的路径。这个包中的所有端点都将生成日志。Exception包含错误响应。
以下是完整的代码:
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进入应用程序。属性并控制日志
更多关于AOP访问的信息:
关于AOP的Spring文档
关于AOP的示例文章
日志请求+自定义格式的有效载荷:
对于自定义格式,只需覆盖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跟踪是否有任何发展,即有一种方法来丰富它与响应体?
用来自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();
}
解释:
The cause of this problem is not a bug, but a legitimate change in the
default configuration. Unfortunately, this change is not noted in the
according section of the documentation. Instead it is burried in the
Upgrade Notes for Spring Boot 2.2
The default-implementation stores the captured data in memory. Hence,
it consumes much memory, without the user knowing, or even worse:
needing it. This is especially undesirable in cluster environments,
where memory is a precious good. And remember: Spring Boot was
invented to simplify cluster deployments!
That is, why this feature is now turned of by default and has to be
turned on by the user explicitly, if needed.