MDC轻量化日志链路跟踪的若干种应用场景 天天实时
0x00 大纲目录0x00 大纲0x01 前言0x02 应用场景CLI 程序Web 应用(服务端)Web 应用(客户端)OkHttp 同步请求OkHttp 异步请求Spring WebClientDubbo 服务线程池定时任务0x03 小结0x04 附录0x01 前言"If debugging is the process of removing software bugs, then programming must be the process of putting them in." - Edsger Dijkstra
“如果调试是消除软件Bug的过程,那么编程就是产出Bug的过程。” —— 艾兹格·迪杰斯特拉
【资料图】
当你的应用程序同时处理多个用户的请求时,你会看到日志文件或者控制台中的输出通常都是交错的,而非线性连续的。尤其是在分布式系统中,一个用户请求可能包含了若干次的服务节点调用,它的日志也因此变得碎片化,如果缺乏一个用于归类和关联的标识,就会导致这笔交易难以被跟踪和追查。
MDC(Mapped Diagnostic Context)是一种用于区分来自不同来源日志的工具。它的本质是一个由日志框架维护的Map
存储结构,应用程序可以向其中写入键值对,并被日志框架访问。我们常用的日志门面SLF4J就对MDC的实现进行了抽象,由日志框架提供真正的实现。在SLF4J的文档中写道:
This class hides and serves as a substitute for the underlying logging system"s MDC implementation.
If the underlying logging system offers MDC functionality, then SLF4J"s MDC, i.e. this class, will delegate to the underlying system"s MDC. Note that at this time, only two logging systems, namely log4j and logback, offer MDC functionality. For java.util.logging which does not support MDC, BasicMDCAdapter will be used. For other systems, i.e. slf4j-simple and slf4j-nop, NOPMDCAdapter will be used.
Thus, as a SLF4J user, you can take advantage of MDC in the presence of log4j, logback, or java.util.logging, but without forcing these systems as dependencies upon your users.
目前为止只有logback和log4j(log4j2)提供了较为完备的实现,其余日志框架下会使用SLF4J内部实现的BasicMDCAdapter
或者NOPMDCAdapter
.
以logback为例,我们创建一个简单的logback.xml
配置文件:
${log.pattern}
一个简单的类用于测试,我们用一个循环来模拟用户两个独立的请求:
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import java.util.UUID;public class Main { private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { for (int i = 0; i < 2; i++) { try { LOGGER.info("Empty MDC Before Putting Data."); MDC.put("traceId", UUID.randomUUID().toString()); LOGGER.info("Hello MDC."); LOGGER.info("GoodBye MDC."); throw new RuntimeException("Test Exception"); } catch (RuntimeException e) { LOGGER.error("Test MDC", e); } finally { MDC.clear(); LOGGER.info("Empty MDC After Clearing Data."); } } }}
运行之后,我们会得到类似这样的日志输出:
[main][INFO][com.example.Main][-] Empty MDC Before Putting Data.[main][INFO][com.example.Main][9ed7cc12-3880-4a38-94d4-b7ba96f37234] Hello MDC.[main][INFO][com.example.Main][9ed7cc12-3880-4a38-94d4-b7ba96f37234] GoodBye MDC.[main][ERROR][com.example.Main][9ed7cc12-3880-4a38-94d4-b7ba96f37234] Test MDCjava.lang.RuntimeException: Test Exceptionat com.example.Main.main(Main.java:19)[main][INFO][com.example.Main][-] Empty MDC After Clearing Data.[main][INFO][com.example.Main][-] Empty MDC Before Putting Data.[main][INFO][com.example.Main][ab94804a-4f9a-4474-ba23-98542884d0ea] Hello MDC.[main][INFO][com.example.Main][ab94804a-4f9a-4474-ba23-98542884d0ea] GoodBye MDC.[main][ERROR][com.example.Main][ab94804a-4f9a-4474-ba23-98542884d0ea] Test MDCjava.lang.RuntimeException: Test Exceptionat com.example.Main.main(Main.java:19)[main][INFO][com.example.Main][-] Empty MDC After Clearing Data.
可以看到,两次请求的traceId
是不一样的,这样就能在日志中将它们区分和识别开来。通常来说,最好在请求完成后对MDC中的数据进行清理,尤其是使用了线程池的情况,由于线程是复用的,除非对原来的键值进行了覆盖,否则它将保留上一次的值。
在CLI程序中,我们可以用上面的写法来设置traceId
,当时对于 Web 应用,由于Controller
入口众多,不可能每个控制器都这样子写,可以使用拦截器实现公共逻辑,避免对Controller
的方法造成污染。先增加一个简单的Controller
,它有两个请求处理方法,一个同步,一个异步:
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.context.annotation.Configuration;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.Map;import java.util.concurrent.Callable;@RestControllerpublic class MDCController { private static final Logger LOGGER = LoggerFactory.getLogger(MDCController.class); @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**"); } } @GetMapping("/syncMDC") public String mdcSync() { LOGGER.info("sync MDC test."); return "syncMDC"; } @GetMapping("/asyncMDC") public Callable mdcAsync() { LOGGER.info("async MDC test."); Map mdcMap = MDC.getCopyOfContextMap(); return () -> { try { MDC.setContextMap(mdcMap); LOGGER.info("异步业务逻辑处理"); return "asyncMDC"; } finally { MDC.clear(); } }; }}
然后是关键的MDC拦截器:
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.web.servlet.AsyncHandlerInterceptor;import javax.servlet.DispatcherType;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.UUID;/** * 日志增强拦截器,给输出日志加上链路跟踪号 * * @author YanFaBu **/public class LogInterceptor implements AsyncHandlerInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (request.getDispatcherType() != DispatcherType.REQUEST) { // 非 DispatcherType.REQUEST 分发类型,尝试从 Attribute 获取 LOG_TRACE_ID MDC.put("traceId", (String) request.getAttribute("traceId")); LOGGER.info("preHandle Non DispatcherType.REQUEST type with DispatcherType {}", request.getDispatcherType()); return true; } // 如果本次调用来自上游服务,那么尝试从请求头获取上游传递的 traceId String traceId = request.getHeader("traceId"); if (traceId == null) { // 本服务节点是起始服务节点,设置 traceId traceId = UUID.randomUUID().toString(); } MDC.put("traceId", traceId); // 异步处理会在内部进行 Request 转发,通过 Attribute 携带 traceId request.setAttribute("traceId", traceId); LOGGER.info("preHandle DispatcherType.REQUEST type with DispatcherType {}", request.getDispatcherType()); return true; } @Override public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 清理 MDC LOGGER.info("afterConcurrentHandlingStarted Clearing MDC."); MDC.clear(); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) { // 清理 MDC LOGGER.info("afterCompletion Clearing MDC with DispatcherType {}", request.getDispatcherType()); MDC.clear(); }}
分别访问这两个Controller
方法,应当看到类似这样的日志输出:
[http-nio-8080-exec-7][INFO][com.example.LogInterceptor][e828f77b-9c0d-42c5-83db-15f19153bf19] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST[http-nio-8080-exec-7][INFO][com.example.MDCController][e828f77b-9c0d-42c5-83db-15f19153bf19] sync MDC test.[http-nio-8080-exec-7][INFO][com.example.LogInterceptor][e828f77b-9c0d-42c5-83db-15f19153bf19] afterCompletion Clearing MDC with DispatcherType REQUEST[http-nio-8080-exec-8][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST[http-nio-8080-exec-8][INFO][com.example.MDCController][7dc0878c-c014-44de-97d4-92108873a030] async MDC test.[http-nio-8080-exec-8][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] afterConcurrentHandlingStarted Clearing MDC.[task-3][INFO][com.example.MDCController][7dc0878c-c014-44de-97d4-92108873a030] 异步业务逻辑处理[http-nio-8080-exec-9][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] preHandle Non DispatcherType.REQUEST type with DispatcherType ASYNC[http-nio-8080-exec-9][INFO][com.example.LogInterceptor][7dc0878c-c014-44de-97d4-92108873a030] afterCompletion Clearing MDC with DispatcherType ASYNC
注意到异步请求处理中的线程号的变化,请求受理-业务处理-请求应答历经了3个不同的线程,凡是跨线程的处理逻辑,必然需要对MDC的传递进行处理,否则链路跟踪会丢失。网上看到过很多例子,都忽略了对DispatcherType
的处理,这样就会导致异步请求中,有一部分日志会失去追踪,导致最终排查问题时链路不完整。通过Attribute
传递不是唯一的方式,也可以借助其他上下文来传递。
import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.Response;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import java.io.IOException;import java.util.Objects;import java.util.UUID;public class Client { private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class); public static void main(String[] args) throws IOException { okHttpSync(); } public static void okHttpSync() throws IOException { try { String traceId = UUID.randomUUID().toString(); MDC.put("traceId", traceId); LOGGER.info("okHttpSync request syncMDC"); OkHttpClient client = new OkHttpClient().newBuilder() .build(); Request request = new Request.Builder() .url("http://localhost:8080/syncMDC") .method("GET", null) .addHeader("traceId", traceId) .build(); try (Response response = client.newCall(request).execute()) { LOGGER.info("okHttpSync response:{}", Objects.requireNonNull(response.body()).string()); } } finally { MDC.clear(); } }}
OkHttp 异步请求import okhttp3.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import java.io.IOException;import java.util.Map;import java.util.Objects;import java.util.UUID;public class Client { private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class); public static void main(String[] args) { okHttpAsync(); } public static void okHttpAsync() { try { String traceId = UUID.randomUUID().toString(); MDC.put("traceId", traceId); LOGGER.info("okHttpAsync request syncMDC"); OkHttpClient client = new OkHttpClient().newBuilder() .build(); Request request = new Request.Builder() .url("http://localhost:8080/syncMDC") .method("GET", null) .addHeader("traceId", traceId) .build(); Map mdcMap = MDC.getCopyOfContextMap(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { try { MDC.setContextMap(mdcMap); LOGGER.error("okHttpAsync error", e); } finally { MDC.clear(); } } @Override public void onResponse(Call call, Response response) throws IOException { try { MDC.setContextMap(mdcMap); LOGGER.info("okHttpAsync response:{}", Objects.requireNonNull(response.body()).string()); } finally { MDC.clear(); } } }); } finally { MDC.clear(); } }}
顺利的话,在客户端应该会得到类似下面的日志输出(注意线程名称的变化):
[main][INFO][com.example.Client][53924455-0fcd-442b-a5aa-aaa33005d299] okHttpSync request syncMDC[main][INFO][com.example.Client][53924455-0fcd-442b-a5aa-aaa33005d299] okHttpSync response:syncMDC[main][INFO][com.example.Client][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] okHttpAsync request syncMDC[OkHttp http://localhost:8080/...][INFO][com.example.Client][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] okHttpAsync response:syncMDC
在服务端对应的日志如下,可以看到traceId
是一致的(如果不一致或者没有看到traceId
,应该检查下上一章提到的拦截器是否有被正确实现):
[http-nio-8080-exec-2][INFO][com.example.LogInterceptor][53924455-0fcd-442b-a5aa-aaa33005d299] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST[http-nio-8080-exec-2][INFO][com.example.MDCController][53924455-0fcd-442b-a5aa-aaa33005d299] sync MDC test.[http-nio-8080-exec-2][INFO][com.example.LogInterceptor][53924455-0fcd-442b-a5aa-aaa33005d299] afterCompletion Clearing MDC with DispatcherType REQUEST[http-nio-8080-exec-3][INFO][com.example.LogInterceptor][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST[http-nio-8080-exec-3][INFO][com.example.MDCController][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] sync MDC test.[http-nio-8080-exec-3][INFO][com.example.LogInterceptor][5cb52293-c8ac-4bc5-87fc-dbeb1e727eba] afterCompletion Clearing MDC with DispatcherType REQUEST
处理思路都是通过HTTP Header携带traceId
到下游服务,让下游服务可以跟踪来源。注意异步请求时,请求处理和应答处理回调线程不在同一个线程,需要对MDC的传递进行处理,否则链路跟踪会丢失。其他的客户端,如HttpClient、Unirest等 HTTP 请求库原理与之相似,这里就不一一列举了。
与OkHttp异步调用类似,注意要在Mono
或者Flux
的subscribe
方法中传递MDC上下文。其实WebClient中有Context
传递的概念,但是这块资料比较少,异步非阻塞的代码又看得头痛,暂时不想去研究了。下面的代码出于演示目的使用,请勿直接使用:
import okhttp3.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.web.reactive.function.client.WebClient;import java.io.IOException;import java.util.Map;import java.util.Objects;import java.util.UUID;import java.util.concurrent.CountDownLatch;public class Client { private static final Logger LOGGER = LoggerFactory.getLogger(LogInterceptor.class); public static void main(String[] args) throws InterruptedException { webClient(); } public static void webClient() throws InterruptedException { String traceId = UUID.randomUUID().toString(); MDC.put("traceId", traceId); LOGGER.info("webClient request syncMDC"); WebClient client = WebClient.create("http://localhost:8080/syncMDC"); Map mdcMap = MDC.getCopyOfContextMap(); CountDownLatch latch = new CountDownLatch(1); client.get() .uri("/") .retrieve() .bodyToMono(String.class) .subscribe(result -> { try { MDC.setContextMap(mdcMap); LOGGER.info("webClient response:{}", result); } finally { MDC.clear(); latch.countDown(); } }, throwable -> { try { MDC.setContextMap(mdcMap); LOGGER.error("webClient error", throwable); } finally { MDC.clear(); } }); latch.await(); }}
输出日志如下,注意线程的变化:
[main][INFO][com.example.Client][8c984fa8-e3cd-4914-875e-ba333d31c7a9] webClient request syncMDC[reactor-http-nio-2][INFO][com.example.Client][8c984fa8-e3cd-4914-875e-ba333d31c7a9] webClient response:syncMDC
Dubbo 服务与 HTTP 调用类似,基于Dubbo的 RPC 调用也是可以跟踪的,利用Dubbo的Filter
和SPI注册机制,我们可以增加自己的过滤器实现日志链路跟踪:
import org.apache.dubbo.common.extension.Activate;import org.apache.dubbo.rpc.*;import org.slf4j.MDC;import java.util.UUID;/** * 服务链路跟踪过滤器 */@Activatepublic class RpcTraceFilter implements Filter { @Override public Result invoke(Invoker> invoker, Invocation invocation) throws RpcException { RpcContext context = RpcContext.getContext(); boolean shouldRemove = false; if (context.isProviderSide()) { // 获取消费端设置的参数 String traceId = context.getAttachment("traceId"); if (traceId == null || traceId.isEmpty()) { traceId = MDC.get("traceId"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString(); shouldRemove = true; } } // 设置 traceId MDC.put("traceId", traceId); // 继续设置下游参数,供在提供方里面作为消费端时,其他服务提供方使用这些参数 context.setAttachment("traceId", traceId); } else if (context.isConsumerSide()) { // 如果连续调用多个服务,则会使用同个线程里之前设置的traceId String traceId = MDC.get("traceId"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString(); // 设置 traceId MDC.put("traceId", traceId); shouldRemove = true; } // 设置传递到提供端的参数 context.setAttachment("traceId", traceId); } try { return invoker.invoke(invocation); } finally { // 调用完成后移除MDC属性 if (shouldRemove) { MDC.clear(); } } }}
在需要用到的服务模块的resource/META-INF/dubbo/org.apache.dubbo.rpc.Filter
文件中注册过滤器(注意路径和名称不能错):
rpcTraceFilter=com.example.RpcTraceFilter
SpringBoot的application.properties
中增加配置(为了简单验证,这里没有使用注册中心。如果你想更严谨地测试,建议在本地启动两个独立的工程,并使用ZooKeeper进行服务注册):
dubbo.application.name=MDCExampledubbo.scan.base-packages=com.exampledubbo.registry.address=N/A# dubbo filterdubbo.consumer.filter=rpcTraceFilterdubbo.provider.filter=rpcTraceFilter
增加一个简单的Dubbo服务:
import org.apache.dubbo.config.annotation.DubboService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;@DubboServicepublic class RpcService implements IRpcService { private static final Logger LOGGER = LoggerFactory.getLogger(RpcService.class); public String mdcRpc() { LOGGER.info("Calling RPC service."); return "mdcRpc"; }}
在Controller
中增加一个方法,进行验证:
import org.apache.dubbo.config.annotation.DubboReference;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.context.annotation.Configuration;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.Map;import java.util.concurrent.Callable;@RestControllerpublic class MDCController { // ......(省略前面的代码) @DubboReference private IRpcService rpcService; @GetMapping("/mdcRpc") public String mdcRpc() { LOGGER.info("rpc MDC test."); return rpcService.mdcRpc(); }}
访问Controller
方法,应该能得到类似下面的输出:
[http-nio-8080-exec-1][INFO][com.example.LogInterceptor][f003f750-2044-41ae-a041-8a76eb0c415b] preHandle DispatcherType.REQUEST type with DispatcherType REQUEST[http-nio-8080-exec-1][INFO][com.example.MDCController][f003f750-2044-41ae-a041-8a76eb0c415b] rpc MDC test.[http-nio-8080-exec-1][INFO][com.example.RpcService][f003f750-2044-41ae-a041-8a76eb0c415b] Calling RPC service.[http-nio-8080-exec-1][INFO][com.example.LogInterceptor][f003f750-2044-41ae-a041-8a76eb0c415b] afterCompletion Clearing MDC with DispatcherType REQUEST
线程池前面提到过跨线程调用时,需要自己处理MDC上下文的传递,如果是单个线程,可以手工进行处理,但如果是线程池,似乎就不能这么干了。线程池种类繁多,处理方式也有细微差别,这里不可能全部列举,以Spring项目中常用的 ThreadPoolTaskExecutor
为例,我们可以利用它提供的setTaskDecorator
方法对任务进行装饰:
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.context.annotation.Bean;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.Map;import java.util.UUID;public class MDCExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(MDCExecutor.class); public static void main(String[] args) { MDC.put("traceId", UUID.randomUUID().toString()); ThreadPoolTaskExecutor executor = asyncTaskExecutor(); executor.initialize(); executor.submit(() -> LOGGER.info("MDC Executor")); executor.shutdown(); } @Bean public static ThreadPoolTaskExecutor asyncTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setTaskDecorator(task -> { Map mdcMap = MDC.getCopyOfContextMap(); return () -> { try { if (mdcMap != null) { MDC.setContextMap(mdcMap); } task.run(); } finally { MDC.clear(); } }; }); return executor; }}
对于其他线程池,通用的思路是覆写其submit
或者execute
方法来实现MDC传递,比如我们下面提到的定时任务调度线程池。
除了Controller
和 RPC 接口发起的调用,最常见的就是定时任务了。如果是定时任务作为业务发起源,可以在任务调度的时候对MDC进行处理。这块处理比较复杂,暂时没有找到比较优雅的切入点:
增加一个实现RunnableScheduledFuture
接口的DecoratedFuture
类:
import org.slf4j.MDC;import java.util.Map;import java.util.Optional;import java.util.UUID;import java.util.concurrent.*;class DecoratedFuture implements RunnableScheduledFuture { Runnable runnable; Callable callable; final RunnableScheduledFuture task; public DecoratedFuture(Runnable r, RunnableScheduledFuture task) { this.task = task; runnable = r; } public DecoratedFuture(Callable c, RunnableScheduledFuture task) { this.task = task; callable = c; } @Override public boolean isPeriodic() { return task.isPeriodic(); } @Override public void run() { try { Map mdcMap = MDC.getCopyOfContextMap(); Optional.ofNullable(mdcMap).ifPresent(MDC::setContextMap); String traceId = MDC.get("traceId"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString(); } MDC.put("traceId", traceId); task.run(); } finally { MDC.clear(); } } @Override public boolean cancel(boolean mayInterruptIfRunning) { return task.cancel(mayInterruptIfRunning); } @Override public boolean isCancelled() { return task.isCancelled(); } @Override public boolean isDone() { return task.isDone(); } @Override public V get() throws InterruptedException, ExecutionException { return task.get(); } @Override public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return task.get(timeout, unit); } @Override public long getDelay(TimeUnit unit) { return task.getDelay(unit); } @Override public int compareTo(Delayed o) { return task.compareTo(o); } @Override public int hashCode() { return task.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } DecoratedFuture> that = (DecoratedFuture>) o; return this.task.equals(that.task); } public Runnable getRunnable() { return runnable; } public RunnableScheduledFuture getTask() { return task; } public Callable getCallable() { return callable; }}
增加一个实现ThreadPoolTaskScheduler
接口的DecoratedThreadPoolTaskScheduler
类:
import org.slf4j.MDC;import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;import java.time.Duration;import java.time.Instant;import java.util.Map;import java.util.Optional;import java.util.concurrent.RejectedExecutionHandler;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.ScheduledFuture;import java.util.concurrent.ThreadFactory;class DecoratedThreadPoolTaskScheduler extends ThreadPoolTaskScheduler { private static final long serialVersionUID = 1L; static Runnable withTraceId(Runnable task) { Map mdcMap = MDC.getCopyOfContextMap(); return ()-> { try { Optional.ofNullable(mdcMap).ifPresent(MDC::setContextMap); task.run(); } finally { MDC.clear(); } }; } @Override protected ScheduledExecutorService createExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { return new DecoratedScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler); } @Override public ScheduledFuture> schedule(Runnable task, Instant startTime) { return super.schedule(withTraceId(task), startTime); } @Override public ScheduledFuture> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { return super.scheduleAtFixedRate(withTraceId(task), startTime, period); } @Override public ScheduledFuture> scheduleAtFixedRate(Runnable task, Duration period) { return super.scheduleAtFixedRate(withTraceId(task), period); } @Override public ScheduledFuture> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { return super.scheduleWithFixedDelay(withTraceId(task), startTime, delay); } @Override public ScheduledFuture> scheduleWithFixedDelay(Runnable task, Duration delay) { return super.scheduleWithFixedDelay(withTraceId(task), delay); }}
增加一个继承ScheduledThreadPoolExecutor
类的DecoratedScheduledThreadPoolExecutor
类,覆写它的两个decorateTask
方法:
import java.util.concurrent.*;class DecoratedScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { public DecoratedScheduledThreadPoolExecutor(int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) { super(poolSize, threadFactory, rejectedExecutionHandler); } @Override protected RunnableScheduledFuture decorateTask(Runnable runnable, RunnableScheduledFuture task) { return new DecoratedFuture<>(runnable, task); } @Override protected RunnableScheduledFuture decorateTask(Callable callable, RunnableScheduledFuture task) { return new DecoratedFuture<>(callable, task); }}
在定时任务Configuration
中,创建DecoratedThreadPoolTaskScheduler
作为调度线程池:
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.scheduling.annotation.EnableScheduling;import org.springframework.scheduling.annotation.SchedulingConfigurer;import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;import org.springframework.scheduling.config.ScheduledTaskRegistrar;/** * 定时调度配置 */@Configuration@EnableAsync@EnableSchedulingpublic class SchedulingConfiguration implements SchedulingConfigurer { public static final String TASK_SCHEDULER = "taskScheduler"; @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setTaskScheduler(taskScheduler()); } @Bean(TASK_SCHEDULER) public ThreadPoolTaskScheduler taskScheduler() { return new DecoratedThreadPoolTaskScheduler(); }}
添加一个简单定时任务:
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.Scheduled;@SpringBootApplicationpublic class App { private static final Logger LOGGER = LoggerFactory.getLogger(App.class); public static void main(String[] args) { SpringApplication.run(App.class, args); } @Scheduled(fixedDelay = 1500) public void cronTaskConfigRefresh() { LOGGER.info("MDC task scheduler."); }}
可以看到类似下面的输出,说明就成功了:
[taskScheduler-1][INFO][com.example.App][0959d1a6-4680-4a95-a29b-b62694f0d348] MDC task scheduler.[taskScheduler-1][INFO][com.example.App][8f034b1e-db40-44cb-9fc2-986eb8f0da6d] MDC task scheduler.[taskScheduler-1][INFO][com.example.App][02428e88-53f8-4151-aba0-86e069c96462] MDC task scheduler.[taskScheduler-1][INFO][com.example.App][fcd5d925-95e0-4e28-aa68-39e765668dde] MDC task scheduler.[taskScheduler-1][INFO][com.example.App][b8ed50c6-0d6d-40c0-b170-976717fe7d22] MDC task scheduler.[taskScheduler-1][INFO][com.example.App][9d173a26-41d4-43dc-beae-731a9f267288] MDC task scheduler.[taskScheduler-1][INFO][com.example.App][0257c93a-9bec-40b7-9447-5a938bd2ce5f] MDC task scheduler.
0x03 小结在实际项目中,通过灵活组合上面的若干种手段,就可以实现轻量化的日志链路跟踪,在大部分情况下基本上已经够用了,当然你也可以引入SkyWalking或ZipKin等探针框架,它们提供的功能也更全面更丰富。如何选择,需要根据具体项目自行权衡。
0x04 附录样例工程下载
标签:
- MDC轻量化日志链路跟踪的若干种应用场景 天天实时
- 当前速读:桐城八中老师_桐城八中
- 焦点讯息:暗夜男爵事件第几集_暗夜男爵
- 吃什么容易发胖长胸_吃什么容易发胖
- 文旅部公布2023年“四季村晚”主会场和示范展示点名单
- 小联赛重心:威廉二世实力强胜,奥斯顽强防守
- 装傻!恩比德谈踢裆:我不记得了 我只知道我们3-0了
- 降价后遗症? 特斯拉一季度交付创新高!净利却跌超20%,马斯克发声了 当前观察
- 快看:今年第 1 季度移动 AP 出货量报告:三星增长 15%、联发科下降31%|全球报道
- 区别对待?宝马MINI冰淇淋事件不能忍,去年在华销量占比近10%-当前关注
- MINI数字化萌宠亮相,成为第四种数字化体验主角
- 世界新资讯:短期外汇借款
- 当前信息:矫常
- Consul 的特点和优势 环球最新
- 微服务 - 拆分微服务的问题和拆分方法 每日看点
- 阳澄湖大闸蟹是哪的_阳澄湖大闸蟹产地介绍|环球快资讯
- 报道:伊朗海军称迫使美核潜艇浮出水面 美军否认:虚假信息
- 如何设置电脑上的默认输入法(在电脑里怎样设置默认输入法) 全球快播
- java split方法(java split)|讯息
- 新加坡房价贵吗(新加坡房价)
- 管理费用包括哪些内容(管理费用包括哪些明细科目)|天天信息
- A股共60只个股发生大宗交易 荣科科技溢价率14.35%居首
- 股票行情快报:浙江自然(605080)4月21日主力资金净卖出43.02万元|环球最新
- 反间谍法修订拟完善关于网络间谍行为的规定
- 天天简讯:机票、酒店、景区都很热!“五一”国内游预订全线超越2019年
- 承袭年度顶级旗舰 X11G,TCL QD-Mini LED 电视 C12G 发布 天天关注
- 全球今日报丨乌当区税务局开展领导带队走访企业活动
- 贵州红花岗经济开发区税务局:税收宣传进企业 代表委员话税收
- 一季度福建居民人均可支配收入同比增长4.4%_天天新视野
- 航拍贵州江凯河大桥 云雾缭绕美如画
- 工厂pmc的主要工作_工厂pmc工作职责
- 电科网安(002268):第七届第二十四次监事会会议决议,审议公司2022年度监事会工作报告等多项议案 环球热点
- 环球微资讯!电科网安(002268):2023年第一季度营业收入4.13亿元,与上期同比减少36.57%
- 特斯拉烈焰红 Model S 现身 视觉效果惊艳
- 本川智能:印制电路板技术在6G通信领域的应用尚未取得实质性进展|世界快看
- 全球微动态丨红塔证券(601236.SH):中国烟草总公司系公司实际控制人
- 当前观察:海兴电力最新公告:2022年度扣非净利润升355.49%至5.88亿元 拟10派7元
- 2023君乐宝保定马拉松赛事公告 君乐宝2021石家庄马拉松 全球消息
- 宝马 mini 被指区别对待中外访客,“慕洋”和“辱华”同样可耻
- 世界微头条丨美的系 A 股“狂奔”
- 鲍威尔本场已砍31分 创个人生涯季后赛新高
- 观速讯丨大学生创业创意点子文学类_大学生创业创意点子
- 环球百事通!菲沃泰(688371)盘中异动 股价振幅达6.78% 跌6.78% 报20.5元(04-21)
- 4月21日 13:15分 东方材料(603110)股价快速拉升 当前快报
- 股转是什么意思怎么交易_股转是什么意思
- 【环球时快讯】hp服务器内存插法图_hp服务器内存
- 一鸣食品(605179):4月21日技术指标出现观望信号-“黑三兵”|全球今日讯
- 时讯:双箭股份签订安徽池州过亿工程大单
- 智能机器人系统建模与仿真_对于智能机器人系统建模与仿真简单介绍|天天通讯
- 世界通讯!2023五一巩义康百万庄园游玩指南(演出+时间+亮点)
- 全球热议:2023年新密4月23日限行吗?限行尾号是多少?
- 如何制作烧烤菠萝蜜_世界快播
- 热讯:富比世2023台湾富豪榜富邦蔡明忠兄弟居首
- 辉瑞入局,国内PARP抑制剂赛道从“四雄”到“五子”
- 世界时讯:徐小明:周五操作策略
- 每日报道:大跌洗盘越凶狠,后面反弹大涨越厉害?如此潜伏是机会了
- 环球实时:经济复苏态势好,公募投资信心足
- 动态:兰德尔脚踝伤势恢复 已正常参加训练
- 著作权的权利范围-今头条
- 世界报道:监察机关留置措施由谁决定
- 高铁板块持续走强 多股涨超10%
- 刑事一般判多少年
- 以书为媒,西城党群服务中心里开了家图书漂流驿站 全球观点
- 百名记者百村百企行丨吉首夯坨村:茶树生“黄金”
- 今日热文:云涌科技:本次计提减值准备计入资产减值损失和信用减值损失科目,合计对公司2022年度合并利润总额影576.67万元
- 华秦科技股东户数下降3.02%,户均持股162.01万元 天天热闻
- 环球速讯:贵阳一地遭遇12级大风,村干部:大风吹翻鸡棚屋顶把人砸伤,冰雹还砸死了上万只鸡
- 山西暴雪致多地积雪超20厘米,局地降幅近30℃,市民:一天过四季,不知道怎么穿衣服|天天信息
- 天天滚动:WBBA总干事Martin Creaner分享全球宽带发展愿景
- 世界简讯:ChatGPT 威胁版权,42 家德国组织要求欧盟加强 AI 监管
- 每日热门:北上资金今日净买入宁德时代4.56亿元、中国中免3.18亿元
- 国家体育总局授权腾讯等组建杭州亚运会电竞国家集训队
- 梦网科技:接受睿远基金管理有限公司等机构调研_全球热闻
- 天天快看:齐心集团:2023年第一季度净利润约4455万元
- 齐心集团:2022年度净利润约1.27亿元 当前热点
- 最流行的室内墙面装修材料
- 雷神模拟器官方_雷神模拟器
- 商务部:今年以来中俄贸易保持良好发展势头_每日快讯
- 深圳最大本土房产中介乐有家App已同步显示业主真实报价-环球热点评
- 中油工程2022年度拟10派0.39元
- 完颜不破和瑶池圣母_完颜不破
- 4月21日杰华特跌13.64%,永赢稳健增长一年持有混合A基金重仓该股 世界速看料
- 手工立体书制作视频_手工立体书制作图解
- 如何防止鸡蛋粘在水煮锅中
- 内蒙古知识产权宣传周活动启动_天天观焦点
- 坦克马海利:坦克500Hi4-T满足用户更多场景需求
- 大国基理丨广西北海:用“一啖茶”的功夫化解纠纷 天天观察
- AI竞争白热化,谷歌再出大招!合并旗下DeepMind和谷歌大脑
- 英雄剑冢_关于英雄剑冢的简介|新要闻
- 英雄剑_关于英雄剑的简介 焦点观察
- 国泰君安(香港):维持中国联通“买入”评级 目标价8港元|天天报道
- 小学音体美测评工作总结(实用13篇)-全球速读
- 天天热推荐:早高峰紧急开道护送临产孕妇,你的背影,真帅!
- 洽洽食品:子公司拟3.5亿元规划新建生产线-今日热议
- 【独家焦点】中国REITs市场的探索和实践
- 环球今日讯!绿色种植引领高质量发展
- 黑龙江省公立医院种牙要降价了
- 探访中国(河北)安全应急博览会:“黑科技”支撑一线救援
- 江苏省锡山高级中学实验学校校徽 江苏锡山高级中学实验学校的英文-世界即时看
- 小米手机新品国内认证 67W 快充,Civi 产品经理晒新机小尾巴 热点在线