開發(fā)中經(jīng)常需要根據(jù)日志排查問題或跟蹤調(diào)用流程磷脯,很多業(yè)務(wù)日志并沒有考慮排查問題時的便利性柠衅,看似都記錄了日志氛赐,但同一個請求鏈路的日志無法對應(yīng)筷登,特別是當(dāng)日志跨服務(wù)時候剃根,或者同一個業(yè)務(wù)邏輯同一時刻有多條日志,根本無法對應(yīng)起來前方,如果日志記可以追蹤的話狈醉,可以根據(jù)全局唯一id搜索得出一條調(diào)用鏈的日志,順著這個日志鏈條就可以看出程序的執(zhí)行全過程惠险,進(jìn)而有利于排查出問題苗傅。
這里我們用兩種方式實現(xiàn):
自己寫代碼實現(xiàn):輕量級,靈活班巩,可任意修改渣慕,適合小項目;目前已實現(xiàn)支持已spring為基礎(chǔ)的springboot,springcloud,dubbo且使用logback日志框架的項目。
使用開源工具:安裝費點事抱慌,功能強大逊桦,適合大項目;支持的開發(fā)語言和框架較多抑进。
1. 代碼實現(xiàn)
不想一步步的自己實現(xiàn)的話强经,有已經(jīng)封裝好的jar(建議www.search.maven.org搜最新版本):
<dependency>
<groupId>com.wuyunonline.tracelog</groupId>
<artifactId>tracelog-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
使用說明和源碼地址:https://gitee.com/jwb-wuyun/traclog
下面是自己實現(xiàn)的步驟
1.1 springmvc
先寫一個攔截器:
import com.tracelog.common.constant.TraceLogConstant;
import com.tracelog.common.util.TraceIdUtil;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TraceLogWebMvcInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String method = request.getMethod();
// 放行跨域預(yù)請求
if (method.equals("OPTIONS")) {
return true;
}
String traceId = request.getHeader(TraceLogConstant.TRACE_ID);
if (StringUtils.isEmpty(traceId)) {
traceId = TraceIdUtil.uuid_timestamp();
}
MDC.put(TraceLogConstant.TRACE_ID, traceId);
return true;
}
}
再配置攔截器:
import com.tracelog.interceptor.TraceLogWebMvcInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class TraceLogWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(mvcInterceptor()).addPathPatterns("/**");
}
@Bean
public TraceLogWebMvcInterceptor mvcInterceptor() {
return new TraceLogWebMvcInterceptor();
}
}
1.2 springcloud
springcloud一般都是用feign,所以我們需要攔截feign寺渗。
先寫攔截器:
import com.tracelog.common.constant.TraceLogConstant;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
public class TraceLogFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header(TraceLogConstant.TRACE_ID, MDC.get(TraceLogConstant.TRACE_ID));
}
}
再配置攔截器:
import com.tracelog.interceptor.TraceLogFeignInterceptor;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TraceLogFeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new TraceLogFeignInterceptor();
}
}
1.3 dubbo
dubbo必須是Apache的dubbo(老版的是阿里的匿情,阿里已將dubbo加入apache)
先寫過濾器:
import com.tracelog.common.constant.TraceLogConstant;
import com.tracelog.common.util.TraceIdUtil;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER})
public class TraceLogDubboFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 獲取dubbo上下文中的traceId
String traceId = invocation.getAttachment(TraceLogConstant.TRACE_ID);
if (StringUtils.isEmpty(traceId)) {
// customer 獲取上游來的traceId,并設(shè)置到dubbo的上下文信殊,如果沒有則生成一個
traceId = MDC.get(TraceLogConstant.TRACE_ID);
if (StringUtils.isEmpty(traceId)) {
traceId = TraceIdUtil.uuid_timestamp();
MDC.put(TraceLogConstant.TRACE_ID, traceId);
}
// provider 設(shè)置traceId到日志到上下文
invocation.setAttachment(TraceLogConstant.TRACE_ID, traceId);
} else {
MDC.put(TraceLogConstant.TRACE_ID, traceId);
}
Result result = invoker.invoke(invocation);
return result;
}
}
聲明過濾器:
在resource下創(chuàng)建Filter文件炬称,里面內(nèi)容:traceLogDubboFilter=.TraceLogDubboFilter,是你的實際路徑涡拘,如下圖:
1.4 注解
比如定時任務(wù)等利用AOP加切面:@Scheduled,@PostConstruct
import com.tracelog.common.constant.TraceLogConstant;
import com.tracelog.common.util.TraceIdUtil;
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.slf4j.MDC;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TracelogAspect {
@Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled) || @annotation(javax.annotation.PostConstruct)")
public void tracelogPointCut() {
}
@Around("tracelogPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MDC.put(TraceLogConstant.TRACE_ID, TraceIdUtil.uuid_timestamp());
return point.proceed();
}
}
1.5 異步線程獲取主線程traceId
針對注解@Async转砖,需要重寫線程池,其他線程的處理可以參考這種方式。
先實現(xiàn)修飾類:
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import java.util.Map;
public class TraceLogMdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> map = MDC.getCopyOfContextMap();
return () -> {
try {
if (map != null) {
MDC.setContextMap(map);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
再配置:
import com.tracelog.interceptor.TraceLogMdcTaskDecorator;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class TraceLogAsyncConfig extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(50);
threadPoolTaskExecutor.setMaxPoolSize(100);
threadPoolTaskExecutor.setQueueCapacity(1000);
threadPoolTaskExecutor.setThreadNamePrefix("Async-");
threadPoolTaskExecutor.setTaskDecorator(new TraceLogMdcTaskDecorator());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
1.6 擴展支持httpclinet府蔗,okhttp
使用Okhttp或HttpClient調(diào)用時候如果想傳遞traceId晋控,必須在自己的Okhttp或HttpClient中加入traclog 攔截器,如下:
Okhttp
實現(xiàn)攔截器:
import com.tracelog.common.constant.TraceLogConstant;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.jetbrains.annotations.NotNull;
import org.slf4j.MDC;
import java.io.IOException;
public class TraceLogOkhttpInterceptor implements Interceptor {
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader(TraceLogConstant.TRACE_ID, MDC.get(TraceLogConstant.TRACE_ID))
.build();
return chain.proceed(request);
}
}
使用攔截器
import com.tracelog.interceptor.TraceLogOkhttpInterceptor;
...
OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(new TraceLogOkhttpInterceptor()).build();
HttpClient
實現(xiàn)攔截器:
import com.tracelog.common.constant.TraceLogConstant;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.protocol.HttpContext;
import org.slf4j.MDC;
public class TraceLogHttpClientInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest httpRequest, HttpContext httpContext){
httpRequest.addHeader(TraceLogConstant.TRACE_ID, MDC.get(TraceLogConstant.TRACE_ID));
}
}
使用攔截器
import com.tracelog.interceptor.TraceLogHttpClientInterceptor;
...
CloseableHttpClient httpClient = HttpClientBuilder.create().addInterceptorFirst(new TraceLogHttpClientInterceptor()).build();
1.7 配置logback.xml
項目中找到logback.xml或logback-spring.xml姓赤, 在日志格式中加入[traceId:%X{traceId}] 例如:
啟動項目赡译,查看效果,類似下圖說明成功不铆。
2. 采用第三方工具
推薦 skywalking蝌焚。
2.1 配置pom.xml
<!--skywalking traceId 記錄到logback日志,請與安裝的服務(wù)器版本對應(yīng)-->
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId>
<version>8.9.0</version>
</dependency>
2.2 配置logback.xml
<!-- 日志輸出格式 -->
<property name="log.pattern" value="[%tid] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%method,%line] - %msg%n"/>
<!-- 系統(tǒng)日志輸出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${log.pattern}</pattern>
</layout>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 日志收集 skywalking 8.4.0版本開始支持 -->
<appender name="grpcLog" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
<pattern>${log.pattern}</pattern>
</layout>
</encoder>
</appender>
日志收集不是必須的誓斥,如果覺得不需要集中收集在一起只洒,那么不需要加GRPCLogClientAppender
注意下面圖中的紅色標(biāo)注
2.3 skywalking安裝和使用
教程:http://www.reibang.com/p/b69bc629f476
2.4 效果展示
確保項目中已配置,skywalking已安裝劳坑,探針也已配置毕谴。
log文件和skywalking都可以通過TID(traceId)追蹤日志鏈。
在控制臺或log文件中會打印帶TID(traceId)的log:
skywalking服務(wù)端會收集到帶TID(traceId)的log: