日志追踪: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;

@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,并设置到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
@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,需要重写线程池,其他线程的处理可以参考这种方式。
先实现修饰类:

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调用时候如果想传递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:


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容