日志追踪:log增加traceId
开发中经常需要根据日志排查问题或跟踪调用流程,很多业务日志并没有考虑排查问题时的便利性,看似都记录了日志,但同一个请求链路的日志无法对应,特别是当日志跨服务时候,或者同一个业务逻辑同一时刻有多条日志,根本无法对应起来,如果日志记可以追踪的话,可以根据全局唯一id搜索得出一条调用链的日志,顺着这个日志链条就可以看出程序的执行全过程,进而有利于排查出问题。
这里我们用两种方式实现:
自己写代码实现:轻量级,灵活,可任意修改,适合小项目;目前已实现支持已spring为基础的springboot,springcloud,dubbo且使用logback日志框架的项目。
使用开源工具:安装费点事,功能强大,适合大项目;支持的开发语言和框架较多。
1. 代码实现
不想一步步的自己实现的话,有已经封装好的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
下面是自己实现的步骤
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(); // 放行跨域预请求 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;@Configurationpublic 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;@Configurationpublic 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,并设置到dubbo的上下文,如果没有则生成一个 traceId = MDC.get(TraceLogConstant.TRACE_ID); if (StringUtils.isEmpty(traceId)) { traceId = TraceIdUtil.uuid_timestamp(); MDC.put(TraceLogConstant.TRACE_ID, traceId); } // provider 设置traceId到日志到上下文 invocation.setAttachment(TraceLogConstant.TRACE_ID, traceId); } else { MDC.put(TraceLogConstant.TRACE_ID, traceId); } Result result = invoker.invoke(invocation); return result; }}
声明过滤器:
在resource下创建Filter文件,里面内容:traceLogDubboFilter=.TraceLogDubboFilter,是你的实际路径,如下图:
3.png
1.4 注解
比如定时任务等利用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@Componentpublic 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,需要重写线程池,其他线程的处理可以参考这种方式。
先实现修饰类:
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;@Configurationpublic 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调用时候如果想传递traceId,必须在自己的Okhttp或HttpClient中加入traclog 拦截器,如下:
Okhttp
实现拦截器:
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
实现拦截器:
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}] 例如:
image.png
启动项目,查看效果,类似下图说明成功。
image.png
2. 采用第三方工具
推荐 skywalking。
2.1 配置pom.xml
<!--skywalking traceId 记录到logback日志,请与安装的服务器版本对应--><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"/> <!-- 系统日志输出 --> <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
注意下面图中的红色标注
4.jpg
5.jpg
2.3 skywalking安装和使用
教程:https://www.jianshu.com/p/b69bc629f476
2.4 效果展示
确保项目中已配置,skywalking已安装,探针也已配置。
log文件和skywalking都可以通过TID(traceId)追踪日志链。
在控制台或log文件中会打印带TID(traceId)的log:
6.jpg
skywalking服务端会收集到带TID(traceId)的log:
作者:乌云暴雨
链接:https://www.jianshu.com/p/c93e55b1c011