欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

AOP/Filter+MDC实现traceId日志追踪

程序员文章站 2022-07-03 15:40:09
...

AOP/Filter+MDC实现traceId日志追踪

在应用日志查询时,我们常常希望可以有个关键字可以查询某个业务的整生命周期,log4j 和 logback提供了MDC(Mapped Diagnostic Context,映射调试上下文)功能,可以在多线程条件下记录日志。

在微服务、分布式中更是希望可以进行链路追踪。


一、AOP+MDC简单实现


/**
 * ************************************************************
 * Copyright © 2021 cnzz Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: Unknown
 * @description: 业务日志追踪
 * @author: cnzz
 * @create: 2021-01-12 13:49
 **/
@Slf4j
@Component
@Aspect
public class TraceIdHandler {
    private static final String TRACE_ID = "traceId";


   /* 参数部分允许使用通配符:
            *  匹配任意字符,但只能匹配一个元素
            .. 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用
            +  必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类
  */
    @Before(value = "execution(* com.ytkj.feec..*(..))")
    public void excuteBefore() {
        if (StringUtils.isBlank(MDC.get(TRACE_ID))) {
            String traceId = UUID.randomUUID().toString().replace("-", "");
            MDC.put(TRACE_ID, traceId);
        }
    }
}

 

log日志文件xml配置  输出格式Pattern      添加   

%X{traceId}
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

    <!-- 属性文件:在properties文件中找到对应的配置项 -->
    <springProperty scope="context" name="logging.path" source="logging.path"/>

    <!--logger上下文名称,区分不同应用程序-->
    <contextName>rcbcloud-settle</contextName>
    <property name="LOG_PATH" value="/data/logs/paycentre" />
    <property name="project_name" value="zuul" />
    <property name="LOG_HOME" value="${LOG_PATH}/%d{yyyyMMdd}/${project_name}/${project_name}"/>

    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出(配色):%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%yellow(%d{yyyy-MM-dd HH:mm:ss.SSS }) %magenta([%thread]) - %red([%-5level]) %cyan(%c [%L]) - %blue([%X{corrId}-%X{traceId}]) - %highlight(%msg) %n

            </pattern>
            <charset class="java.nio.charset.Charset">UTF-8</charset>
        </encoder>
    </appender>

    <!--根据日志级别分离日志,分别输出到不同的文件-->
    <appender name="fileInfoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">

            <level>INFO</level>

          <!--  <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>-->
        </filter>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--按时间保存日志 修改格式可以按小时、按天、月来保存-->
            <fileNamePattern>${LOG_HOME}.info.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!--保存时长-天数  MaxHistory指的是文件数量,超过MaxHistory数量才会删除,只有当每天生成且只生成一个文件时才表示保留天数-->
            <MaxHistory>90</MaxHistory>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
            <!--文件大小-->
            <totalSizeCap>1GB</totalSizeCap>

        </rollingPolicy>
    </appender>

    <appender name="fileErrorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
            </pattern>
        </encoder>
        <!--滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--路径-->
            <fileNamePattern>${LOG_HOME}.error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <MaxHistory>90</MaxHistory>
        </rollingPolicy>
    </appender>
    <root level="debug">
        <appender-ref ref="consoleLog"/>
        <appender-ref ref="fileInfoLog"/>
        <appender-ref ref="fileErrorLog"/>
    </root>
</configuration>

二、使用filter+MDC实现



/**
 * ************************************************************
 * Copyright © 2020 cnzz Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: demo
 * @description: api过滤器
 * @author: cnzz
 * @create: 2020-12-17 11:31
 * <p>
 * 对接口api进行签名验证
 **/
@Configuration
@Slf4j
public class ApiHeaderFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        long startTime=System.currentTimeMillis();
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        //参数偷换,直留data
        //HttpServletRequest没有提供相关的set方法来修改body,所以需要用修饰类
        servletRequest = new BodyRequestWrapper2((HttpServletRequest) request);
        filterChain.doFilter(servletRequest, servletResponse);

        //交易响应时长
        String servletPath = request.getServletPath();
        log.info("响应时长--time={}ms,servletPath={}",System.currentTimeMillis()-startTime,servletPath);
    }

    @Override
    public void destroy() {

    }

}

 



/**
 * ************************************************************
 * Copyright © 2020 cnzz Inc.All rights reserved.  *    **
 * ************************************************************
 *
 * @program: demo
 * @description: 重新请求对象
 * @author: cnzz
 * @create: 2020-12-17 14:03
 * <p>
 * 整理请求参数
 **/
@Slf4j
public class BodyRequestWrapper2 extends HttpServletRequestWrapper {
    private byte[] body;
    private static final String TRACE_ID = "traceId";
    public BodyRequestWrapper2(HttpServletRequest request){
        super(request);

//        StreamUtil.readBytes(request.getReader(), "utf-8");
//        //由于request并没有提供现成的获取json字符串的方法,所以我们需要将body中的流转为字符串
//        String json = new String(StreamUtil.readBytes(request.getReader(), "utf-8"));
        String data = HttpUtil.getDataFromRequest2(request);
        body = data.getBytes();

        //1、获取请求头
        SysHeader header = getSysHeader(request, data);

        //2、MDC + trace_id添加
        String traceId = StringUtils.isNotBlank(header.getCorrId()) ? header.getCorrId() : UUID.randomUUID().toString().replace("-", "");
        MDC.put(TRACE_ID, traceId);

        //3、servletPath+userInfo+ipAddr+userAgent
        String ipAddr = IpUtils.getIpAddr(request);
        String servletPath = request.getServletPath();
        String userAgent = request.getHeader("User-Agent");
        log.info("【请求filter】servletPath={},ipAddr={},userAgent={}", servletPath, ipAddr,userAgent);
        if (!ApiRouter.isApiRouter(servletPath)) {

            log.info("【需要验签】{}", servletPath);
            //校验
            ValidSysReqUtil2.validSysHeader(header);
        }

    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    /**
     * 在使用@RequestBody注解的时候,其实框架是调用了getInputStream()方法,所以我们要重写这个方法
     *
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }


    private SysHeader getSysHeader(HttpServletRequest request, String data) {
        return new SysHeader()
                .setTimestamp(request.getHeader("timestamp"))
                .setSign(request.getHeader("sign"))
                .setSignType(request.getHeader("signType"))
                .setCorrId(request.getHeader("corrId"))
                .setData(data);
    }

}

效果展示

AOP/Filter+MDC实现traceId日志追踪