我正在用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+赞,我可以在这里添加准确的代码示例)

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。

我创建了一个名为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

自从最初的问题发布以来,执行器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.

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