亲测可用,SpringBoot项目打印接口请求信息日志,CommonsRequestLoggingFilter实现方式

需求背景

  1. 线上项目出现bug时,可以通过接口的请求参数来排查定位问题。
  2. 和业务方battle时,能够证明他是自己操作的问题。

效果图

在这里插入图片描述

在这里插入图片描述

实现思路

  • Spring提供了CommonsRequestLoggingFilter过滤器,该过滤器可以在接口请求前和请求后分别打印日志;
  • 通过继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑;

其他方案对比

  1. aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截。
  2. 拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰。

优缺点分析

缺点

  1. 无法获取返回值,适合一些不关心返回值的场景(如果接口出现异常,此时也没有返回值)。
  2. 对于Payload参数,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream()流,所以必须要先调用@RequstBody,才能取到值(可以去看下ContentCachingRequestWrapper的具体实现)

优点

  1. 实现简单,代码层次逻辑清晰,与业务逻辑解耦。

具体实现

继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑

package com.yp.basic.log.filter;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.yp.basic.log.properties.OptLogProperties;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Set;

/**
 * 自定义的请求日志打印过滤器,打印所有接口请求参数、耗时等日志 <br/>
 *
 * 对比其他两种方式:<br/>
 * <ul>
 *     <li>1、aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截</li>
 *     <li>2、拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰</li>
 * </ul>
 *
 *  缺点:<br/>
 *  <ul>
 *      <li>1、无法获取返回值,适合一些不关心返回值的场景。</li>
 *      <li>2. 内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。</li>
 *  </ul>
 *
 *  优点:<br/>
 *  <ul>
 *      <li>1、 实现简单,代码层次逻辑清晰。</li>
 *  </ul>
 *
 * @author: wcong
 * @date: 2022/11/25 15:17
 */
@Slf4j
public class RequestLogPrintFilter extends CommonsRequestLoggingFilter {

    /**
     * 接口请求 开始 日志前缀标识,目的是为了提高每次判断时的性能
     */
    public final static String REQUEST_START_PREFIX_FLAG = "0";
    /**
     * 接口请求 开始 日志前缀
     */
    private final static String REQUEST_START_PREFIX = "### request start[";
    /**
     * 接口请求 结束 日志前缀标识,目的是为了提高每次判断时的性能
     */
    public final static String REQUEST_END_PREFIX_FLAG = "1";
    /**
     * 接口请求 结束 日志前缀
     */
    private final static String REQUEST_END_PREFIX = "### request end[";

    /**
     * 是否为prod环境
     */
    private static final boolean IS_PROD_EVN;
    /**
     * 不打印接口请求日志的url集合
     */
    private static final Set<String> EXCLUDE_HTTP_LOG_URLS;
    static {
        final String activeProfile = SpringUtil.getActiveProfile();
        final OptLogProperties optLogProperties = SpringUtil.getBean(OptLogProperties.class);
        IS_PROD_EVN = "prod".equals(activeProfile);
        EXCLUDE_HTTP_LOG_URLS = optLogProperties.getExcludeHttpLogUrls();
    }

    /**
     * 重写父类方法:封装打印消息的格式
     */
    @Override
    protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
        // 是否为不打印的url
        if (isExcludeLogUrl((request.getRequestURI()))) {
            return null;
        }

