第7篇:Web集成模块 - HTTP请求的全链路追踪

前言

前面我们实现了链路追踪的核心机制,本章将其与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

本章小结

✅ 完成的任务

  1. TraceIdFilter:实现请求级别的追踪上下文初始化
  2. RequestLogFilter:记录HTTP请求的完整生命周期
  3. LogHandlerInterceptor:在Controller层面记录处理信息
  4. RequestUtils:提供HTTP请求信息提取工具
  5. 集成配置:完成Web环境的自动配置

🎯 学习要点

  • 过滤器链的执行顺序和职责分工
  • HTTP头传递实现分布式追踪
  • ThreadLocal管理在Web环境中的重要性
  • 资源清理防止内存泄漏的机制

💡 思考题

  1. 如何处理文件上传请求的日志记录?
  2. 微服务间如何传递追踪信息?
  3. 如何优化高并发下的日志性能?

🚀 下章预告

下一章我们将开发Spring Boot Starter,学习如何创建自己的自动配置模块,让框架可以像官方组件一样开箱即用。


💡 设计原则 : Web集成的关键在于无侵入、全覆盖、高性能。通过Filter和Interceptor的配合,我们实现了HTTP请求的全链路追踪。

相关推荐
Liquad Li6 小时前
ABP vNext 标准分层解决方案项目结构完整解析
后端
半夜燃烧的香烟6 小时前
docker 安装minio nginx,配置nginx根据文根路由minio展示图片
java·nginx·docker
吴阿福|一人公司6 小时前
深度解析 Python 类变量修改的命名空间隔离
java·服务器·数据结构
zzz_23686 小时前
【Java基础】链表的七十二变——从LRU缓存到手写浏览器前进后退
java·链表·缓存
番茄去哪了6 小时前
神领物流面试题(一)
java·大数据·中间件
云烟成雨TD6 小时前
Agent Scope Java 2.x 系列【9】接入高德 MCP 服务
java·人工智能·agent
布朗克1687 小时前
39 Spring Boot Web实战
前端·spring boot·后端·实战
gaohe26AIliuzeyu7 小时前
Java内部类
java·开发语言
西安邮电大学7 小时前
有关数组的经典算法题
java·后端·其他·算法·面试
山东点狮信息科技有限公司7 小时前
点狮HRM-HRM系统安全体系与数据保护方案
后端·安全·spring·spring cloud·微服务·系统安全·资产