0


web 应用层接口请求日志

需求:
前文已经讲过如何使用MDC在日志中为每个请求生成一个唯一

traceID

,日志生成traceID。
请求作为入口,一般的系统都会有一个

或者

文件

记录每个请求,方便运维统计接口调用情况,实现方案大体两种:

  • 使用 Spring AOP
  • 使用 Filter
  • 使用 Interceptor

感兴趣的,代码可以通过我的Demo工程获取。

一、Filter

1.1 CommonsRequestLoggingFilter

Spring Boot 自带了一个现成的 CommonsRequestLoggingFilter,它可以记录请求的详细信息并支持非常灵活的配置,省去了手动管理 Filter 的复杂性。
但是个人不建议在生产环境使用!

  1. 配置Filter
@ConfigurationpublicclassRequestLoggingConfig{@BeanpublicCommonsRequestLoggingFilterrequestLoggingFilter(){CommonsRequestLoggingFilter loggingFilter =newCommonsRequestLoggingFilter();
        loggingFilter.setIncludeClientInfo(true);
        loggingFilter.setIncludeQueryString(true);
        loggingFilter.setIncludePayload(true);
        loggingFilter.setMaxPayloadLength(1000);// 设置要记录的最大请求体长度
        loggingFilter.setIncludeHeaders(true);// 可选:是否记录请求头return loggingFilter;}}
  1. 配置日志打印

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?><configuration><loggername="org.springframework.web.filter.CommonsRequestLoggingFilter"><levelvalue="DEBUG"/></logger></configuration>

**要注意如果没有重写CommonsRequestLoggingFilter的方法,日志级别必须是

DEBUG

**。

1.1.1 INFO级别 日志不打印问题

通过

CommonsRequestLoggingFilter

源码可以知晓,

shouldLog

默认是使用

DEBUG

的,错误原因就很简单了。
根据类的设计可以知晓,

CommonsRequestLoggingFilter

设计是为了开发人员在开发阶段、排查错误阶段打印接口日志,所以对于统计接口信息来讲就不太合适,所以个人不推荐使用。

CommonsRequestLoggingFilter

继承

AbstractRequestLoggingFilter

再往上继承

OncePerRequestFilter

再往上继承

GenericFilterBean

GenericFilterBean

实现了

Filter

。所以本质上都是

Filter

方案,只不过可以选择使用现有的类去满足自己的需求而已。

CommonsRequestLoggingFilter.java

publicclassCommonsRequestLoggingFilterextendsAbstractRequestLoggingFilter{@OverrideprotectedbooleanshouldLog(HttpServletRequest request){return logger.isDebugEnabled();}/**
     * Writes a log message before the request is processed.
     */@OverrideprotectedvoidbeforeRequest(HttpServletRequest request,String message){
        logger.debug(message);}/**
     * Writes a log message after the request is processed.
     */@OverrideprotectedvoidafterRequest(HttpServletRequest request,String message){
        logger.debug(message);}}

修改后的代码:

@ConfigurationpublicclassWebConfig{@BeanpublicCommonsRequestLoggingFilterlogFilter(){CommonsRequestLoggingFilter filter
                =newCommonsRequestLoggingFilter(){@OverrideprotectedbooleanshouldLog(HttpServletRequest request){returntrue;}@OverrideprotectedvoidbeforeRequest(HttpServletRequest request,String message){
                logger.info(message);}@OverrideprotectedvoidafterRequest(HttpServletRequest request,String message){
                logger.info(message);}};
        filter.setIncludeQueryString(true);
        filter.setIncludePayload(true);
        filter.setMaxPayloadLength(10000);
        filter.setIncludeHeaders(false);
        filter.setAfterMessagePrefix("REQUEST DATA: ");return filter;}}

1.2 OncePerRequestFilter

OncePerRequestFilter

是 Spring 提供的一个抽象类,它可以确保一个请求只会经过一次过滤。

  1. 日志类继承实现
@Slf4j@ComponentpublicclassCustomRequestLoggingFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)throwsServletException,IOException{long startTime =System.currentTimeMillis();// 忽略文件上传请求if(isFileUpload(request)){
            filterChain.doFilter(request, response);return;}try{// 继续处理请求
            filterChain.doFilter(request, response);}finally{// 请求结束后记录日志long duration =System.currentTimeMillis()- startTime;logRequest(request, duration);}}privatevoidlogRequest(HttpServletRequest request,long duration){StringBuilder logMessage =newStringBuilder();
        logMessage.append("Method=").append(request.getMethod()).append("; ");
        logMessage.append("URI=").append(request.getRequestURI()).append("; ");
        logMessage.append("Query=").append(request.getQueryString()).append("; ");
        logMessage.append("RemoteIP=").append(request.getRemoteAddr()).append("; ");
        logMessage.append("Duration=").append(duration).append("ms;");

        log.info(logMessage.toString());}privatebooleanisFileUpload(HttpServletRequest request){return"POST".equalsIgnoreCase(request.getMethod())&& request.getContentType()!=null&& request.getContentType().startsWith("multipart/form-data");}}
  1. 配置日志打印

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?><configuration><loggername="com.github.nan.web.core.filter.CustomRequestLoggingFilter"level="INFO"additivity="false"><!-- 一般来讲是写到一个单独的文件,这里只是一个参考 --><appender-refref="STDOUT"/></logger></configuration>

