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接口也不再需要手工去写日志追踪逻辑。但是这个优化还不够好,请关注我,下期告诉你为什么。这期只对以下文件做了修改