优化网关和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

@@ -4,10 +4,7 @@ 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.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.stereotype.Component;

View File

@@ -0,0 +1,109 @@
package com.sdm.common.config;
import com.sdm.common.log.CustomFeignLogger;
import feign.Logger;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import static feign.Util.UTF_8;
/**
* 1. Spring Cloud OpenFeign 的自动配置机制
* 在 Spring Cloud OpenFeign 中,有一个自动配置类 FeignClientsConfiguration它负责为 Feign 客户端提供默认的配置。
* 这个配置类会检查 Spring 容器中是否存在用户自定义的 Bean如果存在就使用用户自定义的否则使用默认的。FeignLoggingConfig 正是通过提供这些自定义 Bean 来改变 Feign 的行为。
* 2. 三个关键 Bean 的作用
* 2.1 Logger.Level Bean
* 这个 Bean 告诉 Feign 应该记录什么级别的日志:
* NONE不记录任何日志默认
* BASIC只记录请求方法、URL、响应状态码和执行时间
* HEADERS记录基本信息以及请求和响应的头信息
* FULL记录完整的请求和响应信息包括头信息、请求体和响应体
* 通过返回 Logger.Level.FULL我们告诉 Feign 要记录完整的请求和响应信息。
*
* 2.2 Logger Bean
* 这个 Bean 提供了具体的日志记录实现。Feign 会使用这个 Logger 来实际记录日志。
* 当我们返回 CustomFeignLogger 实例时Feign 就会使用我们自定义的日志记录逻辑,而不是默认的 Slf4jLogger。
*
* 2.3 ErrorDecoder Bean
* 这个 Bean 提供了自定义的错误解码器。当 Feign 调用返回错误状态码(如 4xx 或 5xx会使用这个解码器来处理错误。
*
* 3. 配置的协同工作
* 为了让这些配置生效,还需要配合 application.yml 中的配置:
* logging:
* level:
* # 设置Feign客户端所在包的日志级别为DEBUG
* com.sdm.common.feign: DEBUG
* # 设置Feign客户端接口的日志级别
* com.sdm.common.feign.inter: DEBUG
*
* # Feign客户端配置
* feign:
* client:
* config:
* # 全局默认配置
* default:
* loggerLevel: FULL
*
*
*实际执行过程
* 当 Feign 发起一个请求时,实际的执行顺序是:
* Feign 框架检查 FeignLoggingConfig 设置的日志级别FULL
* Feign 收集完整的请求信息
* Feign 调用 CustomFeignLogger 的 logRequest 方法
* CustomFeignLogger 检查 logger.isDebugEnabled()(这会查看 application.yml 的配置)
* 如果允许DEBUG 级别),就构造日志信息并调用 logger.debug() 输出
*/
@Configuration
public class FeignLoggingConfig {
/**
* 自定义Feign日志级别为FULL记录完整的请求和响应信息
*/
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
/**
* 自定义Feign Logger使用Slf4j记录日志
*/
@Bean
public Logger feignLogger() {
return new CustomFeignLogger();
}
/**
* 自定义错误解码器,用于记录错误响应
*/
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
/**
* 自定义错误解码器实现类
*/
public static class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new ErrorDecoder.Default();
@Override
public Exception decode(String methodKey, Response response) {
// 记录错误响应
try {
String body = Util.toString(response.body().asReader(UTF_8));
LoggerFactory.getLogger("FeignClient").error("Feign调用出错方法: {},状态码: {},响应体: {}",
methodKey, response.status(), body);
} catch (IOException e) {
LoggerFactory.getLogger("FeignClient").error("读取Feign错误响应失败", e);
}
return defaultErrorDecoder.decode(methodKey, response);
}
}
}

View File

