分布式日志链路追踪

因为在目前的任务分配上,我们是属于按照微服务上来划分开发小组的,也就是基本上我们都只是负责自己的服务了,在针对自己服务的请求日志上,我用的是aop形式来进行记录,这样可以最大限度的降低代码的入侵性

/**
 * 前端接口日志
 */
@Slf4j
@Component
@Aspect
public class FrontLogAspect {
    
    /**
     * 记录接口日志
     *
     * @param proceeding
     * @return
     */
    @Around(value = "execution(* com.zhaojiafang.gateway.controller.sales.*.*(..))")
    public Object log(ProceedingJoinPoint proceeding) throws Throwable {

        // 创建流水号
        TLocalHelper.createSeq();
        Thread thread = Thread.currentThread();
        String name = thread.getName();
        thread.setName(TLocalHelper.getSeq());
        HttpServletRequest request = getRequest();
        String args = null;
        try {
            args = new ObjectMapper().writeValueAsString(proceeding.getArgs());
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            args="";
        }
        log.info("前端接口请求路径:{},请求参数:{},流水号:{}", request.getRequestURI(), args, TLocalHelper.getSeq());
        Object result = proceeding.proceed();
        log.info("接口执行结束,返回数据:{},流水号:{}", result.toString(), TLocalHelper.getSeq());
        thread.setName(name);
        TLocalHelper.closeLocal();
        return result;
    }

    /**
     * 获取request
     *
     * @return
     */
    public static HttpServletRequest getRequest() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        return sra.getRequest();
    }
}
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 线程缓存工具
 *
 * @author yaojn
 */
public class TLocalHelper {

    private static ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<Map<String, String>>();

    public static void closeLocal() {
        threadLocal.remove();
    }
    private static class LastTime {
        private static long time;
        private static int i;
    }

    /**
     * 创建流水
     *
     * @return
     */
    public static String createSeq() {
        SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
        Date now = new Date();
        String sequence = null;
        long nowSec = now.getTime() / 1000;
        synchronized (LastTime.class) {
            if (LastTime.time >= nowSec) {
                LastTime.i++;
            } else {
                LastTime.i = 0;
                LastTime.time = nowSec;
            }
            sequence = "25" + df.format(new Date(LastTime.time * 1000))
                    + String.format("%04d", LastTime.i);
        }
        Map<String, String> tlMap = new HashMap<String, String>();
        tlMap.put("localseq", sequence);
        threadLocal.set(tlMap);
        return sequence;
    }

    /**
     * 获取流水
     *
     * @return
     */
    public static String getSeq() {
        Map<String, String> map = threadLocal.get();
        if (Objects.nonNull(map)) {
            return map.get("localseq");
        } else {
            return "";
        }
    }
}

ps: 我之前的日志记录都会记录相对应的流水号:log.info("前端接口请求路径:{},请求参数:{},流水号:{}")

然后这段时间又学到了一个概念,就是MDC ,这个MDC 是slf 包下的一个类,他的作用就在于当我们在代码中设置

MDC.put("TRACE_ID", TraceLogUtils.getTraceId());

并且结合

logback 的pattern 设置中添加 [%X{TRACE_ID}]

这样就可以不用在显示的打印出 流水号,而是logback的日志会自动的打印上流水号


当然上面的代码很不完善,因为用threalocal 他并不支持线程上下文切换上traceId 的传递,基于此 可以参考线程上下文切换数值传递的方法,这个有很多的解决方法来进行实现,就不展开说了。


但是在实际的开发或者生成中,其实上面的代码有点缺陷,因为基本都是分布式微服务开发,然后我这个的流水号只是基于当前服务的,这种的方式肯定存在着缺陷,在服务之间调用上不利于链路追踪

因为我需要在 前端用户调用->A->B->C 这种形式下把最开始的A中或者前端生成的traceId 给保存下来并且在调用下游服务的时候也传递过去,所以在总结下service层需要做3件事情

1.接受前一个服务传递过来的traceId

2.日志打印

3.调用下游服务的时候传递traceId;

因此我第一时间想到的就是通过请求头的形式把traceId 放入请求头中进行传递,

因此我定义了2个类

1:定义过滤器,将headler 里面的数据给取出来然后保存到threadlocal里面

  private static final String TRACE_ID = "traceId";
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        try {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String traceId = httpRequest.getHeader(TRACE_ID);
            TraceIdHelper.setTraceId(traceId);
            filterChain.doFilter(request, response);
        } finally {
            // 清除MDC的traceId值,确保当次请求不会影响其他请求
            TraceIdHelper.clearTraceId();
        }
    }

2.定义feign拦截器

在服务之间调用的时候把traceId 通过feign拦截器中设置的请求头设置进行传递

public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            requestTemplate.header(TraceIdFilter.TRACE_ID, TraceIdHelper.getTraceId());
        }
    }
}

在当时我通过feign拦截的时候遇到了一个No thread-bound request found 的异常

在网上找到的答案是:拦截器的实质是因为重开子线程,无法获取到threadlocal 需要进行额外配置

# 更换hystrix策略,解决无法传递threadLocal变量问题
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: SEMAPHORE

上面的代码帮我完成了feign调用之间的traceId的传递,但是我们在项目中也用到了rabbitmq 作为服务之间的通信使用,但是基于rabbitmq 目前还没有找到更好的方式来传递数据,目前我能想到的就是在传递消息体的过程中新增个traceId 字段,但是因为这种是涉及到了代码入侵这一块了,所以这个目前没有实现

posted @   冷扑星  阅读(57)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示