优化网关和feign基础日志排查

This commit is contained in:
2025-11-14 11:52:24 +08:00
parent 5b479ff90a
commit 5d7421da8d
66 changed files with 654 additions and 1673 deletions

View File

@@ -1,11 +1,13 @@
package com.sdm.gateway2;
import com.sdm.common.config.CustomLoadBalancerClient;
import com.sdm.gateway2.route.CustomLoadBalancerClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.context.annotation.ComponentScan;
/**
* gateway 网关部署到测试环境请求一直无响应,已废弃使用 gateway2作为部署

View File

@@ -1,68 +0,0 @@
package com.sdm.gateway2.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.util.UUID;
@Slf4j
@Component
public class RequestLoggingFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 生成请求追踪ID
String traceId = UUID.randomUUID().toString().replace("-", "");
ServerHttpRequest request = exchange.getRequest();
String method = request.getMethod().toString();
String path = request.getURI().getPath();
String query = request.getURI().getQuery();
String clientIp = getClientIp(request);
// 记录请求开始日志
log.info("[{}] {} {}?{} from {}", traceId, method, path, query != null ? query : "", clientIp);
long startTime = System.currentTimeMillis();
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 记录响应日志
log.info("[{}] {} {} returned status {} in {} ms",
traceId, method, path, response.getStatusCode(), duration);
})
);
}
private String getClientIp(ServerHttpRequest request) {
String xForwardedFor = request.getHeaders().getFirst("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeaders().getFirst("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
InetSocketAddress remoteAddress = request.getRemoteAddress();
return remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : "unknown";
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1,211 @@
package com.sdm.gateway2.filter;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 网关请求响应日志记录过滤器
* 集成了TraceId生成与传递功能
*/
@Slf4j
@Component
public class RequestResponseLoggingFilter implements GlobalFilter, Ordered {
/**
* MDC 中存储 traceId 的 key需与日志格式中的 %X{traceId} 对应)
*/
public static final String TRACE_ID_KEY = "traceId";
/**
* 请求头/响应头中传递 traceId 的 key
*/
public static final String TRACE_ID_HEADER = "X-Trace-Id";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 生成或获取 traceId优先从请求头获取支持前端传递便于联调
String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID_HEADER);
if (traceId == null || traceId.trim().isEmpty()) {
// 生成 UUID 并去除横杠32位简洁易读
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 存入 MDC供日志打印使用所有日志框架可通过 %X{traceId} 获取)
MDC.put(TRACE_ID_KEY, traceId);
// 写入响应头(返回给前端,便于前端排查问题时匹配日志)
// 只在响应头中还没有X-Trace-Id时才设置避免与下游服务设置的TraceId重复
ServerHttpResponse response = exchange.getResponse();
if (!response.getHeaders().containsKey(TRACE_ID_HEADER)) {
response.getHeaders().set(TRACE_ID_HEADER, traceId);
}
// 构建带有 traceId 的新请求
ServerHttpRequest request = exchange.getRequest().mutate()
.header(TRACE_ID_HEADER, traceId)
.build();
ServerHttpRequest finalRequest = request;
String method = request.getMethod().toString();
String path = request.getURI().getPath();
String query = request.getURI().getQuery();
String clientIp = getClientIp(request);
// 记录请求详细信息
StringBuilder requestLog = new StringBuilder();
requestLog.append("\n==================== 网关接收到请求 ====================\n");
requestLog.append("TraceId: ").append(traceId).append("\n");
requestLog.append("请求方法: ").append(method).append("\n");
requestLog.append("请求路径: ").append(path).append("\n");
requestLog.append("请求参数: ").append(query != null ? query : "").append("\n");
requestLog.append("客户端IP: ").append(clientIp).append("\n");
// 记录请求头
requestLog.append("请求头:\n");
for (Map.Entry<String, List<String>> header : request.getHeaders().entrySet()) {
requestLog.append(" ").append(header.getKey()).append(": ").append(header.getValue()).append("\n");
}
requestLog.append("========================================================");
log.info(requestLog.toString());
long startTime = System.currentTimeMillis();
// 包装响应以捕获响应数据
DataBufferFactory bufferFactory = response.bufferFactory();
String finalTraceId = traceId;
String finalTraceId1 = traceId;
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
// 记录响应信息
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
StringBuilder responseLog = new StringBuilder();
responseLog.append("\n==================== 网关响应信息 ====================\n");
responseLog.append("TraceId: ").append(finalTraceId).append("\n");
responseLog.append("请求方法: ").append(method).append("\n");
responseLog.append("请求路径: ").append(path).append("\n");
responseLog.append("响应状态: ").append(getStatusCode()).append("\n");
responseLog.append("处理时间: ").append(duration).append("ms\n");
// 记录响应头
responseLog.append("响应头:\n");
for (Map.Entry<String, List<String>> header : getHeaders().entrySet()) {
responseLog.append(" ").append(header.getKey()).append(": ").append(String.join(",", header.getValue())).append("\n");
}
// 注意:记录响应体可能会影响性能,特别是对于大文件
// 如果需要记录响应体,可以取消下面的注释,但要注意性能影响
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
String responseBody = new String(content, StandardCharsets.UTF_8);
DataBuffer newBuffer = bufferFactory.wrap(content);
responseLog.append("响应体: ").append(responseBody).append("\n");
responseLog.append("========================================================");
log.info(responseLog.toString());
return newBuffer;
}));
}
return super.writeWith(body);
}
// 处理零字节响应的情况
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
StringBuilder responseLog = new StringBuilder();
responseLog.append("\n==================== 网关响应信息 ====================\n");
responseLog.append("TraceId: ").append(finalTraceId1).append("\n");
responseLog.append("请求方法: ").append(method).append("\n");
responseLog.append("请求路径: ").append(path).append("\n");
responseLog.append("响应状态: ").append(getStatusCode()).append("\n");
responseLog.append("处理时间: ").append(duration).append("ms\n");
// 记录响应头
responseLog.append("响应头:\n");
for (Map.Entry<String, List<String>> header : getHeaders().entrySet()) {
responseLog.append(" ").append(header.getKey()).append(": ").append(header.getValue()).append("\n");
}
responseLog.append("========================================================");
log.info(responseLog.toString());
return super.writeAndFlushWith(body);
}
};
// 将装饰后的响应和带 traceId 的请求替换到 exchange 中
return chain.filter(exchange.mutate()
.request(finalRequest)
.response(decoratedResponse)
.build())
.doFinally(signalType -> {
// 清除 MDC 中的 traceId关键避免线程池复用导致的 traceId 污染)
MDC.remove(TRACE_ID_KEY);
});
}
private String getClientIp(ServerHttpRequest request) {
String xForwardedFor = request.getHeaders().getFirst("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeaders().getFirst("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
// 检查Origin头部
String origin = request.getHeaders().getFirst("Origin");
if (origin != null && !origin.isEmpty()) {
// Origin格式为 http://domain:port 或 https://domain:port
try {
String[] parts = origin.split("://");
if (parts.length > 1) {
String hostPort = parts[1];
String[] hostPortParts = hostPort.split(":");
return hostPortParts[0];
}
} catch (Exception e) {
// 解析失败则继续使用其他方式获取IP
}
}
InetSocketAddress remoteAddress = request.getRemoteAddress();
return remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : "unknown";
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1; // 确保在大多数其他过滤器之前执行
}
}

