SpringCloud 遠程調用日誌記錄traceId和spanId

f
最近在項目開發中遇到了一些問題,項目爲多機部署,大量日誌輸出導致很難篩出指定請求的全部相關日誌,以及下游服務調用對應的日誌。因此計劃對項目日誌打印進行一些小改造,使用一個traceId跟蹤請求的全部路徑,前提是不修改原有的打印方式。

簡單的解決思路

想要跟蹤請求,第一個想到的就是當請求來時生成一個traceId放在ThreadLocal裏,然後打印時去取就行了。但在不改動原有輸出語句的前提下自然需要日誌框架的支持了,搜索的一番發現主流日誌框架都提供了MDC功能。

MDC 介紹
MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日誌的功能。MDC 可以看成是一個與當前線程綁定的Map,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日誌時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據。

簡而言之,MDC就是日誌框架提供的一個InheritableThreadLocal,項目代碼中可以將鍵值對放入其中,然後使用指定方式取出打印即可。

簡單總結一下主要思路:

  1. 當請求來的時候,用Filter攔截,主要初始化請求的上下文,包括MDC
  2. 配置文件中打印MDC中的traceId和spanId
  3. 微服務之間調用時,服務調用方需要傳遞當前的上線文,這個時候解析上下文,spanId自增。

具體代碼

  • 定義請求上下文,用ThradLocal存儲,代碼如下:
@Getter
@Setter
public class ProjectContext {

    public static final String CONTEXT_KEY = "CONTEXT_KEY";
    private static final String DEFAULT_SPAN = "1";
    private static final Random RANDOM = new Random();

    /**
     * 每次請求唯一記錄值
     */
    private String traceId;

    /**
     * 一次請求的多次處理唯一標記
     */
    private String spanId;

    /**
     * 請求ip
     */
    private String ip;

    private static ThreadLocal<ProjectContext> LOCAL = new TransmittableThreadLocal<>();

    public static ProjectContext getContext() {
        ProjectContext context = LOCAL.get();
        if (Objects.isNull(context)) {
            context = new ProjectContext();
        }

        return context;
    }

    static void nextSpan() {
        if (Objects.isNull(getContext())) {
            initContext();
            return;
        }
        if (Objects.isNull(getContext().getSpanId())) {
            getContext().setSpanId(DEFAULT_SPAN);
            return;
        }

        // 獲取當前的spanId
        String span = getContext().getSpanId();
        if (span.endsWith(".")) {
            span = span.substring(0, span.length() - 1);
        }
        // 找到切割位置
        int p = span.lastIndexOf(".");
        String last = span.substring(p + 1);
        // 最後需要自增的原數據
        int lastId = Integer.parseInt(last);
        // 完成自增並設置設置到spanId中
        if (p < 0) {
            getContext().setSpanId(String.valueOf(lastId + 1));
        } else {
            getContext().setSpanId(span.substring(0, p) + (lastId + 1));
        }
    }

    /**
     * 透傳上下文
     *
     * @param contextString 被序列化的上下文字符串
     */
    public static void fromString(String contextString) {
        ProjectContext context = GsonUtil.toBean(contextString, ProjectContext.class);

        fromContext(context);
    }

    public static void fromContext(ProjectContext context) {
        LOCAL.set(context);

        nextSpan();
    }

    static void setContext(ProjectContext context) {
        LOCAL.set(context);
    }

    public static void initContext(String ip) {
        initContext();

        ProjectContext context = getContext();

        context.setIp(ip);

        setContext(context);
    }

    public static void initContext() {
        ProjectContext context = new ProjectContext();
        context.setTraceId(String.valueOf(genLogId()));
        context.setSpanId(DEFAULT_SPAN);

        setContext(context);
    }

    public void release() {

    }

    public static long genLogId() {
        return Math.round(((System.currentTimeMillis() % 86400000L) + RANDOM.nextDouble()) * 100000.0D);
    }

    @Override
    public String toString() {
        return GsonUtil.toJson(this);
    }
}

  • 定義過濾器,用於攔截所有的請求,初始化traceId和spanId,RequestFilter
@Slf4j
@WebFilter(filterName = "requestWrapperFilter", urlPatterns = "/*")
public class RequestFilter implements Filter {
    public static final String TRACE_ID = "trace";
    public static final String SPAN_ID = "span";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    /**
     * 初始化請求鏈路信息:唯一key,日誌初始化,body包裝防止獲取日誌打印時後續不能繼續使用
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String contextString = ((HttpServletRequest) request).getHeader(ProjectContext.CONTEXT_KEY);
        if (Objects.nonNull(contextString)) {
            ProjectContext.fromString(contextString);
        } else {
            // 無內容時,也自動初始化
            ProjectContext.initContext();
        }
        initLog();
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }

    public static void initLog() {
        MDC.put(TRACE_ID, ProjectContext.getContext().getTraceId());
        MDC.put(SPAN_ID, ProjectContext.getContext().getSpanId());
    }
}
  • 定義feign的請求攔截器,傳遞當前上下文信息,FeignClientInterceptor
public class FeignClientInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        try {
            ProjectContext projectContext = ProjectContext.getContext();
            if (Objects.nonNull(projectContext)) {
                requestTemplate.header(CONTEXT_KEY, projectContext.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

測試

訪問 http://localhost:9100/hi?name=456

在這裏插入圖片描述

可以看到spanId 的自增,同一個請求traceId是一樣的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章