一、請求鏈路追蹤是什么?
能標識一次請求的完整流程,包括日志打印、響應標識等,以便於出現問題可以快速定位並解決問題。
二、使用步驟
1. 相關知識點
- ThreadLocal:一種保證一種規避多線程訪問出現線程不安全的方法,當我們在創建一個變量后,如果每個線程對其進行訪問的時候訪問的都是線程自己的變量這樣就不會存在線程不安全問題。
- MDC:(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能,基於ThreadLocal實現的一種工具類。
- 攔截器:基於攔截器對每個請求注入traceId。
2. 代碼實現
- 封裝TraceId工具類:
/** * @author yinfeng * @description traceId工具類 * @since 2021/10/2 11:10 */
public class TraceIdUtil {
private static final String TRACE_ID = "traceId";
public static void set() {
MDC.put(TRACE_ID, generate());
}
public static String get() {
return MDC.get(TRACE_ID);
}
public static void remove() {
MDC.remove(TRACE_ID);
}
public static String generate() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}
- springboot環境注入工具類
/** * @author yinfeng * @description 資源配置工具類 * @since 2021/10/2 0:02 */
public class PropertySourcesUtil {
private static final String NAME = "aop.yinfeng";
private static ConfigurableEnvironment environment;
private static SpringApplication application;
public static void setEnvironment(ConfigurableEnvironment environment) {
if (PropertySourcesUtil.environment == null) {
PropertySourcesUtil.environment = environment;
}
}
public static SpringApplication getApplication() {
return application;
}
public static void setApplication(SpringApplication application) {
PropertySourcesUtil.application = application;
}
public static void set(String key, Object value) {
getSourceMap().put(key, value);
}
public static Object get(String key) {
return getSourceMap().get(key);
}
public static Map<String, Object> getSourceMap() {
PropertySource<?> propertySource = environment.getPropertySources().get(NAME);
Map<String, Object> source;
if (propertySource == null) {
source = new LinkedHashMap<String, Object>();
propertySource = new MapPropertySource(NAME, source);
environment.getPropertySources().addLast(propertySource);
}
source = (Map<String, Object>) propertySource.getSource();
return source;
}
}
- 支持配置的日志實體類:
/** * @author yinfeng * @description 日志配置類 * @since 2021/10/1 17:45 */
@Data
@ConfigurationProperties(prefix = "aop.logging")
public class LogProperties {
private String logDir;
// 因為logback和log4j的日志格式略有不同,所以提供2種打印格式
private String logbackPattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} %X{traceId} %-5level %logger{30} : %msg%n";
private String log4jPattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} %X{traceId} %-5level %clr{%-30.30c{1.}}{cyan} : %msg%n";
}
- 環境增強注入配置:因為請求鏈路追蹤在各個服務中比較常用,所以以starter的形式進行封裝,在spring環境加載后進行配置注入。
/** * @author yinfeng * @description 環境注入抽象類 * @since 2021/10/1 17:55 */
public abstract class AbstractEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final String DEV = "dev";
private static final String STG = "stg";
private static final String PRD = "prod";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
PropertySourcesUtil.setEnvironment(environment);
final List<String> profiles = Arrays.asList(environment.getActiveProfiles());
if (profiles.contains(PRD)) {
doPrd(environment, application);
} else if (profiles.contains(STG)) {
doStg(environment, application);
} else {
doDev(environment, application);
}
onProfile(environment, application);
}
protected void doPrd(ConfigurableEnvironment environment, SpringApplication application) {
}
protected void doStg(ConfigurableEnvironment environment, SpringApplication application) {
}
protected void doDev(ConfigurableEnvironment environment, SpringApplication application) {
}
protected void onProfile(ConfigurableEnvironment environment, SpringApplication application) {
}
}
/** * @author yinfeng * @description 日志環境注入 * @since 2021/10/1 17:52 */
@EnableConfigurationProperties(LogProperties.class)
public class LogEnvAdvice extends AbstractEnvironmentPostProcessor {
@Override
protected void onProfile(ConfigurableEnvironment environment, SpringApplication application) {
final Binder binder = Binder.get(environment);
final BindResult<LogProperties> bindResult = binder.bind("aop.logging", Bindable.of(LogProperties.class));
LogProperties logProperties = new LogProperties();
if (bindResult.isBound()) {
logProperties = bindResult.get();
}
// 配置日志打印格式
if (isLogback(application)) {
PropertySourcesUtil.set("logging.pattern.console", logProperties.getLogbackPattern());
PropertySourcesUtil.set("logging.pattern.file", logProperties.getLogbackPattern());
return;
}
PropertySourcesUtil.set("logging.pattern.console", logProperties.getLog4jPattern());
PropertySourcesUtil.set("logging.pattern.file", logProperties.getLog4jPattern());
}
/** * 判斷是否是logback日志格式 * * @param application application * @return */
private boolean isLogback(SpringApplication application) {
final LoggingSystem loggingSystem = LoggingSystem.get(application.getClassLoader());
return LogbackLoggingSystem.class.equals(loggingSystem.getClass());
}
}
- 在spring.factory文件配置log環境注入類
org.springframework.boot.env.EnvironmentPostProcessor=com.yinfeng.common.enviroment.LogEnvAdvice
- 配置攔截器,在每個請求進入時注入traceId,因為基於threadLocal實現,所以需要在請求完成后進行手動清除,否則gc會掃描不到
/** * @author yinfeng * @description 日志攔截器 * @since 2021/10/2 11:09 */
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
TraceIdUtil.set();
return true;
}
/** * 回收資源,防止oom * @param request * @param response * @param handler * @param ex * @throws Exception */
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
TraceIdUtil.remove();
}
}
/** * @author yinfeng * @description 攔截器增強 * @since 2021/10/2 11:15 */
public class InterceptorAdvice implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 將攔截器注入到容器中
final InterceptorRegistration registration = registry.addInterceptor(new LogInterceptor()).order(Integer.MIN_VALUE);
registration.addPathPatterns("/**");
}
}
3. 測試一下效果
到此為止,通過traceId追蹤請求鏈路代碼基本完成,下面咱們來測認識一下
- 在pom文件中引入咱們的starter
<dependency>
<groupId>com.yinfeng</groupId>
<artifactId>common-starter</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
</exclusions>
</dependency>
- 通過knife4j接口文檔發送請求
- 查看日志:可以看到咱們所有的業務日志打印都會帶上traceId,方便咱們快速定位問題