前言
前面我们实现了链路追踪的核心机制,本章将其与Web环境深度集成,实现HTTP请求级别的全链路追踪。这是将框架应用到实际Web应用的关键一步。
Web集成的核心挑战
HTTP请求的生命周期追踪
graph LR
A[HTTP请求] --> B[TraceIdFilter]
B --> C[RequestLogFilter]
C --> D[LogHandlerInterceptor]
D --> E[Controller方法]
E --> F[Service方法]
F --> G[响应返回]
集成要点:
- 🎯 请求入口:在请求开始时初始化追踪上下文
- 🔄 上下文传递:确保整个请求链路中上下文的一致性
- 📊 请求统计:记录HTTP请求的完整信息
- 🧹 资源清理:在请求结束时清理ThreadLocal
TraceIdFilter - 追踪ID过滤器
java
package com.simpleflow.log.web.filter;
import com.simpleflow.log.context.ThreadLocalTraceHolder;
import com.simpleflow.log.context.TraceContext;
import com.simpleflow.log.util.RequestIdGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 追踪ID过滤器
*
* 为每个HTTP请求初始化或传递追踪上下文
* 支持从HTTP头中读取上游传递的追踪信息
*/
@Component
@Order(1) // 最高优先级
public class TraceIdFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TraceIdFilter.class);
// HTTP头常量
private static final String HEADER_REQUEST_ID = "X-Request-ID";
private static final String HEADER_TRACE_ID = "X-Trace-ID";
private static final String HEADER_USER_ID = "X-User-ID";
private static final String HEADER_SESSION_ID = "X-Session-ID";
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
// 初始化追踪上下文
TraceContext traceContext = initializeTraceContext(httpRequest);
// 将追踪ID写入响应头
writeTraceHeaders(httpResponse, traceContext);
logger.debug("请求开始 - URI: {}, 追踪信息: {}",
httpRequest.getRequestURI(), traceContext);
// 继续处理请求
chain.doFilter(request, response);
} finally {
// 清理ThreadLocal,防止内存泄漏
ThreadLocalTraceHolder.clearCurrentTrace();
logger.debug("请求结束 - URI: {}, 上下文已清理", httpRequest.getRequestURI());
}
}
/**
* 初始化追踪上下文
*/
private TraceContext initializeTraceContext(HttpServletRequest request) {
// 尝试从HTTP头中获取上游传递的追踪信息
String requestId = getHeaderValue(request, HEADER_REQUEST_ID);
String traceId = getHeaderValue(request, HEADER_TRACE_ID);
String userId = getHeaderValue(request, HEADER_USER_ID);
String sessionId = getHeaderValue(request, HEADER_SESSION_ID);
TraceContext traceContext;
if (traceId != null) {
// 如果有上游追踪ID,继续使用
traceContext = new TraceContext(
requestId != null ? requestId : RequestIdGenerator.generate(),
traceId
);
logger.debug("继续上游追踪链路 - TraceID: {}", traceId);
} else {
// 否则创建新的追踪链路
traceContext = ThreadLocalTraceHolder.initTrace();
logger.debug("开始新的追踪链路 - TraceID: {}", traceContext.getTraceId());
}
// 设置用户和会话信息
if (userId != null) {
traceContext.setUserId(userId);
}
if (sessionId != null) {
traceContext.setSessionId(sessionId);
}
// 添加HTTP请求信息
traceContext.addExtra("method", request.getMethod());
traceContext.addExtra("uri", request.getRequestURI());
traceContext.addExtra("remoteAddr", getClientIpAddress(request));
traceContext.addExtra("userAgent", request.getHeader("User-Agent"));
// 设置到ThreadLocal
ThreadLocalTraceHolder.setCurrentTrace(traceContext);
return traceContext;
}
/**
* 将追踪信息写入响应头
*/
private void writeTraceHeaders(HttpServletResponse response, TraceContext traceContext) {
if (traceContext != null) {
response.setHeader(HEADER_REQUEST_ID, traceContext.getRequestId());
response.setHeader(HEADER_TRACE_ID, traceContext.getTraceId());
if (traceContext.getUserId() != null) {
response.setHeader(HEADER_USER_ID, traceContext.getUserId());
}
}
}
/**
* 获取HTTP头值
*/
private String getHeaderValue(HttpServletRequest request, String headerName) {
String value = request.getHeader(headerName);
return (value != null && !value.trim().isEmpty()) ? value.trim() : null;
}
/**
* 获取客户端真实IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
return request.getRemoteAddr();
}
}
RequestLogFilter - 请求日志过滤器
java
package com.simpleflow.log.web.filter;
import com.simpleflow.log.context.ThreadLocalTraceHolder;
import com.simpleflow.log.web.utils.RequestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 请求日志过滤器
*
* 记录HTTP请求的详细信息,包括请求参数、响应状态、执行时间等
*/
@Component
@Order(2) // 在TraceIdFilter之后执行
public class RequestLogFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RequestLogFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
long startTime = System.currentTimeMillis();
String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
try {
// 记录请求开始日志
logRequestStart(httpRequest, requestId);
// 继续处理请求
chain.doFilter(request, response);
// 记录请求完成日志
long duration = System.currentTimeMillis() - startTime;
logRequestEnd(httpRequest, httpResponse, requestId, duration);
} catch (Exception e) {
// 记录请求异常日志
long duration = System.currentTimeMillis() - startTime;
logRequestError(httpRequest, requestId, duration, e);
throw e;
}
}
/**
* 记录请求开始日志
*/
private void logRequestStart(HttpServletRequest request, String requestId) {
if (!logger.isInfoEnabled()) {
return;
}
String method = request.getMethod();
String uri = request.getRequestURI();
String queryString = request.getQueryString();
String clientIp = RequestUtils.getClientIpAddress(request);
StringBuilder logMsg = new StringBuilder();
logMsg.append("HTTP请求开始 - ");
logMsg.append("RequestID: ").append(requestId).append(", ");
logMsg.append("Method: ").append(method).append(", ");
logMsg.append("URI: ").append(uri);
if (queryString != null) {
logMsg.append("?").append(queryString);
}
logMsg.append(", ClientIP: ").append(clientIp);
// 记录重要的HTTP头
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
logMsg.append(", UserAgent: ").append(userAgent);
}
String contentType = request.getContentType();
if (contentType != null) {
logMsg.append(", ContentType: ").append(contentType);
}
logger.info(logMsg.toString());
}
/**
* 记录请求完成日志
*/
private void logRequestEnd(HttpServletRequest request, HttpServletResponse response,
String requestId, long duration) {
if (!logger.isInfoEnabled()) {
return;
}
String method = request.getMethod();
String uri = request.getRequestURI();
int status = response.getStatus();
StringBuilder logMsg = new StringBuilder();
logMsg.append("HTTP请求完成 - ");
logMsg.append("RequestID: ").append(requestId).append(", ");
logMsg.append("Method: ").append(method).append(", ");
logMsg.append("URI: ").append(uri).append(", ");
logMsg.append("Status: ").append(status).append(", ");
logMsg.append("Duration: ").append(duration).append("ms");
// 根据响应状态选择日志级别
if (status >= 500) {
logger.error(logMsg.toString());
} else if (status >= 400) {
logger.warn(logMsg.toString());
} else {
logger.info(logMsg.toString());
}
}
/**
* 记录请求异常日志
*/
private void logRequestError(HttpServletRequest request, String requestId,
long duration, Exception e) {
String method = request.getMethod();
String uri = request.getRequestURI();
StringBuilder logMsg = new StringBuilder();
logMsg.append("HTTP请求异常 - ");
logMsg.append("RequestID: ").append(requestId).append(", ");
logMsg.append("Method: ").append(method).append(", ");
logMsg.append("URI: ").append(uri).append(", ");
logMsg.append("Duration: ").append(duration).append("ms, ");
logMsg.append("Error: ").append(e.getMessage());
logger.error(logMsg.toString(), e);
}
}
LogHandlerInterceptor - 拦截器实现
java
package com.simpleflow.log.web.interceptor;
import com.simpleflow.log.context.ThreadLocalTraceHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 日志处理拦截器
*
* 在Controller方法执行前后记录详细的处理信息
*/
@Component
public class LogHandlerInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LogHandlerInterceptor.class);
private static final String START_TIME_ATTRIBUTE = "startTime";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (!logger.isDebugEnabled()) {
return true;
}
// 记录开始时间
request.setAttribute(START_TIME_ATTRIBUTE, System.currentTimeMillis());
String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
String handlerName = handler.getClass().getSimpleName();
logger.debug("Controller处理开始 - RequestID: {}, Handler: {}, URI: {}",
requestId, handlerName, request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
if (!logger.isDebugEnabled()) {
return;
}
Long startTime = (Long) request.getAttribute(START_TIME_ATTRIBUTE);
if (startTime != null) {
long duration = System.currentTimeMillis() - startTime;
String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
logger.debug("Controller处理完成 - RequestID: {}, Duration: {}ms, Status: {}",
requestId, duration, response.getStatus());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
if (ex != null) {
String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
logger.error("Controller处理异常 - RequestID: {}, Error: {}",
requestId, ex.getMessage(), ex);
}
// 清理请求属性
request.removeAttribute(START_TIME_ATTRIBUTE);
}
}
RequestUtils - 请求工具类
java
package com.simpleflow.log.web.utils;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 请求工具类
*
* 提供HTTP请求信息提取的工具方法
*/
public class RequestUtils {
/**
* 获取客户端真实IP地址
*/
public static String getClientIpAddress(HttpServletRequest request) {
// 检查是否通过代理
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
// 多级代理的情况,第一个IP为客户端真实IP
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}
String proxyClientIp = request.getHeader("Proxy-Client-IP");
if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) {
return proxyClientIp;
}
String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
return wlProxyClientIp;
}
return request.getRemoteAddr();
}
/**
* 获取所有请求头
*/
public static Map<String, String> getAllHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headers.put(headerName, headerValue);
}
return headers;
}
/**
* 获取所有请求参数
*/
public static Map<String, String[]> getAllParameters(HttpServletRequest request) {
return new HashMap<>(request.getParameterMap());
}
/**
* 构建完整的请求URL
*/
public static String getFullRequestUrl(HttpServletRequest request) {
StringBuilder url = new StringBuilder();
url.append(request.getScheme()).append("://");
url.append(request.getServerName());
if (request.getServerPort() != 80 && request.getServerPort() != 443) {
url.append(":").append(request.getServerPort());
}
url.append(request.getRequestURI());
if (request.getQueryString() != null) {
url.append("?").append(request.getQueryString());
}
return url.toString();
}
/**
* 检查是否为Ajax请求
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String requestedWith = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equals(requestedWith);
}
/**
* 获取用户Agent信息
*/
public static String getUserAgent(HttpServletRequest request) {
return request.getHeader("User-Agent");
}
}
Web配置类
java
package com.simpleflow.log.web.config;
import com.simpleflow.log.web.interceptor.LogHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web日志配置
*/
@Configuration
public class WebLogConfiguration implements WebMvcConfigurer {
@Autowired
private LogHandlerInterceptor logHandlerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logHandlerInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/health", "/actuator/**");
}
}
实战测试
java
@RestController
@RequestMapping("/api/users")
@LogClass(prefix = "用户API")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
@LogMethod(logArgs = true, logResult = true)
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
@LogMethod(sensitiveFields = {"password"})
public ResponseEntity<User> createUser(@RequestBody UserCreateRequest request) {
User user = userService.createUser(request);
return ResponseEntity.ok(user);
}
}
日志输出示例:
yaml
2024-08-23 10:30:15.123 INFO - HTTP请求开始 - RequestID: REQ_20240823103015123_host_001, Method: GET, URI: /api/users/1, ClientIP: 192.168.1.100
2024-08-23 10:30:15.124 DEBUG - Controller处理开始 - RequestID: REQ_20240823103015123_host_001, Handler: UserController, URI: /api/users/1
2024-08-23 10:30:15.125 INFO - 用户API - UserService.findById开始执行,参数:[1]
2024-08-23 10:30:15.150 INFO - 用户API - UserService.findById执行成功,耗时:25ms,返回值:{"id":1,"name":"张三"}
2024-08-23 10:30:15.151 DEBUG - Controller处理完成 - RequestID: REQ_20240823103015123_host_001, Duration: 27ms, Status: 200
2024-08-23 10:30:15.152 INFO - HTTP请求完成 - RequestID: REQ_20240823103015123_host_001, Method: GET, URI: /api/users/1, Status: 200, Duration: 29ms
本章小结
✅ 完成的任务
- TraceIdFilter:实现请求级别的追踪上下文初始化
- RequestLogFilter:记录HTTP请求的完整生命周期
- LogHandlerInterceptor:在Controller层面记录处理信息
- RequestUtils:提供HTTP请求信息提取工具
- 集成配置:完成Web环境的自动配置
🎯 学习要点
- 过滤器链的执行顺序和职责分工
- HTTP头传递实现分布式追踪
- ThreadLocal管理在Web环境中的重要性
- 资源清理防止内存泄漏的机制
💡 思考题
- 如何处理文件上传请求的日志记录?
- 微服务间如何传递追踪信息?
- 如何优化高并发下的日志性能?
🚀 下章预告
下一章我们将开发Spring Boot Starter,学习如何创建自己的自动配置模块,让框架可以像官方组件一样开箱即用。
💡 设计原则 : Web集成的关键在于无侵入、全覆盖、高性能。通过Filter和Interceptor的配合,我们实现了HTTP请求的全链路追踪。