package com.xiaopeng.trafficControl.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);
|
}
|
}
|