@@ -1,5 +1,6 @@
package com.sdm.common.common.mdc;
package com.sdm.common.config;
import com.sdm.common.filter.TraceIdFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@@ -1,4 +1,4 @@
package com.sdm.common.common.mdc;
package com.sdm.common.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
@@ -42,7 +42,7 @@ public class TraceIdFilter implements Filter {
MDC.put(TRACE_ID_KEY, traceId);
// 3. 写入响应头返回给前端便于前端排查问题时匹配日志
response.setHeader(TRACE_ID_HEADER, traceId);
// response.setHeader(TRACE_ID_HEADER, traceId);
// 4. 放行请求继续执行后续过滤器/控制器
filterChain.doFilter(request, response);

View File

@@ -1,4 +1,4 @@
package com.sdm.common.common.mdc;
package com.sdm.common.interceptor;
import com.sdm.common.common.ThreadLocalContext;
import feign.RequestInterceptor;

View File

@@ -1,4 +1,4 @@
package com.sdm.common.utils.log;
package com.sdm.common.log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -0,0 +1,110 @@
package com.sdm.common.log;
import feign.Request;
import feign.Response;
import feign.Util;
import feign.slf4j.Slf4jLogger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import static feign.Util.UTF_8;
import static feign.Util.decodeOrDefault;
/**
* 自定义Feign日志记录器
* 用于记录详细的请求和响应信息
* 特性:
* 1. Feign 框架的设计机制
* Feign 是一个声明式的 HTTP 客户端,它允许开发者通过接口定义来调用 HTTP API。在 Feign 内部,有一个日志记录机制,但默认情况下它是不启用的。
* 当您配置了 Feign 的日志级别(如我们在 FeignLoggingConfig 中设置的 Logger.Level.FULLFeign 会在执行 HTTP 请求的过程中调用相应的日志记录方法。
*
* 2. Feign Logger 类的继承体系
* CustomFeignLogger 继承自 Slf4jLogger而 Slf4jLogger 又继承自 Feign 的基础 Logger 类。
*
* 3. 关键方法的调用时机
* 3.1 请求日志记录 - logRequest 方法
* 当 Feign 发起一个 HTTP 请求时,会调用 logRequest 方法
* 在这个方法中,我们可以访问到:
* configKey标识哪个 Feign 客户端方法被调用
* logLevel日志级别
* request完整的请求对象包括方法、URL、头部和请求体
*
* 3.2 响应日志记录 - logAndRebufferResponse 方法
* 当 Feign 收到 HTTP 响应时,会调用 logAndRebufferResponse 方法:
* 在这个方法中,我们可以访问到:
* configKey标识哪个 Feign 客户端方法被调用
* logLevel日志级别
* response完整的响应对象包括状态码、头部和响应体
* elapsedTime请求处理耗时
*
*
* 4. Feign 内部执行流程
* Feign 的执行流程大致如下:
* 当调用一个 Feign 客户端方法时Feign 会构建一个 HTTP 请求
* 在发送请求前Feign 调用配置的 Logger 的 logRequest 方法记录请求信息
* Feign 发送 HTTP 请求并等待响应
* 收到响应后Feign 调用配置的 Logger 的 logAndRebufferResponse 方法记录响应信息
* Feign 将响应返回给调用者
*
* 5. 配置的协同工作
* 为了让 CustomFeignLogger 正常工作,需要配合以下配置:
* FeignLoggingConfig 中注册 CustomFeignLogger 为 Bean
* logback.xml 中设置 FeignClient 的日志级别为 INFO
* application.yml 中设置 Feign 的 loggerLevel 为 FULL
* 只有当这些配置协同工作时Feign 才会调用我们的自定义日志记录器来记录详细的请求和响应信息。
*/
public class CustomFeignLogger extends Slf4jLogger {
private final Logger logger = LoggerFactory.getLogger("FeignClient");
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
StringBuilder sb = new StringBuilder();
sb.append("\n==================== Feign 请求信息 ====================\n");
sb.append("请求方法: ").append(request.httpMethod().name()).append("\n");
sb.append("请求URL: ").append(request.url()).append("\n");
sb.append("请求头: ").append(request.headers()).append("\n");
if (request.body() != null) {
sb.append("请求体: ").append(decodeOrDefault(request.body(), UTF_8, "Binary data")).append("\n");
}
sb.append("========================================================");
logger.info(sb.toString());
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("\n==================== Feign 响应信息 ====================\n");
sb.append("响应状态: ").append(response.status()).append("\n");
sb.append("响应头: ").append(response.headers()).append("\n");
sb.append("响应时间: ").append(elapsedTime).append("ms\n");
if (response.body() != null) {
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
sb.append("响应体: ").append(decodeOrDefault(bodyData, UTF_8, "Binary data")).append("\n");
// 重新构建响应体,以便后续处理
response = response.toBuilder().body(bodyData).build();
}
sb.append("========================================================");
logger.info(sb.toString());
return response;
}
@Override
protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
logger.info("==================== Feign 请求异常 ====================\n" +
"请求接口: {}\n" +
"异常信息: {}\n" +
"耗时: {}ms\n" +
"========================================================",
configKey, ioe.getMessage(), elapsedTime);
return super.logIOException(configKey, logLevel, ioe, elapsedTime);
}
}

View File

@@ -1,4 +1,4 @@
package com.sdm.common.common.mdc;
package com.sdm.common.mdc;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;