手写链路追踪优化-自动全局追踪代替局部手动追踪

发布于:2025-08-31 ⋅ 阅读:(15) ⋅ 点赞:(0)

1. 前文回顾和优化需求分析

前文的demo已经具备了简单的日志追踪能力
前文:手写链路追踪
它的缺点也明显,它是API level的,也就是如果想全局追踪,每个API都需要手写一份,这就会产生很多重复代码,同时我们在API层面也不想见到跟业务无关的代码,该怎么优化呢?
要想全局有效,一劳永逸,你可能会想到放到filter或者aspect处理,让我们对比一下哪个更合适

特性 Filter Aspect
作用范围 Web请求层面 方法调用层面
依赖关系 依赖Servlet容器 依赖AOP框架(如Spring AOP)
触发条件 所有HTTP请求 特定方法调用
配置方式 web.xml或注解 注解或XML配置
执行顺序 按注册顺序 按优先级
灵活性 相对较低 较高,可精确控制切入点

因为是从API转移,符合Web请求层面和http触发条件,跟filter情景一致,所以考虑把它转移到filter

2. 代码实现

2.1 创建一个filter

实现如下功能 :

  • servletRequest拦截trace id的请求头
  • 如果upstream没有传入trace id,系统内部自己随机生成一个
  • 用trace id替换当前线程的线程名(这是利用了在传统的Spring MVC中,默认情况下是一个请求对应一个阻塞线程的原理,这里留个伏笔,请思考还有什么遗漏)
  • 为了线程安全,直接修改线程名需要注意恢复
package com.sandwich.logtracing.filter;

import com.sandwich.logtracing.util.RandomStrUtils;
import jakarta.servlet.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.RequestFacade;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;

/**
 * @Author 公众号: IT三明治
 * @Date 2025/8/30
 * @Description: log filter, to update the log thread name with a trace id
 */
@Slf4j
public class LogFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String traceId = ((RequestFacade) servletRequest).getHeader("x-request-correlation-id");
        //if the request header don't have a trace id,then generate a random one
        if (StringUtils.isBlank(traceId)) {
            traceId = RandomStrUtils.generateRandomString(15);
        }
        // keep original thread name
        Thread currentThread = Thread.currentThread();
        String originalName = currentThread.getName();
        try {
            //replace current thread name with a trace id
            Thread.currentThread().setName(traceId);
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            //restore thread name before api request end
            Thread.currentThread().setName(originalName);
        }
    }
}

2.2 注册filter

package com.sandwich.logtracing.config;

import com.sandwich.logtracing.filter.LogFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

/**
 * @Author 公众号: IT三明治
 * @Date 2025/8/30
 * @Description:
 */
@Configuration
public class WebConfiguration {

    @Bean
    @ConditionalOnMissingBean(LogFilter.class)
    @Order(Ordered.HIGHEST_PRECEDENCE + 101)
    public FilterRegistrationBean<LogFilter> logFilterFilterRegistrationBean() {
        FilterRegistrationBean<LogFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new LogFilter());
        bean.addUrlPatterns("/*");
        return bean;
    }
}

2.3 删除API中trace id的处理逻辑

package com.sandwich.logtracing.controller;

import com.sandwich.logtracing.entity.ApiResponse;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * @Author 公众号: IT三明治
 * @Date 2025/8/29
 * @Description: login demo controller
 */
@Slf4j
@RestController
@RequestMapping("/test")
public class LoginController {

    @PostMapping("/login")
    public ApiResponse<String> login(@RequestBody LoginRequest loginRequest) {
        for (int i=1; i<= 10; i++) {
            log.info("processing login for user {}, login step {} done", loginRequest.getUsername(), i);
        }
        log.info("user {} login success", loginRequest.getUsername());
        return ApiResponse.success("Sandwich login success", Thread.currentThread().getName());
    }

    @Data
    public static class LoginRequest {
        private String username;
        private String password;
    }
}

注意:Thread.currentThread().getName()已经变成trace id了。

2.4 为了显示API请求的所有内容,还是用shell的方式请求

#!/bin/bash

# Define the API endpoint
API_URL="http://localhost:8080/test/login"

function generate_random_string() {
    # 使用openssl生成随机字符串(如果已安装)
    if command -v openssl &> /dev/null; then
        openssl rand -base64 20 | tr -dc 'a-zA-Z0-9' | fold -w 15 | head -n 1
    else
        # 使用系统方法生成
        local chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        local result=""
        result=$(printf "%s" "${chars:$((RANDOM % ${#chars})):1}"{1..15} | tr -d '\n')
        echo "$result"
    fi
}

function normalLogin() {
    # 生成15位随机字符串作为traceId
    traceId=$(generate_random_string)
    echo "Generated traceId from client side: $traceId"
    response=$(curl -X POST $API_URL \
        -H "Content-Type: application/json" \
        -H "x-request-correlation-id: $traceId" \
        -d '{"username": "Sandwich", "password": "test"}')
    echo "Response from login API:"
    # 通过python工具将返回信息格式化成json格式
    echo "$response" | python -m json.tool
}

normalLogin

3. 验证测试

  • 启动项目
  • 执行shell请求
Administrator@USER-20230930SH MINGW64 /d/git/java/log-tracing/shell (master)
$ ./login.sh
Generated traceId from client side: ubBKwVCauRVV78m
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   144    0   100  100    44   7636   3360 --:--:-- --:--:-- --:--:-- 11076
Response from login API:
{
    "responseCode": 200,
    "message": "success",
    "data": "Sandwich login success",
    "traceId": "ubBKwVCauRVV78m"
}
  • 用trace id追踪日志信息
    日志追踪

4. 总结

经过以上实现,我们把日志追踪搬到了filter实现,API只需要完成业务逻辑即可,新增API接口也不再需要手工去写日志追踪逻辑。但是这个优化还不够好,请关注我,下期告诉你为什么。这期只对以下文件做了修改