View File

@@ -0,0 +1,86 @@
package com.sdm.gateway2.route;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Component
public class CustomLoadBalancerClient implements ReactorServiceInstanceLoadBalancer {
@Value("${serverType:1}")
private int serverType;
@Value("${serverIp:}")
private String serverIp;
private final AtomicInteger atomicInteger = new AtomicInteger(0);
// 服务列表
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
public CustomLoadBalancerClient(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
assert supplier != null;
return supplier.get().next().map(serviceInstances -> getInstanceResponse(serviceInstances, request));
}
/**
* 负载均衡获取服务
*
* @param instances instances
* @return Response<ServiceInstance>
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
if (instances.isEmpty()) {
log.error("服务器列表为空");
return new EmptyResponse();
}
ServiceInstance serviceInstance = null;
// 0单机处理1负载均衡轮询
if (serverType == 0) {
for (ServiceInstance instance : instances) {
if (instance.getHost().equals(serverIp)) {
serviceInstance = instance;
break;
}
}
if (serviceInstance != null) {
log.info("转发到指定服务器:" + serviceInstance.getHost());
} else {
log.warn("未找到指定IP的服务器使用列表中的第一个服务器");
serviceInstance = instances.get(0);
}
} else {
// 获取当前的调用编号(每来一次请求则累加1)
int sequence = atomicInteger.getAndIncrement();
// 达到1000清零
if (sequence == 1000) {
atomicInteger.set(0);
}
// 调用编号与服务器个数取余
int index = sequence % instances.size();
log.info("负载均衡到服务器:" + instances.get(index).getHost());
serviceInstance = instances.get(index);
}
return new DefaultResponse(serviceInstance);
}
}

View File

@@ -1,6 +1,6 @@
spring:
profiles:
active: dev
active: local
main:
web-application-type: reactive
cloud:

View File

@@ -6,7 +6,9 @@
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr([%15.15t]){faint} %clr(%logger){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr([%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr([%15.15t]){faint} %clr(%logger){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<!-- 普通日志格式(无颜色) -->
<property name="FILE_LOG_PATTERN" value="[%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } [%15.15t] %logger : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" />
<!-- 日志文件存储地址 -->
<property name="LOG_HOME" value="${LOG_PATH:-/home/app/gateway2/logs}" />
@@ -39,16 +41,41 @@
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{36} - %msg%n</pattern>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 4. core.log 专用输出器(保留 callerInfo 格式) -->
<appender name="CORE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/core.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/core.log.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
<TotalSizeCap>500MB</TotalSizeCap>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- 仅 core.log 显示真实调用位置(类名.方法名(行号) -->
<pattern>[%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:- } [%15.15t] %X{callerInfo} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
<!-- 绑定
coreLogger → 输出到 core.log + 控制台 -->
<logger name="coreLogger" level="INFO" additivity="false">
<appender-ref ref="CORE_FILE" /> <!-- 核心日志写入 core.log -->
<appender-ref ref="STDOUT" /> <!-- 同时输出到控制台(显示 CoreLogger -->
</logger>
<!-- 特定包的日志级别 -->
<!-- <logger name="com.sdm.gateway2" level="INFO" />
<logger name="org.springframework.cloud.gateway" level="INFO" />