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


当前回答

下面粘贴的代码与我的测试一起工作,可以从我的[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;
        }

    }
}

其他回答

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

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

下面粘贴的代码与我的测试一起工作,可以从我的[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;
        }

    }
}

如前所述,Logbook非常适合于此,但在使用Java模块时,由于在Logbook -api和Logbook -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>

目前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的示例文章