package com.xiaopeng.automaticPilot.filter; import com.alibaba.fastjson2.JSON; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * 请求/响应日志过滤器 * 打印:traceId、method、uri、queryString、请求体、响应状态、耗时、响应体 */ @Slf4j @Component @Order(Ordered.HIGHEST_PRECEDENCE + 10) public class RequestResponseLoggingFilter extends OncePerRequestFilter { /** 不打印 body 的路径前缀(健康检查、actuator 等) */ private static final Set EXCLUDE_PATHS = new HashSet<>(Arrays.asList( "/actuator", "/healthcheck", "/health", "/prometheus" )); /** 只对这些 Content-Type 打印 body(避免打印二进制流) */ private static final Set LOGGABLE_CONTENT_TYPES = new HashSet<>(Arrays.asList( MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.TEXT_PLAIN_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE, "application/x-www-form-urlencoded" )); private static final int MAX_BODY_LENGTH = 2000; @Override protected boolean shouldNotFilter(HttpServletRequest request) { String uri = request.getRequestURI(); return EXCLUDE_PATHS.stream().anyMatch(uri::startsWith); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 初始化 MDC(Filter 先于 Interceptor 执行,在此统一设置) String traceId = request.getHeader("X-Trace-Id"); if (traceId == null || traceId.isEmpty()) { traceId = java.util.UUID.randomUUID().toString().replace("-", ""); } MDC.put("traceId", traceId); MDC.put("clientIp", getClientIp(request)); MDC.put("requestUri", request.getRequestURI()); response.setHeader("X-Trace-Id", traceId); ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper respWrapper = new ContentCachingResponseWrapper(response); long start = System.currentTimeMillis(); try { chain.doFilter(reqWrapper, respWrapper); } finally { long cost = System.currentTimeMillis() - start; logRequest(reqWrapper); logResponse(respWrapper, cost); // 必须把响应体写回,否则客户端收不到数据 respWrapper.copyBodyToResponse(); MDC.clear(); } } private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { return ip.split(",")[0].trim(); } ip = request.getHeader("X-Real-IP"); if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { return ip; } return request.getRemoteAddr(); } private void logRequest(ContentCachingRequestWrapper request) { String traceId = MDC.get("traceId"); String uri = request.getRequestURI(); String query = request.getQueryString(); String method = request.getMethod(); String body = extractBody(request.getContentAsByteArray(), request.getContentType()); log.info(">>> [{}] {} {}{} | body={}", traceId, method, uri, query != null ? "?" + query : "", body); } private void logResponse(ContentCachingResponseWrapper response, long cost) { String traceId = MDC.get("traceId"); int status = response.getStatus(); String body = extractBody(response.getContentAsByteArray(), response.getContentType()); log.info("<<< [{}] status={} cost={}ms | body={}", traceId, status, cost, body); } private String extractBody(byte[] bodyBytes, String contentType) { if (bodyBytes == null || bodyBytes.length == 0) { return ""; } if (!isLoggableContentType(contentType)) { return "[binary, " + bodyBytes.length + " bytes]"; } String body = new String(bodyBytes, StandardCharsets.UTF_8); if (body.length() > MAX_BODY_LENGTH) { body = body.substring(0, MAX_BODY_LENGTH) + "...(truncated)"; } return body; } private boolean isLoggableContentType(String contentType) { if (contentType == null) return true; String ct = contentType.toLowerCase(); return LOGGABLE_CONTENT_TYPES.stream().anyMatch(ct::contains); } }