我们在通过OpenFeign调用服务端接口默认是不打印日志的,这样我们就不知道具体调用的哪个远程方法以及出入参,如果出现异常,不利于通过日志定位及分析问题。如何开启OpenFeign的日志及设置日志级别可以参考之前写的这篇博客。
下面是Logger.Level.FULL级别下的OpenFeign日志内容。
ok,在讲如何自定义OpenFeign日志前,我们先探究下OpenFeign默认情况日志是咋搞得,探究之后知道它是咋工作的,便会很容易实现自定义日志。
一、探究OpenFeign默认使用的日志
当启动带有@FeignClient注解的项目时,Spring会为每一个被@FeignClient修饰的接口创建一个feign客户端实例,而这个feign客户端实例是通过FeignClient工厂得到的。跳过前面代码,直接看
FeignClient工厂通过上面这个方法创建一个带有指定数据及上下文的Feign客户端。其中第261行,需要先获得一个Feign的构建器,进入到该方法。
feign方法
通过85,86两行可以看出在创建Feign的构建器之前先得到一个Feign的日志工厂,并通过该工厂创建一个Logger实例。其中
// 获取FeignLoggerFactory实例
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
// 上面的get方法
protected <T> T get(FeignContext context, Class<T> type) {
T instance = context.getInstance(this.contextId, type);
if (instance == null) {
throw new IllegalStateException(
"No bean found of type " + type + " for " + this.contextId);
}
return instance;
}
我们看一下FeignLoggerFactory,它是一个接口,默认只有一个实现类DefaultFeignLoggerFactory。即通过上面的get方法获取到的是一个DefaultFeignLoggerFactory实例。
继续往下看
// 通过默认的Feign日志工厂DefaultFeignLoggerFactory创建Logger实例
Logger logger = loggerFactory.create(this.type);
/**
* 上面的create方法实际上调用的是DefaultFeignLoggerFactory中的create方法,
* 如下,可以看到new了一个Slf4jLogger,OpenFeign默认是使用Slf4jLogger处
* 理日志的
*/
@Override
public Logger create(Class<?> type) {
return this.logger != null ? this.logger : new Slf4jLogger(type);
}
既然使用的是Slf4jLogger来操作日志,我们不妨看一下它到底是个啥,下面粘贴这个类的源码。
Slf4jLogger类
package feign.slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import feign.Request;
import feign.Response;
/**
* Logs to SLF4J at the debug level, if the underlying logger has debug logging enabled. The
* underlying logger can be specified at construction-time, defaulting to the logger for
* {@link feign.Logger}.
*/
public class Slf4jLogger extends feign.Logger {
private final Logger logger;
public Slf4jLogger() {
this(feign.Logger.class);
}
public Slf4jLogger(Class<?> clazz) {
this(LoggerFactory.getLogger(clazz));
}
public Slf4jLogger(String name) {
this(LoggerFactory.getLogger(name));
}
Slf4jLogger(Logger logger) {
this.logger = logger;
}
// 记录请求日志
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
if (logger.isDebugEnabled()) {
super.logRequest(configKey, logLevel, request);
}
}
// 记录响应日志
@Override
protected Response logAndRebufferResponse(String configKey,
Level logLevel,
Response response,
long elapsedTime)
throws IOException {
if (logger.isDebugEnabled()) {
return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
}
return response;
}
@Override
protected void log(String configKey, String format, Object... args) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(methodTag(configKey) + format, args));
}
}
}
通过该类注释即方法可以看出,记录请求响应日志前先执行logger.isDebugEnabled()判断是否是debug级别。所以默认情况下必须在debug级别下才会打印日志。这也解释了为什么默认情况下要在配置文件中,配置logging.level.com.example.consumer.service=debug。我们还可以看出,Slf4jLogger记录请求及响应日志是调用父类方法,那么我们再看下其父类feign.Logger是个啥吧。
Logger类(只列举部分重要方法)
public abstract class Logger {
/**
* Override to log requests and responses using your own implementation. Messages will be http
* request and response text.
*/
protected abstract void log(String configKey, String format, Object... args);
protected void logRequest(String configKey, Level logLevel, Request request) {
log(configKey, "---> %s %s HTTP/1.1", request.httpMethod().name(), request.url());
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
for (String field : request.headers().keySet()) {
for (String value : valuesOrEmpty(request.headers(), field)) {
log(configKey, "%s: %s", field, value);
}
}
......
......
......
log(configKey, "---> END HTTP (%s-byte body)", bodyLength);
}
}
protected Response logAndRebufferResponse(String configKey,
Level logLevel,
Response response,
long elapsedTime)
throws IOException {
String reason =
response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ? " " + response.reason()
: "";
int status = response.status();
log(configKey, "<--- HTTP/1.1 %s%s (%sms)", status, reason, elapsedTime);
......
......
......
if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {
log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));
}
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
return response.toBuilder().body(bodyData).build();
} else {
log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);
}
}
return response;
}
我们可以看到Logger的logRequest和logAndRebufferResponse方法里的模板和我们控制台打印出来的日志信息很像。实际上也正是通过这两个方法记录请求响应日志信息的。
二、自定义OpenFeign日志
ok,通过前面的探究我们明白了OpenFeign默认情况下的日志是啥情况了。
首先我们要明确两件事:
- 默认是通过实现了FeignLoggerFactory接口的DefaultFeignLoggerFactory这个工厂类来创建Slf4jLogger。
- 实际是通过Slf4jLogger实现日志的,而记录请求和响应的操作是使用父类Logger的logRequest和logAndRebufferResponse方法。
明确了上面两件事为我们实现自定义OpenFeign日志提供了思路,首先创建实现了FeignLoggerFactory接口的自定义Feign日志工厂类,并且这个类提供创建自定义Logger实例的方法。让获取到的日志工厂是我们自定义的而不是DefaultFeignLoggerFactory。其次,我们自定义的Logger要实现feign.Logger并重写logRequest和logAndRebufferResponse方法,让请求响应日志按我们想要的样式打印。
以下举例两种实现自定义日志形式,一种是在原有的Slf4jLogger的日志基础上增加我们想要的内容,另一种是纯自定义,可以把Feign日志接入到其他日志中。
1、扩展Slf4jLogger日志内容
i)、CustomizationFeignLogger(新增自定义Feign日志类)
public class CustomizationFeignLogger extends feign.Logger {
private Logger logger;
public CustomizationFeignLogger() {
this(feign.Logger.class);
}
public CustomizationFeignLogger(Class<?> clazz) {
this.logger = new Slf4jLogger(clazz);
}
@Override
protected void log(String configKey, String format, Object... args) {
try {
Class<Slf4jLogger> clazz = Slf4jLogger.class;
// 通过反射获取Slf4jLogger的log方法
Method log = clazz.getDeclaredMethod("log", String.class, String.class, Object[].class);
// Slf4jLogger的log方法被protected修饰符修饰,需设置setAccessible(true)
log.setAccessible(true);
// 调用Slf4jLogger的log方法
log.invoke(logger, configKey, format, args);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
// 在Slf4jLogger日志的基础上新增的自定义扩展日志
log(configKey, "---> 我是在Slf4jLogger日志的基础上扩展的请求信息内容");
try {
Class<Slf4jLogger> clazz = Slf4jLogger.class;
// 通过反射获取Slf4jLogger的logRequest方法
Method logRequest = clazz.getDeclaredMethod("logRequest", String.class, Level.class, Request.class);
// Slf4jLogger的logRequest方法被protected修饰符修饰,需设置setAccessible(true)
logRequest.setAccessible(true);
// 调用Slf4jLogger的logRequest方法
logRequest.invoke(logger, configKey, logLevel, request);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
// 在Slf4jLogger日志的基础上新增的自定义扩展日志
log(configKey, "---> 我是在Slf4jLogger日志的基础上扩展的响应信息内容");
try {
Class<Slf4jLogger> clazz = Slf4jLogger.class;
// 通过反射获取Slf4jLogger的logAndRebufferResponse方法
Method logAndRebufferResponse = clazz.getDeclaredMethod("logAndRebufferResponse", String.class,
Level.class, Response.class, long.class);
// Slf4jLogger的logAndRebufferResponse方法被protected修饰符修饰,需设置setAccessible(true)
logAndRebufferResponse.setAccessible(true);
// 调用Slf4jLogger的logAndRebufferResponse方法
return (Response)logAndRebufferResponse.invoke(logger, configKey, logLevel, response,elapsedTime);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return response.toBuilder().body(new byte[]{}).build();
}
}
ii)、CustomizationFeignLoggerFactory(新增自定义Feign日志工厂类)
@Configuration
public class CustomizationFeignLoggerFactory implements FeignLoggerFactory {
private Logger logger;
public CustomizationFeignLoggerFactory() {
}
public CustomizationFeignLoggerFactory(Logger logger) {
this.logger = logger;
}
@Override
public Logger create(Class<?> type) {
return this.logger != null ? this.logger : new CustomizationFeignLogger(type);
}
}
测试看下效果 ,如下图,我们自定义扩展的内容确实加到了Feign的日志里。但是缺点也是有的,首先通过我上面的方式,扩展的内容只能加到请求或响应日志的前后。其次Feign的日志和我使用的统一日志风格不同且不能切入到项目使用的统一日志。
2、将Feign日志接入到统一日志(Slf4j)
i)、CustomizationFeignLogger(新增自定义Feign日志类)
import feign.Request;
import feign.Response;
import feign.Util;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import static feign.Util.UTF_8;
import static feign.Util.decodeOrDefault;
/**
* @Description 自定义Feign日志
**/
@Slf4j
public class CustomizableFeignLogger extends feign.Logger {
public CustomizableFeignLogger() {
}
@Override
protected void log(String configKey, String format, Object... args) {
}
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
Request.HttpMethod httpMethod = request.httpMethod();
String url = request.url();
String param = " ";
if (request.requestBody().asBytes() != null) {
param = new String(request.requestBody().asBytes(), UTF_8);
}
log.info(configKey + "---> {} url:{}, param:{}.", httpMethod, url, param);
}
@Override
protected void logRetry(String configKey, Level logLevel) {
log.info(configKey, "---> RETRYING");
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
String result = "";
try {
if (response.body() != null) {
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
result = decodeOrDefault(bodyData, UTF_8, "Binary data");
return response.toBuilder().body(bodyData).build();
}
return response;
} finally {
if (response.status() == 200) {
log.info(configKey + "---> result:{}.", result);
} else {
log.error(configKey + "---> result:{}.", result);
}
}
}
@Override
protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
log.info(configKey + "<--- ERROR {}: {}", ioe.getClass().getSimpleName(), ioe.getMessage());
StringWriter sw = new StringWriter();
ioe.printStackTrace(new PrintWriter(sw));
log.info(configKey + "{}", sw.toString());
return ioe;
}
}
ii)、CustomizationFeignLoggerFactory(新增自定义Feign日志工厂类)
import feign.Logger;
import org.springframework.cloud.openfeign.FeignLoggerFactory;
import org.springframework.context.annotation.Configuration;
/**
* @Description 自定义Feign日志工厂
**/
@Configuration
public class CustomizationFeignLoggerFactory implements FeignLoggerFactory {
private Logger logger;
public CustomizationFeignLoggerFactory() {
}
public CustomizationFeignLoggerFactory(Logger logger) {
this.logger = logger;
}
@Override
public Logger create(Class<?> type) {
return this.logger != null ? this.logger : new CustomizationFeignLogger();
}
}
测试看下效果 ,如下图,我们自定义扩展的内容确实加到了Feign的日志里。并且日志是统一日志的形式而不是原有的Feign的形式。
通过Slf4jLogger源码可知,在调用父类logRequest和logAndRebufferResponse方式前需要判断是否时debug级别。而我们自定义方法中没有判断,所以配置文件中可以去掉logging.level.com.example.consumer.service=debug。
---------------------------------------------------------------------------------------------------------------------------------
2021年12月28日补充
上面介绍的是通过debug的方式确定OpenFeign的日志工厂和Logger类实现。还有一种方式可以知道。有一个类FeignClientsConfiguration,它是Feign客户端的默认配置,在其中我们可以看到这样一个方法。
这个方式表示如果Spring容器中没有FeignLoggerFactory类型的bean,那么就new一个DafaultFeignLoggerFactory的bean。默认情况下是使用DafaultFeignLoggerFactory,如果想实现自定义日志,那么就可以实现自定义的FeginLoogerFactory的bean,就不会走这个方法创建默认的Feign日志工厂了。