为什么要有日志的链路追踪?
每一次调用, 都打一个标签:traceId. 这个traceId贯穿整个调用过程. 如果出错. 可以通过这个traceId获取整个调用链路的日志. 帮助快速定位错误信息;
本次博客场景不是使用微服务架构, 而是简单的springboot项目. 通过http, oncall系统调用provide系统
默认你有springboot基础.
调用流程:
浏览器–>oncall系统–>provide系统.
首先看oncall系统.
先写一个过滤器, 对每次调用都添加一个traceId
package com.example.traceoncall.filter;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
/**
* 添加过滤器 将请求绑定 traceId
*
* @author: Lenovo
* @date: 2021/8/4
* @description:
*/
@Component
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
try {
fetchTraceId(httpServletRequest);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} finally {
MDC.clear();
}
}
/**
* 填充traceId
* @param httpServletRequest
*/
private void fetchTraceId(HttpServletRequest httpServletRequest) {
String traceId =
Optional.ofNullable(httpServletRequest).map(httpServletRequest1 -> httpServletRequest1.getHeader(CommonConstant.LOG_TRACE_ID)).orElse("");
if (StrUtil.isBlank(traceId)) {
traceId = IdUtil.fastSimpleUUID();
}
MDC.put(CommonConstant.LOG_TRACE_ID, traceId);
}
}
其中 MDC 本质是一个ThreadLocal,线程局部副本. 这个MDC可以在logback.xml中使用. 下面看一下logback.xml部分细节
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="Info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
上面有一个 [%X{traceId}] 就是我们之前在filter中设置进去的.
在oncall的业务方法中. http调用provide系统时. 构建request时需要把traceId 放到Header中;
String traceId = MDC.get(CommonConstant.LOG_TRACE_ID);
HttpRequest httpRequest = HttpRequest.post("localhost:8082/trace/provide");
if (StrUtil.isNotBlank(traceId)) {
httpRequest.header(CommonConstant.LOG_TRACE_ID, traceId);
}
再写一个Advice 统一处理返回值.
package com.example.traceoncall.filter;
import com.example.traceoncall.result.ResultJson;
import org.slf4j.MDC;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 处理response统一返回的
*
* @author: Lenovo
* @date: 2021/8/5
* @description:
*/
@ControllerAdvice
public class MyControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<?
extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
if (body instanceof ResultJson) {
ResultJson data = (ResultJson) body;
data.setTraceId(getTraceId());
return data;
}
return body;
}
private String getTraceId() {
return MDC.get(CommonConstant.LOG_TRACE_ID);
}
}
这时候就已经完成oncall.
provide系统中
写一个跟oncall里一样的过滤器;就ok了. 不用其他的配置;
普通的调用就已经完成了;
下面说一下线程之间的传递.
- 使用@Async注解的;
需要我们自己配置下 线程池
package com.example.traceoncall.threadpool;
/**
* @author: Lenovo
* @date: 2021/8/4
* @description:
*/
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* @description: 执行异步任务线程池
* @author LinYa
*/
@Slf4j
@Configuration
public class AsyncConfiguration implements AsyncConfigurer {
@Bean("kingAsyncExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
int corePoolSize = 10;
executor.setCorePoolSize(corePoolSize);
int maxPoolSize = 50;
executor.setMaxPoolSize(maxPoolSize);
int queueCapacity = 10;
executor.setQueueCapacity(queueCapacity);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
String threadNamePrefix = "Async-Executor-";
executor.setThreadNamePrefix(threadNamePrefix);
executor.setTaskDecorator(new MDCTaskDecorator());
executor.setWaitForTasksToCompleteOnShutdown(true);
// 使用自定义的跨线程的请求级别线程工厂类19
int awaitTerminationSeconds = 60;
executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
executor.initialize();
return executor;
}
@Override
public Executor getAsyncExecutor() {
return executor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> log.error(String.format("执行异步任务'%s'", method), ex);
}
}
上面最主要的是executor.setTaskDecorator(new MDCTaskDecorator()); 给线程池设置一个装饰器
具体的装饰器代码:
package com.example.traceoncall.threadpool;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author: Lenovo
* @date: 2021/8/4
* @description:
*/
@Component
public class MDCTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return new Runnable() {
@Override
public void run() {
try {
MDC.setContextMap(contextMap);
runnable.run();
} finally {
MDC.clear();
}
}
};
}
}
上面主要作用是,将父线程的mdc参数 传递当子线程中去;
使用@Async时:
@Async("kingAsyncExecutor")
public void test() {
Thread thread = Thread.currentThread();
log.info("threadName===="+thread.getName());
log.info("=======Async========");
}
- 使用自定义线程池时.
int coreSize = threadNormalEnum.getCoreSize();
int maxSize = threadNormalEnum.getMaxSize();
int queueSize = threadNormalEnum.getQueueSize();
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(coreSize);
threadPoolTaskExecutor.setMaxPoolSize(maxSize);
threadPoolTaskExecutor.setQueueCapacity(queueSize);
threadPoolTaskExecutor.setThreadFactory(new NamedThreadFactory(ThreadNormalEnum.NORMAL_THREAD.name(), false));
threadPoolTaskExecutor.setTaskDecorator(new MDCTaskDecorator());
threadPoolTaskExecutor.initialize();
ThreadPoolExecutor threadPoolExecutor1 = threadPoolTaskExecutor.getThreadPoolExecutor();
需要先使用spring的ThreadPoolTaskExecutor. 然后再获取ThreadPoolExecutor;
主要还是之前的那个MDCTaskDecorator适配器.
这样再使用这个线程池执行任务时, 就可以传递线程了;
结果展示
oncall中log日志如下:
provide中日志如下:
线程池调用,看日志中线程名称