优化网关和feign基础日志排查
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.sdm.common.common.mdc;
|
||||
package com.sdm.common.interceptor;
|
||||
|
||||
import com.sdm.common.common.ThreadLocalContext;
|
||||
import feign.RequestInterceptor;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.sdm.common.utils.log;
|
||||
package com.sdm.common.log;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
110
common/src/main/java/com/sdm/common/log/CustomFeignLogger.java
Normal file
110
common/src/main/java/com/sdm/common/log/CustomFeignLogger.java
Normal 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.FULL),Feign 会在执行 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user