        final StringBuilder messageInfo = getMessageInfo(request, prefix, suffix);
        // 请求开始还是结束
        if (REQUEST_START_PREFIX_FLAG.equals(prefix)) {
            // 请求开始
            MDC.put("logStartTime", String.valueOf(System.currentTimeMillis()));
        } else {
            // 请求结束,记录耗时
            final Long logStartTime = Convert.toLong(MDC.get("logStartTime"), 0L);
            messageInfo.append("\r\n接口耗时: ").append(System.currentTimeMillis() - logStartTime).append("ms");
        }
        return messageInfo.toString();
    }

    /**
     * 重写父类方法:请求前调用逻辑
     */
    @Override
    protected void beforeRequest(HttpServletRequest request, String message) {
        // 是否为不打印的url
        if (isExcludeLogUrl((request.getRequestURI()))) {
            return;
        }
        doPrintLog(message);
    }

    /**
     * 重写父类方法:请求后调用逻辑
     */
    @Override
    protected void afterRequest(HttpServletRequest request, String message) {
        // 是否为不打印的url
        if (isExcludeLogUrl((request.getRequestURI()))) {
            return;
        }
        doPrintLog(message);
    }
    /**
     * 重写父类方法:是否打印日志
     */
    @Override
    protected boolean shouldLog(HttpServletRequest request) {
        // 父类中的逻辑是:logger.isDebugEnabled()
        return true;
    }

    /**
     * 统一封装打印的日志格式
     *
     * @param request   javax.servlet.http.HttpServletRequest
     * @param prefix    打印前缀
     * @param suffix    打印后缀
     * @return 封装好的日志格式
     */
    private StringBuilder getMessageInfo(HttpServletRequest request, String prefix, String suffix) {
        StringBuilder msg = new StringBuilder();

        // 判断是 请求开始 还是 请求结束
        if (REQUEST_START_PREFIX_FLAG.equals(prefix)) {
            msg.append(REQUEST_START_PREFIX);
        } else {
            msg.append(REQUEST_END_PREFIX);
        }

        msg.append(StrUtil.format("method={}; ", request.getMethod().toLowerCase()));
        msg.append("uri=").append(request.getRequestURI());

        // 是否有传递 查询字符串 信息
        if (isIncludeQueryString()) {
            String queryString = request.getQueryString();
            if (queryString != null) {
                msg.append('?').append(queryString);
            }
        }

        // 是否有传递 payload 信息
        if (isIncludePayload()) {
            String payload = getMessagePayload(request);
            if (payload != null) {
                msg.append("; payload=").append(payload);
            }
        }

        // 是否包含 客户端 信息
        if (isIncludeClientInfo()) {
            String client = request.getRemoteAddr();
            if (StrUtil.isNotBlank(client)) {
                msg.append("; client=").append(client);
            }
            HttpSession session = request.getSession(false);
            if (session != null) {
                msg.append("; session=").append(session.getId());
            }
            String user = request.getRemoteUser();
            if (user != null) {
                msg.append("; user=").append(user);
            }
        }

        msg.append(suffix);
        return msg;
    }

    /**
     * 具体打印的方法
     *
     * @param message   打印的消息
     */
    private void doPrintLog(String message) {
        // 生产环境打印debug级别
        if (IS_PROD_EVN) {
            log.debug(message);
        } else {
            log.info(message);
        }
        // log.info(message);
    }


    private Boolean isExcludeLogUrl(String url) {
        return EXCLUDE_HTTP_LOG_URLS.contains(url);
    }

}

注册过滤器

package com.yp.basic.log;

import com.yp.basic.log.properties.OptLogProperties;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

/**
 * 日志自动配置
 *
 * @author: wcong
 * @date: 2022/11/25 15:17
 */
@EnableAsync
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(OptLogProperties.class)
public class LogAutoConfiguration {

    @Bean
    @ConditionalOnProperty(prefix = OptLogProperties.PREFIX, name = "enable-http-log", havingValue = "true", matchIfMissing = true)
    public FilterRegistrationBean<CommonsRequestLoggingFilter> logFilterRegistration() {
        CommonsRequestLoggingFilter filter = new RequestLogPrintFilter();
        // 是否打印header中的内容,参数很多
        filter.setIncludeHeaders(false);
        // 是否打印查询字符串内容
        filter.setIncludeQueryString(true);
        // 是否打印 payLoad内容,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。
        filter.setIncludePayload(true);
        // 是否打印客户端信息(ip、session、remoteUser)
        filter.setIncludeClientInfo(true);
        // 1024字节(1kb),超出部分截取
        // 在UTF-8编码方案中,一个英文字符占用一个字节,一个汉字字符占用三个字节的空间。
        filter.setMaxPayloadLength(1024);
        // 设置 before request 日志前缀,默认为:Before request [
        filter.setBeforeMessagePrefix(RequestLogPrintFilter.REQUEST_START_PREFIX_FLAG);
        // 设置 before request 日志后缀,默认为:]
        filter.setBeforeMessageSuffix("]");
        // 设置 before request 日志前缀,默认为:After request [
        filter.setAfterMessagePrefix(RequestLogPrintFilter.REQUEST_END_PREFIX_FLAG);
        // 设置 after request 日志后缀,默认为:]
        filter.setAfterMessageSuffix("]");

        FilterRegistrationBean<CommonsRequestLoggingFilter> registration = new FilterRegistrationBean<>(filter);
        registration.addUrlPatterns("/*");
        registration.setOrder(0);
        registration.setName("commonsRequestLoggingFilter");
        return registration;
    }

}

OptLogProperties:在yml中提供配置

package com.yp.basic.log.properties;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashSet;
import java.util.Set;

import static com.yp.basic.log.properties.OptLogProperties.PREFIX;

/**
 * 操作日志配置类
 *
 */
@Data
@NoArgsConstructor
@ConfigurationProperties(prefix = PREFIX)
public class OptLogProperties {
    public static final String PREFIX = "xxx.log";

    /**
     * 是否启用
     */
    private Boolean enabled = true;

    /**
     * 是否打印http接口请求日志
     */
    private Boolean enableHttpLog = true;

    /**
     * 不打印http接口请求日志的url
     */
    private Set<String> excludeHttpLogUrls = new HashSet<>();

}

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值