SpringBoot項目實現(xiàn)分布式日志鏈路追蹤
1.概述
作為一名后端開發(fā)工程師,排查系統(tǒng)問題用得最多的手段之一就是查看系統(tǒng)日志,在當(dāng)下主要的分布式集群環(huán)境中一般使用ELK(Elasticsearch , Logstash, Kibana)
來統(tǒng)一收集日志,以便后續(xù)查看日志定位追蹤相關(guān)問題。但是在并發(fā)情況下,大量的系統(tǒng)用戶即多線程并發(fā)訪問后端服務(wù)導(dǎo)致同一個請求的日志記錄不再是連續(xù)相鄰的,此時多個請求的日志是一起串行輸出到文件中,所以我們篩選出指定請求的全部相關(guān)日志還是比較麻煩的,同時當(dāng)后端異步處理功能邏輯以及微服務(wù)的下游服務(wù)調(diào)用日志追蹤也有著相同的問題。
為了快速排查、定位、解決日常反饋的系統(tǒng)問題,我們就必須解決上面所說的查看請求日志的痛點。解決方案就是:每個請求都使用一個唯一標(biāo)識traceId
來追蹤全部的鏈路顯示在日志中,并且不修改原有的打印方式(代碼無入侵),然后使用使用Logback的MDC
機(jī)制日志模板中加入traceId
標(biāo)識,取值方式為%X{traceId}
。這樣在收集的日志文件中就可以看到每行日志有一個tracceId
值,每個請求的值都不一樣,這樣我們就可以根據(jù)traceId
查詢過濾出一次請求的所有上下文日志了。
2.實現(xiàn)方案
MDC(Mapped Diagnostic Context,映射調(diào)試上下文)
是 log4j
和logback
提供的一種方便在多線程條件下記錄日志的功能。MDC
可以看成是一個與當(dāng)前線程綁定的Map,可以往其中添加鍵值對。MDC
中包含的內(nèi)容可以被同一線程中執(zhí)行的代碼所訪問。當(dāng)前線程的子線程會繼承其父線程中的 MDC
的內(nèi)容。當(dāng)需要記錄日志時,只需要從MDC
中獲取所需的信息即可。MDC
的內(nèi)容則由程序在適當(dāng)?shù)臅r候保存進(jìn)去。對于一個 Web 應(yīng)用來說,通常是在請求被處理的最開始保存這些數(shù)據(jù)。
由于MDC
內(nèi)部使用的是ThreadLocal
所以只有本線程才有效,子線程和下游的服務(wù)MDC
里的值會丟失;所以方案主要的難點是解決traceId值的傳遞問題,需要重點關(guān)注一下兩點:
MDC
中traceId
數(shù)據(jù)如何傳遞給下游服務(wù),下游服務(wù)如何接收traceId
并放入MDC
中- 異步的情況下(線程池)如何把
traceId
值傳給子線程。
2.1 設(shè)置日志模板
無論是我們的項目使用的是log4j還是logback框架,我們都需要先調(diào)整日志配置文件的日志格式如下:
<!-- 日志格式 --> <property name="CONSOLE_LOG_PATTERN" value="[%X{traceId}] [%-5p] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t@${PID}] %c %M : %m%n"/>
這樣才能有效地把traceId
收集到日志文件中。
2.2 請求上下文設(shè)置traceId并有效傳遞下游服務(wù)
按照上面說的,每個請求使用一個唯一標(biāo)識traceId
來追蹤一次請求的全部日志,這就要求我們的traceId
必須保證唯一性,不然就會出現(xiàn)請求日志混亂問題,是絕對不允許的。這里我們利用hutool
框架的生成id工具IdUtil
來生成唯一值,可以生成uuid或者使用雪花算法Snowflake生成唯一id都可以,因為這里id是記錄在日志文件中做唯一標(biāo)識用的,所以對id字符類型,遞增性那些沒啥要求,只要唯一標(biāo)識即可,按照之前習(xí)慣,我就用雪花算法生成唯一id標(biāo)識了。
生成traceId
并放入到MDC
上下文中
public class WebTraceFilter extends OncePerRequestFilter { ? ? ? ?@Override ? ?protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?FilterChain filterChain) throws IOException, ServletException { ? ? ? ?try { ? ? ? ? ? ?String traceId = request.getHeader(MDCTraceUtils.TRACE_ID_HEADER); ? ? ? ? ? ?if (StrUtil.isEmpty(traceId)) { ? ? ? ? ? ? ? ?MDCTraceUtils.addTrace(); ? ? ? ? ? } else { ? ? ? ? ? ? ? ?MDCTraceUtils.putTrace(traceId); ? ? ? ? ? } ? ? ? ? ? ?filterChain.doFilter(request, response); ? ? ? } finally { ? ? ? ? ? ?MDCTraceUtils.removeTrace(); ? ? ? } ? } }
這里通過一個過濾器來設(shè)置traceId
放入到MDC
中,可以將該過濾器的執(zhí)行優(yōu)先級設(shè)置比較靠前,這樣就可以有效保證我們一次請求上下文的日志中都有traceId
了。同時這個過濾器我們是集成在自定義實現(xiàn)的web starter
中,公司的所有服務(wù)都會引用web starter
集成該過濾器,意味著只要我們請求下游服務(wù)時添加了traceId
這個header
,下游服務(wù)執(zhí)行到該過濾器時就會拿到上游服務(wù)傳遞過來的traceId
值放入到當(dāng)前服務(wù)的MDC
中。MDCTraceUtils
工具類代碼如下:
public class MDCTraceUtils { ? ?/** ? ? * 追蹤id的名稱 ? ? */ ? ?public static final String KEY_TRACE_ID = "traceId"; ? ? ?/** ? ? * 日志鏈路追蹤id信息頭 ? ? */ ? ?public static final String TRACE_ID_HEADER = "x-traceId-header"; ? ? ? ?/** ? ? * 創(chuàng)建traceId并賦值MDC ? ? */ ? ?public static void addTrace() { ? ? ? ?String traceId = createTraceId(); ? ? ? ?MDC.put(KEY_TRACE_ID, traceId); ? } ? ? ?/** ? ? * 賦值MDC ? ? */ ? ?public static void putTrace(String traceId) { ? ? ? ?MDC.put(KEY_TRACE_ID, traceId); ? } ? ? ?/** ? ? * 獲取MDC中的traceId值 ? ? */ ? ?public static String getTraceId() { ? ? ? ?return MDC.get(KEY_TRACE_ID); ? } ? ? ?/** ? ? * 清除MDC的值 ? ? */ ? ?public static void removeTrace() { ? ? ? ?MDC.remove(KEY_TRACE_ID); ? } ? ? ?/** ? ? * 創(chuàng)建traceId ? ? */ ? ?public static String createTraceId() { ? ? ? ?return IdUtil.getSnowflake().nextIdStr(); ? } ? }
接下來我們就來演示下traceId
如何在服務(wù)間有效傳遞。無論是微服務(wù)間的服務(wù)調(diào)用還是單體項目的調(diào)用下游服務(wù),我都建議使用Spring Cloud
框架中的openfeign
組件進(jìn)行服務(wù)間接口調(diào)用,如果對組件openfeign
不太熟悉的,可以看看之前我總結(jié)的 openfeign實現(xiàn)原理進(jìn)行了解。這里就用openFeign
進(jìn)行模擬服務(wù)間調(diào)用下游服務(wù)獲取車間列表的接口
@FeignClient(name = "workshopService", url = "http://127.0.0.1:16688/textile", path = "/workshop") public interface WorkshopService { ? ?@GetMapping("/list/temp") ? ?ResponseVO<List<WorkshopDTO>> getList(); }
增加feign攔截器,繼續(xù)把當(dāng)前服務(wù)的traceId
值傳遞給下游服務(wù)
public class FeignInterceptor implements RequestInterceptor { ? ?@Override ? ?public void apply(RequestTemplate requestTemplate) { ? ? ? ?ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ? ? ? ?// 傳遞請求相關(guān)header ? ? ? ?if (requestAttributes != null) { ? ? ? ? ? ?HttpServletRequest request = requestAttributes.getRequest(); ? ? ? ? ? ?Enumeration<String> headerNames = request.getHeaderNames(); ? ? ? ? ? ?if (headerNames != null) { ? ? ? ? ? ? ? ?while (headerNames.hasMoreElements()) { ? ? ? ? ? ? ? ? ? ?String name = headerNames.nextElement(); ? ? ? ? ? ? ? ? ? ?// 跳過 content-length ? ? ? ? ? ? ? ? ? ?if (Objects.equals("content-length", name)){ ? ? ? ? ? ? ? ? ? ? ? ?continue; ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ?String value = request.getHeader(name); ? ? ? ? ? ? ? ? ? ?requestTemplate.header(name, value); ? ? ? ? ? ? ? } ? ? ? ? ? } ? ? ? } ? ? ? ?// 傳遞日志追蹤的traceId ? ? ? ?String traceId = MDCTraceUtils.getTraceId(); ? ? ? ?if (StringUtils.isNotBlank(traceId)) { ? ? ? ? ? ?requestTemplate.header(MDCTraceUtils.TRACE_ID_HEADER, traceId); ? ? ? } ? } }
可以看到這里主要完成傳遞請求的header,traceId這個header單獨處理,這是因為webTraceFilter
過濾器中只把traceId
放入了MDC
中,并沒有吧traceId
放入到請求的header中,servlet
層的filter
過濾器Spring
不建議修改請求的參數(shù),包括header,改起來也比較麻煩,所以這里需要單獨處理傳遞。當(dāng)然這里的攔截器FeignInterceptor
和上面的過濾器WebTraceFilter
都需要注入到Spring容器中。
編寫代碼進(jìn)行接口調(diào)用測試:
? ?@GetMapping("/trace") ? ?public void testTrace() { ? ? ? ?log.info("開始執(zhí)行咯"); ? ? ? ?BaseQuery query = new BaseQuery(); ? ? ? ?ResponseVO<List<WorkshopDTO>> responseVO = workshopService.getList(); ? ? ? ?log.info("接口返回結(jié)果:{}", responseVO); ? }
執(zhí)行日志打印如下,當(dāng)前服務(wù)的日志:
[1675794072381583360] [INFO ] [2023-07-03 17:10:16.289] [http-nio-18888-exec-5@24417] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:18888/fds/test/trace","httpMethod":"GET","classMethod":"com.plasticene.fast.controller.TestController.testTrace","requestParams":null}
[1675794072381583360] [INFO ] [2023-07-03 17:10:16.299] [http-nio-18888-exec-5@24417] com.plasticene.fast.controller.TestController testTrace$original$mZGAheRd : 開始執(zhí)行咯
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.087] [http-nio-18888-exec-5@24417] com.plasticene.fast.controller.TestController testTrace$original$mZGAheRd : 接口返回結(jié)果:ResponseVO(code=200, msg=OK, data=[WorkshopDTO(id=3, orgId=4, name=檢驗車間, location=杭州市西湖區(qū), remark=這里是最嚴(yán)格的, machineCount=null)])
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.088] [http-nio-18888-exec-5@24417] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Response result: null
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.089] [http-nio-18888-exec-5@24417] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : time cost: 805
traceId
為:1675794072381583360,看看下游服務(wù)textile的日志如下:
[1675794072381583360] [INFO ] [2023-07-03 17:10:16.438] [http-nio-16688-exec-1@24461] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:16688/textile/workshop/list/temp","httpMethod":"GET","classMethod":"com.plasticene.textile.controller.WorkshopController.getAllList","requestParams":null}
[1675794072381583360] [DEBUG] [2023-07-03 17:10:16.939] [http-nio-16688-exec-1@24461] com.plasticene.textile.dao.WorkshopDAO.selectList debug : ==> Preparing: SELECT id, org_id, name, location, remark, create_time, update_time, creator, updater FROM workshop WHERE (org_id = ?) ORDER BY id DESC
[1675794072381583360] [DEBUG] [2023-07-03 17:10:16.972] [http-nio-16688-exec-1@24461] com.plasticene.textile.dao.WorkshopDAO.selectList debug : ==> Parameters: 4(Integer)
[1675794072381583360] [DEBUG] [2023-07-03 17:10:17.008] [http-nio-16688-exec-1@24461] com.plasticene.textile.dao.WorkshopDAO.selectList debug : <== Total: 1
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.029] [http-nio-16688-exec-1@24461] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Response result: [{"id":3,"orgId":4,"name":"檢驗車間","location":"杭州市西湖區(qū)","remark":"這里是最嚴(yán)格的","machineCount":null}]
[1675794072381583360] [INFO ] [2023-07-03 17:10:17.040] [http-nio-16688-exec-1@24461] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : time cost: 621
可以看到兩個服務(wù)的traceId
都是一樣的,這就說明我們的traceId
有效傳遞了。
當(dāng)然我們也可以使用Spring自帶的RestTemplate、或者h(yuǎn)ttpClient、OkHttp3等框架進(jìn)行接口調(diào)用,只要請求接口時設(shè)置traceId
這個header
即可,使用restTemplate
客戶端調(diào)接口時,還可以通過擴(kuò)展點ClientHttpRequestInterceptor
接口的實現(xiàn)類對請求進(jìn)行攔截處理進(jìn)行統(tǒng)一traceId
的header
設(shè)置,這樣就不用每個接口請求都要設(shè)置一遍,盡量減少重復(fù)勞動做到優(yōu)雅不過時。這里不在展示詳細(xì),請自我去實現(xiàn)。
2.3 異步父子線程traceId傳遞
上面說過MDC
內(nèi)部使用的是ThreadLocal
,所以只有本線程才有效,子線程和下游的服務(wù)MDC
里的值會丟失。我們項目服務(wù)使用的logback日志框架,所以我們需要重寫logback的LogbackMDCAdapter
,由于logback的MDC
實現(xiàn)內(nèi)部使用的是ThreadLocal
不能傳遞子線程,所以需要重寫替換為阿里的TransmittableThreadLocal
。TransmittableThreadLocal 是Alibaba開源的、用于解決在使用線程池等會池化復(fù)用線程的執(zhí)行組件情況下,提供ThreadLocal
值的傳遞功能,解決異步執(zhí)行時上下文傳遞的問題。
重寫logback的LogbackMDCAdapter
,自定義實現(xiàn)TtlMDCAdapter
類,其實就是把LogbackMDCAdapter
的ThreadLocal
換成TransmittableThreadLocal
即可,其他代碼都是一樣的。
/** *重構(gòu){@link LogbackMDCAdapter}類,搭配TransmittableThreadLocal實現(xiàn)父子線程之間的數(shù)據(jù)傳遞 * * @author fjzheng * @version 1.0 * @date 2022/7/14 13:50 */ public class TtlMDCAdapter implements MDCAdapter { ? ?private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>(); ? ? ?private static final int WRITE_OPERATION = 1; ? ?private static final int MAP_COPY_OPERATION = 2; ? ? ?private static TtlMDCAdapter mtcMDCAdapter; ? ? ?/** ? ? * keeps track of the last operation performed ? ? */ ? ?private final ThreadLocal<Integer> lastOperation = new ThreadLocal<>(); ? ? ?static { ? ? ? ?mtcMDCAdapter = new TtlMDCAdapter(); ? ? ? ?MDC.mdcAdapter = mtcMDCAdapter; ? } ? ? ?public static MDCAdapter getInstance() { ? ? ? ?return mtcMDCAdapter; ? } ? ? ?private Integer getAndSetLastOperation(int op) { ? ? ? ?Integer lastOp = lastOperation.get(); ? ? ? ?lastOperation.set(op); ? ? ? ?return lastOp; ? } ? ? ?private static boolean wasLastOpReadOrNull(Integer lastOp) { ? ? ? ?return lastOp == null || lastOp == MAP_COPY_OPERATION; ? } ? ? ?private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) { ? ? ? ?Map<String, String> newMap = Collections.synchronizedMap(new HashMap<>()); ? ? ? ?if (oldMap != null) { ? ? ? ? ? ?// we don't want the parent thread modifying oldMap while we are ? ? ? ? ? ?// iterating over it ? ? ? ? ? ?synchronized (oldMap) { ? ? ? ? ? ? ? ?newMap.putAll(oldMap); ? ? ? ? ? } ? ? ? } ? ? ? ? ?copyOnInheritThreadLocal.set(newMap); ? ? ? ?return newMap; ? } ? ? ?/** ? ? * Put a context value (the <code>val</code> parameter) as identified with the ? ? * <code>key</code> parameter into the current thread's context map. Note that ? ? * contrary to log4j, the <code>val</code> parameter can be null. ? ? * <p/> ? ? * <p/> ? ? * If the current thread does not have a context map it is created as a side ? ? * effect of this call. ? ? * ? ? * @throws IllegalArgumentException in case the "key" parameter is null ? ? */ ? ?@Override ? ?public void put(String key, String val) { ? ? ? ?if (key == null) { ? ? ? ? ? ?throw new IllegalArgumentException("key cannot be null"); ? ? ? } ? ? ? ? ?Map<String, String> oldMap = copyOnInheritThreadLocal.get(); ? ? ? ?Integer lastOp = getAndSetLastOperation(WRITE_OPERATION); ? ? ? ? ?if (wasLastOpReadOrNull(lastOp) || oldMap == null) { ? ? ? ? ? ?Map<String, String> newMap = duplicateAndInsertNewMap(oldMap); ? ? ? ? ? ?newMap.put(key, val); ? ? ? } else { ? ? ? ? ? ?oldMap.put(key, val); ? ? ? } ? } ? ? ?/** ? ? * Remove the the context identified by the <code>key</code> parameter. ? ? * <p/> ? ? */ ? ?@Override ? ?public void remove(String key) { ? ? ? ?if (key == null) { ? ? ? ? ? ?return; ? ? ? } ? ? ? ?Map<String, String> oldMap = copyOnInheritThreadLocal.get(); ? ? ? ?if (oldMap == null) { ? ? ? ? ? ?return; ? ? ? } ? ? ? ? ?Integer lastOp = getAndSetLastOperation(WRITE_OPERATION); ? ? ? ? ?if (wasLastOpReadOrNull(lastOp)) { ? ? ? ? ? ?Map<String, String> newMap = duplicateAndInsertNewMap(oldMap); ? ? ? ? ? ?newMap.remove(key); ? ? ? } else { ? ? ? ? ? ?oldMap.remove(key); ? ? ? } ? ? } ? ? ? ?/** ? ? * Clear all entries in the MDC. ? ? */ ? ?@Override ? ?public void clear() { ? ? ? ?lastOperation.set(WRITE_OPERATION); ? ? ? ?copyOnInheritThreadLocal.remove(); ? } ? ? ?/** ? ? * Get the context identified by the <code>key</code> parameter. ? ? * <p/> ? ? */ ? ?@Override ? ?public String get(String key) { ? ? ? ?final Map<String, String> map = copyOnInheritThreadLocal.get(); ? ? ? ?if ((map != null) && (key != null)) { ? ? ? ? ? ?return map.get(key); ? ? ? } else { ? ? ? ? ? ?return null; ? ? ? } ? } ? ? ?/** ? ? * Get the current thread's MDC as a map. This method is intended to be used ? ? * internally. ? ? */ ? ?public Map<String, String> getPropertyMap() { ? ? ? ?lastOperation.set(MAP_COPY_OPERATION); ? ? ? ?return copyOnInheritThreadLocal.get(); ? } ? ? ?/** ? ? * Returns the keys in the MDC as a {@link Set}. The returned value can be ? ? * null. ? ? */ ? ?public Set<String> getKeys() { ? ? ? ?Map<String, String> map = getPropertyMap(); ? ? ? ? ?if (map != null) { ? ? ? ? ? ?return map.keySet(); ? ? ? } else { ? ? ? ? ? ?return null; ? ? ? } ? } ? ? ?/** ? ? * Return a copy of the current thread's context map. Returned value may be ? ? * null. ? ? */ ? ?@Override ? ?public Map<String, String> getCopyOfContextMap() { ? ? ? ?Map<String, String> hashMap = copyOnInheritThreadLocal.get(); ? ? ? ?if (hashMap == null) { ? ? ? ? ? ?return null; ? ? ? } else { ? ? ? ? ? ?return new HashMap<>(hashMap); ? ? ? } ? } ? ? ?@Override ? ?public void setContextMap(Map<String, String> contextMap) { ? ? ? ?lastOperation.set(WRITE_OPERATION); ? ? ? ? ?Map<String, String> newMap = Collections.synchronizedMap(new HashMap<>()); ? ? ? ?newMap.putAll(contextMap); ? ? ? ? ?// the newMap replaces the old one for serialisation's sake ? ? ? ?copyOnInheritThreadLocal.set(newMap); ? } }
接下來只需要實現(xiàn)程序啟動時加載上我們自己實現(xiàn)的TtlMDCAdapter
:
/** * * 初始化TtlMDCAdapter實例,并替換MDC中的adapter對象 * * @author fjzheng * @version 1.0 * @date 2022/7/14 13:55 */ public class TtlMDCAdapterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { ? ?@Override ? ?public void initialize(ConfigurableApplicationContext applicationContext) { ? ? ? ?//加載TtlMDCAdapter實例 ? ? ? ?TtlMDCAdapter.getInstance(); ? } }
這樣我們在異步多線程情況下MDC
的traceId
值就能正常傳遞,下面來看看測試示例:
? // 定義線程池 ? private ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() ? ? ? ? ? .setNameFormat("letter-pool-%d").build(); ? private ExecutorService fixedThreadPool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors()*2, ? ? ? ? ? ?Runtime.getRuntime().availableProcessors() * 40, ? ? ? ? ? ?0L, ? ? ? ? ? ?TimeUnit.MILLISECONDS, ? ? ? ? ? ?new LinkedBlockingQueue<Runnable>(Runtime.getRuntime().availableProcessors() * 20), ? ? ? ? ? ?namedThreadFactory); ? ?// 測試接口 ? ?@GetMapping("/async") ? ?public void testAsync() { ? ? ? ?log.info("打印日志了"); ? ? ? ?fixedThreadPool.execute(()->{ ? ? ? ? ? ?log.info("異步執(zhí)行了"); ? ? ? ? ? ?try { ? ? ? ? ? ? ? ?Student student = null; ? ? ? ? ? ? ? ?String name = student.getName(); ? ? ? ? ? } catch (Exception e) { ? ? ? ? ? ? ? ?log.error("異步報錯了:", e); ? ? ? ? ? } ? ? ? ? }); ? }
執(zhí)行結(jié)果日志打印如下:、
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.683] [http-nio-18888-exec-8@24417] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:18888/fds/test","httpMethod":"GET","classMethod":"com.plasticene.fast.controller.TestController.test","requestParams":null}
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.698] [http-nio-18888-exec-8@24417] com.plasticene.fast.controller.TestController test$original$mZGAheRd : 打印日志了
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.700] [http-nio-18888-exec-8@24417] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Response result: null
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.700] [http-nio-18888-exec-8@24417] com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : time cost: 24
[1675805796950241280] [INFO ] [2023-07-03 17:56:51.700] [letter-pool-1@24417] com.plasticene.fast.controller.TestController lambda$test$0 : 異步執(zhí)行了
[1675805796950241280] [ERROR] [2023-07-03 17:56:51.704] [letter-pool-1@24417] com.plasticene.fast.controller.TestController lambda$test$0 : 異步報錯了:
java.lang.NullPointerException: null
at com.plasticene.fast.controller.TestController.lambda$test$0(TestController.java:93)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
3.總結(jié)
以上全部就是關(guān)于Spring Boot如何實現(xiàn)分布式日志鏈路追蹤的相關(guān)知識點。工欲善其事,必先利其器,我們要想快速通過日志定位系統(tǒng)問題,就必須通過traceId
高效查找一次請求的全部上下文日志,包括異步執(zhí)行的邏輯。
以上就是SpringBoot項目實現(xiàn)分布式日志鏈路追蹤的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot 日志鏈路追蹤的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
HttpClient的DnsResolver自定義DNS解析另一種選擇深入研究
這篇文章主要為大家介紹了HttpClient的DnsResolver自定義DNS解析另一種選擇深入研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10SpringCloud URL重定向及轉(zhuǎn)發(fā)代碼實例
這篇文章主要介紹了SpringCloud URL重定向及轉(zhuǎn)發(fā)代碼實例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03Java @Value("${xxx}")取properties時中文亂碼的解決
這篇文章主要介紹了Java @Value("${xxx}")取properties時中文亂碼的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07spring task @Scheduled注解各參數(shù)的用法
這篇文章主要介紹了spring task @Scheduled注解各參數(shù)的用法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10SpringBoot AOP注解失效問題排查與解決(調(diào)用內(nèi)部方法)
這篇文章主要介紹了SpringBoot AOP注解失效問題排查與解決(調(diào)用內(nèi)部方法),文中通過代碼示例介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-04-04mybatis動態(tài)sql之Map參數(shù)的講解
今天小編就為大家分享一篇關(guān)于mybatis動態(tài)sql之Map參數(shù)的講解,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03