手写链路追踪优化-自动全局追踪代替局部手动追踪
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 {@Overridepublic 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 oneif (StringUtils.isBlank(traceId)) {traceId = RandomStrUtils.generateRandomString(15);}// keep original thread nameThread currentThread = Thread.currentThread();String originalName = currentThread.getName();try {//replace current thread name with a trace idThread.currentThread().setName(traceId);filterChain.doFilter(servletRequest, servletResponse);} finally {//restore thread name before api request endThread.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());}@Datapublic 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; thenopenssl rand -base64 20 | tr -dc 'a-zA-Z0-9' | fold -w 15 | head -n 1else# 使用系统方法生成local chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"local result=""result=$(printf "%s" "${chars:$((RANDOM % ${#chars})):1}"{1..15} | tr -d '\n')echo "$result"fi
}function normalLogin() {# 生成15位随机字符串作为traceIdtraceId=$(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 CurrentDload 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接口也不再需要手工去写日志追踪逻辑。但是这个优化还不够好,请关注我,下期告诉你为什么。这期只对以下文件做了修改