在项目中使用 Slf4j 的 MDC 机制 可以做到链路追踪,但是项目的微服务化,很多项目都需要进行链路追踪,所以通过 SpringBoot的自动装配机制 将链路追踪 封装为 一个组件,其他项目直接 POM 坐标引用就可以直接做到日志的链路追踪。
在封装组件的过程中最好成立一个单独的父子工程组件项目,不单单可以封装链路追踪的组件,还可以后续封装其他组件。
例如:工具类的组件 相信大家在开发过程中会使用到很多公用的工具类 可以封装到组件项目中,大家可以参考一下我的这种方式
用到技术:
这里的代码也是以新建父子工程组件项目来演示公开的。
这里代码有点多,如果不想费劲看的话可以直接拉到底部,直接git 将项目拉下来参考使用。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sample-framework</artifactId>
<groupId>com.su</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sample-trace-boot-starter</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<scope>provided</scope> <!--私有 不影响引用项目-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope> <!--私有 不影响引用项目-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<scope>provided</scope> <!--私有 不影响引用项目-->
</dependency>
</dependencies>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sample-framework</artifactId>
<groupId>com.su</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sample-trace-boot-starter</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
package com.sample.trace.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
//加载过滤器需要的注解
@ServletComponentScan(basePackages = "com.sample.trace")
@ComponentScan(basePackages = "com.sample.trace")
public class TraceAutoConfiguration {
public TraceAutoConfiguration() {
log.info("TraceAutoConfiguration---------create ok");
}
}
package com.sample.trace.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author 邓健
* @version 1.0
* @date 2020/9/8 10:22
*/
@Component(value = "traceProperties")
@ConfigurationProperties(prefix = "zohe.trace")
public class TraceProperties {
/**
* 服务名称
*/
private String serverName="NO_NAME";
public String getServerName() {
return serverName;
}
public void setServerName(String serverName) {
this.serverName = serverName;
}
}
package com.sample.trace.bean;
import lombok.Data;
/**
* MDC参数类
*
* @author su
* @version 1.0
* @date 2022/4/11 10:21
*/
@Data
public class MDCParam {
/**
* traceId
*/
private String traceId;
/**
* 上层服务名称
*/
private String upServer;
/**
* 当前服务名称
*/
private String serverName;
}
package com.sample.trace.consts;
/**
* trace 常量
*/
public interface TraceConstants {
/**
* traceId
*/
String TRACE_ID_KEY = "traceId";
/**
* 上级服务名称
*/
String UP_SERVER = "upServer";
/**
* 当前服务名称
*/
String SERVER_NAME = "serverName";
}
package com.sample.trace.utils;
import cn.hutool.core.bean.BeanUtil;
import com.sample.trace.bean.MDCParam;
import org.slf4j.MDC;
import java.util.Map;
public class MDCUtil {
public static void put(MDCParam param) {
Map<String, Object> map = BeanUtil.beanToMap(param);
TraceLocalUtil.setMap(map);
//MDC.setContextMap(traceMap); 源码是,会存在覆盖问题 传入的Map 替换 原来MDC的中的map 决定使用put
for (String key : map.keySet()) {
if (map.get(key) != null) {
MDC.put(key, map.get(key).toString());
}
}
}
public static void clear() {
MDC.clear();
TraceLocalUtil.clear();
}
}
package com.sample.trace.utils;
import java.util.Map;
/**
* ThreadLocal 存储 TraceId
*/
public class TraceLocalUtil {
private static final ThreadLocal<Map<String, Object>> traceIdTreadLocal = new ThreadLocal<>();
public static void setMap(Map<String, Object> traceMap) {
traceIdTreadLocal.set(traceMap);
}
public static Map<String, Object> getMap() {
return traceIdTreadLocal.get();
}
public static void clear() {
traceIdTreadLocal.remove();
}
}
RabbitMQ使用AOP 的方式 接受 MDC插入traceId
package com.sample.trace.aop;
import com.sample.trace.bean.MDCParam;
import com.sample.trace.config.TraceProperties;
import com.sample.trace.utils.MDCUtil;
import com.sample.trace.utils.TraceLocalUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Map;
import java.util.UUID;
/**
* RabbitMQ traceId 生成
*/
@Aspect
@Component
@Order(0)
public class RabbitTraceAspect {
@Resource
private TraceProperties traceProperties;
@Pointcut("@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener)")
public void pointcut() {
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
try {
Map<String, Object> localMap = TraceLocalUtil.getMap();
if (localMap == null) {
MDCUtil.clear();
MDCParam param = new MDCParam();
param.setTraceId(UUID.randomUUID().toString());
param.setServerName(traceProperties.getServerName());
//TODO upServer
param.setUpServer(traceProperties.getServerName());
MDCUtil.put(param);
}
} catch (Exception e) {
e.printStackTrace();
}
Object result = joinPoint.proceed();
// 清除TreadLocal MDC
MDCUtil.clear();
return result;
}
}
http 请求 使用过滤器 来 插入 traceId
package com.sample.trace.filter;
import com.sample.trace.bean.MDCParam;
import com.sample.trace.config.TraceProperties;
import com.sample.trace.consts.TraceConstants;
import com.sample.trace.utils.MDCUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.*;
import javax.servlet.FilterConfig;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class HttpTraceIdFilter implements Filter {
public HttpTraceIdFilter() {
log.info("HttpTraceIdFilter---------create ok");
}
private TraceProperties traceProperties;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//tomcat 容器启动 springboot 项目 关于servlet 的配置都会失效
//filter 也是servlet 也是
if (traceProperties == null) {
WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(filterConfig.getServletContext());
traceProperties = applicationContext.getBean(TraceProperties.class);
}
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
//先清除本地缓存
try {
MDCUtil.clear();
MDCParam param = new MDCParam();
String traceId = httpRequest.getHeader(TraceConstants.TRACE_ID_KEY);
String upServer = httpRequest.getHeader(TraceConstants.UP_SERVER)
if (StringUtils.isBlank(traceId)) {
traceId = UUID.randomUUID().toString();
}
param.setTraceId(traceId);
if (StringUtils.isNotBlank(upServer)) {
param.setUpServer(upServer);
}
param.setServerName(traceProperties.getServerName());
MDCUtil.put(param);
} catch (Exception e) {
e.printStackTrace();
}
chain.doFilter(request, response);
}
}
这里使用OpenFegin 来作为交互组件 使用请求拦截器来传递 TraceId
package com.sample.trace.interce;
import com.sample.trace.consts.TraceConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class OpenFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String traceId = MDC.get(TraceConstants.TRACE_ID_KEY);
String serverName = MDC.get(TraceConstants.SERVER_NAME);
requestTemplate.header(TraceConstants.TRACE_ID_KEY, traceId);
requestTemplate.header(TraceConstants.UP_SERVER, serverName);
}
}
additional-spring-configuration-metadata.json
{
"properties": [
{
"name": "zohe.trace.serverName",
"type": "java.lang.String",
"defaultValue": "NO_NAME",
"description": "trace 当前服务名称"
}
]
}
spring.factories 自动装配
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sample.trace.config.TraceAutoConfiguration
spring-configuration-metadata.json
{
"groups": [
{
"name": "server",
"type": "org.springframework.boot.autoconfigure.web.ServerProperties",
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
}
],
"properties": [
{
"name": "zohe.trace.server-name",
"type": "java.lang.String",
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
}
],
"hints": [
{
"name": "spring.jpa.hibernate.ddl-auto",
"values": [
{
"value": "none",
"description": "Disable DDL handling."
},
{
"value": "validate",
"description": "Validate the schema, make no changes to the database."
}
]
}
]
}
测试项目需要修改 logback.xml 加上 traceId 输出的位置
这里就简单截图一下测试项目 需要项目的可以看底部 最下边gitee 自己拉
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<contextName>zohe-video-struct</contextName>
<property name="LOGPATH" value="/home/admin/logs"/>
<property name="APP_NAME" value="zohe-video-struct"/>
<property name="FILE_NAME" value="%d{yyyyMMdd}"/>
<property name="STDOUT_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%thread|%class|Line:%line|%X{upServer}|%X{traceId}| %msg%n"/>
<property name="PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%thread|%class|Line:%line|%X{traceId} %msg%n"/> <!-- 输出到控制台 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${STDOUT_PATTERN}</pattern>
</layout>
</appender>
<!-- 输出到文件 -->
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOGPATH}/${APP_NAME}/${APP_NAME}.debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.debug.bak</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder>
<pattern>${PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印DEBUG日志 -->
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOGPATH}/${APP_NAME}/${APP_NAME}.info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.info.bak</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder>
<pattern>${PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印INFO日志 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOGPATH}/${APP_NAME}/${APP_NAME}.error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.error.bak</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder>
<pattern>${PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印ERROR日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOGPATH}/${APP_NAME}/${APP_NAME}.warn.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.warn.bak</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder>
<pattern>${PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印WARN日志 -->
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="info"/>
</appender>
<!-- 指定日志级别 -->
<root level="INFO">
<appender-ref ref="stdout" />
<!-- 本地测试时如果不用输出到文件,可以注释掉 -->
<appender-ref ref="debug" />
<appender-ref ref="info" />
<appender-ref ref="warn" />
<appender-ref ref="error" />
</root>
</configuration>
项目gitee 地址: https://gitee.com/DianHaiShiYuDeMing/sample-framework
需要注意的事:
有需要改进的地方请留言评论。共同学习,共同进步。