二、AOP

AOP(面向切面编程) 也是一个非常灵活且强大的方式来记录请求数据。AOP 可以在不修改现有代码的情况下,横切关注点(如日志记录、事务管理等),并且能够更加精细地控制在哪些方法或控制器上进行日志记录。

  1. 请求日志切面
@Slf4jpublicclassRequestLoggingAspect{@Before("execution(* com.github.nan.web.demos.web..*(..))")publicvoidlogBefore(JoinPoint joinPoint){HttpServletRequest request =((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();// 忽略文件上传请求if("POST".equalsIgnoreCase(request.getMethod())&& request.getContentType()!=null&& request.getContentType().startsWith("multipart/form-data")){return;}

        log.info("Request received: [Method: {}] [URI: {}] [Query: {}] [Remote IP: {}] [Arguments: {}]",
                request.getMethod(),
                request.getRequestURI(),
                request.getQueryString(),
                request.getRemoteAddr(),Arrays.toString(joinPoint.getArgs()));}@Around("execution(* com.github.nan.web.demos.web..*(..))")publicObjectlogAround(ProceedingJoinPoint joinPoint)throwsThrowable{long startTime =System.currentTimeMillis();Object result = joinPoint.proceed();long duration =System.currentTimeMillis()- startTime;

        log.info("Request completed: [Duration: {} ms] [Return value: {}]",
                duration,
                result !=null? result.toString():"null");return result;}}

AOP 的优点

  • 灵活性:你可以根据具体的类或方法定义切点,选择性地记录某些控制器的请求。
  • 高可定制性:可以记录更加详细的日志内容,例如请求参数、执行耗时、返回值等。
  • 避免重复代码:AOP 可以在不同的控制器中实现统一的日志记录逻辑,不需要在每个控制器中写相同的代码。

三、Interceptor

Spring 提供了 HandlerInterceptor 接口,用于在处理 HTTP 请求之前、处理之后以及完成请求时执行一些操作。拦截器通常用于日志记录、权限检查等场景。

  1. 拦截器代码
@Slf4j@ComponentpublicclassRequestLoggingInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler){
        request =wrapRequest(request);String payload ="";if(request instanceofContentCachingRequestWrapper){ContentCachingRequestWrapper wrapper =(ContentCachingRequestWrapper) request;byte[] content = wrapper.getContentAsByteArray();try{
                payload =newString(content, wrapper.getCharacterEncoding());}catch(UnsupportedEncodingException e){
                payload ="[unknown encoding]";}}

        log.info("Request received: [Method: {}] [URI: {}] [Query: {}] [Remote IP: {}] [Payload : {}]",
                request.getMethod(),
                request.getRequestURI(),
                request.getQueryString(),
                request.getRemoteAddr(),
                payload);returntrue;// 返回 true 继续处理请求,false 则终止请求}@OverridepublicvoidafterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex){
        log.info("Request completed: [Status: {}]", response.getStatus());}privateHttpServletRequestwrapRequest(HttpServletRequest request){// 仅在 POST 请求的 JSON 请求体时进行包装,确保缓存请求体if(HttpMethod.POST.name().equalsIgnoreCase(request.getMethod())&&MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType())){returnnewContentCachingRequestWrapper(request);}return request;}}
  1. 注册拦截器
@ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{@AutowiredprivateRequestLoggingInterceptor requestLoggingInterceptor;@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(requestLoggingInterceptor).addPathPatterns("/**");}}

四、常见问题

4.1

Post

请求 payLoad 丢失

HttpServletRequest 的请求体只能被消费一次,之后再尝试读取时就会发现请求体已经被“耗尽”。

解决方案:使用

ContentCachingRequestWrapper

你可以将原始的 HttpServletRequest 包装为 ContentCachingRequestWrapper,这样就可以在拦截器中读取请求体,而不会影响后续控制器的处理。 可以参考 Interceptor 的代码。

五、总结

上述,

Filter

AOP

Interceptor

的代码方案只是阐述了实现的方式,一些实现的细节,需要根据自己的需求去补充,例如上传/下载文件要如何记录? 存在敏感信息的接口如何处理?


本文转载自: https://blog.csdn.net/code_nn/article/details/143213871
版权归原作者 知楠行易 所有, 如有侵权,请联系我们删除。

“web 应用层接口请求日志”的评论:

还没有评论