SprigBoot日志:使用traceId跟踪全局请求

1,093 阅读3分钟

SprigBoot日志:使用traceId跟踪全局请求

Trace ID 简介

Trace ID是一种用于在分布式系统中跟踪请求的唯一标识符。

当一个请求进入系统时,它会被分配一个唯一的Trace ID。随着请求穿过系统中的每个服务,Trace ID会被传递下去,并在每个服务中记录。

利用Trace ID,我们可以追踪请求的路径,并查看请求在每个服务中的处理状态和结果。通过跟踪请求的生命周期,我们也可以确定每个请求需要多长时间才能完成,并且可以查看每个服务的处理时间和性能。

使用Logback的MDC实现请求跟踪

Logback的MDC(Mapped Diagnostic Context)是一种用于在应用程序中跟踪请求或事件的日志记录技术。MDC允许我们在应用程序中存储上下文信息,并在日志记录期间将该信息注入到日志消息中。

MDC的工作方式是将上下文信息存储在一个线程本地的映射(比如 ThreadLocal)中。在应用程序中,可以通过调用MDC.put(key, value)方法将信息存储在MDC中。然后,在记录日志消息时,可以使用类似于${key}的占位符将上下文信息嵌入到日志消息中。

以下是使用Logback的MDC记录日志消息的示例:

日志配置

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Properties>
        <Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%X{traceId}] %-5level %logger{36} - %msg%n</Property>
    </Properties>

    <Appenders>
        <Console name="Console" target="SYSTEM_OUT" follow="true">
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </Console>
        <Async name="AsyncConsole" bufferSize="16384" queueSize="500" ringBufferSize="1024" blocking="false">
            <AppenderRef ref="Console"/>
        </Async>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="AsyncConsole"/>
        </Root>
    </Loggers>
</Configuration>

Trace ID 生成

package com.teochew;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;

public class MyService {

    private static final Logger log = LoggerFactory.getLogger(MyService.class);

    public static void main(String[] args) {
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        log.info("test log");
    }
}

运行结果

Trace ID 生成.png

在上面的示例中,我们在处理请求之前将请求ID存储在MDC中,然后在日志消息中使用%X{traceId}

占位符引用该值。

线程传递traceId

由于MDC是基于ThreadLocal实现,上下文信息是线程私有的,所以如果代码中使用了多线程,traceId是无法自动传递的。比如下面这段代码及其输出

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;

import static java.util.concurrent.TimeUnit.SECONDS;

public class MyService {
    private static final ThreadPoolExecutor DEFAULT_EXECUTOR = new ThreadPoolExecutor(20, 80,
            10L, SECONDS, new LinkedBlockingQueue<>(),
            (r) -> {
                int count = 0;
                return new Thread(r, "Thread-" + (++count));
            });

    private static final Logger log = LoggerFactory.getLogger(MyService.class);

    public static void main(String[] args) {
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        DEFAULT_EXECUTOR.execute(() -> {
            log.info("test log");
        });
    }
}

线程传递traceId.png

我们可以看到,在日志输出中 traceId信息为空,

package com.teochew;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.Map;
import java.util.UUID;

public class MyService {
    private static final Logger log = LoggerFactory.getLogger(MyService.class);

    public static void main(String[] args) {
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
				// 修饰器 传递traceId
        executor.setTaskDecorator(runnable -> wrapAsync(runnable, MDC.getCopyOfContextMap()));
        executor.initialize();
        executor.execute(() -> {
            log.info("test log");
        });
    }

    public static Runnable wrapAsync(Runnable task, Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            if (MDC.get("traceId") == null) {
                MDC.put("traceId", UUID.randomUUID().toString());
            }
            try {
                task.run();
            } finally {
                MDC.clear();
            }
        };
    }

}

线程传递traceId_2.png

以上代码,我们通过线程修饰器来做traceId的传递,除这种方式外,我们也可以通过ThreadLocal来传递traceId

分布式系统间传递traceId (dubbo)

Dubbo默认集成了Zipkin,在Dubbo服务调用时,Dubbo会自动将TraceId和SpanId等信息写入MDC中,然后在服务调用链路中传递。

在Dubbo服务实现类中,可以使用MDC来获取TraceId:

public class XxxServiceImpl implements XxxService {

    @Override
    public String xxxMethod(String arg) {
        String traceId = MDC.get("traceId");
        // ...
    }
}

在客户端中主动设置TraceId的方式如下

public class XxxClient {

    private static final String TRACE_ID_KEY = "traceId";

    public String xxxMethod(String arg) {
        StringtraceId = UUID.randomUUID().toString();  // 生成新的TraceId
        MDC.put(TRACE_ID_KEY, traceId);  // 将TraceId设置到MDC中
        try {
            // 调用Dubbo服务
            return xxxService.xxxMethod(arg);
        } finally {
            MDC.remove(TRACE_ID_KEY);  // 最后记得从MDC中移除TraceId
        }
    }
}