package com.xiaopeng.account.config.log; import com.xiaopeng.common.log.config.LogHeaderProperties; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; 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.*; /** * 请求/响应日志过滤器 * 打印:traceId、method、uri、queryString、请求体、响应状态、耗时、响应体 */ @Slf4j @Component @Order(Ordered.HIGHEST_PRECEDENCE + 10) public class RequestResponseLoggingFilter extends OncePerRequestFilter { @Autowired private LogHeaderProperties logHeaderProperties; /** 不打印 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()); if (logHeaderProperties.isEnabled()) { String headers = extractHeaders(request); log.info(">>> [{}] {} {}{} | headers={} | body={}", traceId, method, uri, query != null ? "?" + query : "", headers, body); } else { log.info(">>> [{}] {} {}{} | body={}", traceId, method, uri, query != null ? "?" + query : "", body); } } private String extractHeaders(HttpServletRequest request) { List names = logHeaderProperties.getNames(); StringBuilder sb = new StringBuilder("{"); boolean first = true; if (names != null && !names.isEmpty()) { // 只打印配置的请求头 for (String name : names) { String value = request.getHeader(name); if (value != null) { if (!first) sb.append(", "); first = false; sb.append(name).append("=").append(value); } } } else { // 未配置则打印全部请求头 Enumeration headerNames = request.getHeaderNames(); while (headerNames != null && headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String value = request.getHeader(name); if (!first) sb.append(", "); first = false; sb.append(name).append("=").append(value); } } return sb.append("}").toString(); } 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); } }