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);
}
}
效果展示
上一篇: java项目中读取src目录下的文件
下一篇: symfony访问web目录外的图片