日誌追蹤:log增加traceId

烏雲發表於2022-11-24

開發中經常需要根據日誌排查問題或跟蹤呼叫流程,很多業務日誌並沒有考慮排查問題時的便利性,看似都記錄了日誌,但同一個請求鏈路的日誌無法對應,特別是當日志跨服務時候,或者同一個業務邏輯同一時刻有多條日誌,根本無法對應起來,如果日誌記可以追蹤的話,可以根據全域性唯一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/t...

下面是自己實現的步驟

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://segmentfault.com/a/11...

2.4 效果展示

確保專案中已配置,skywalking已安裝,探針也已配置。
log檔案和skywalking都可以透過TID(traceId)追蹤日誌鏈。
在控制檯或log檔案中會列印帶TID(traceId)的log:
6.jpg

skywalking服務端會收集到帶TID(traceId)的log:
image.png

相關文章