| New file |
| | |
| | | package com.xiaopeng.vehicleConvoy.config.log; |
| | | |
| | | 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<String> EXCLUDE_PATHS = new HashSet<>(Arrays.asList( |
| | | "/actuator", "/healthcheck", "/health", "/prometheus" |
| | | )); |
| | | |
| | | /** 只对这些 Content-Type 打印 body(避免打印二进制流) */ |
| | | private static final Set<String> 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); |
| | | } |
| | | } |