最近接手了一个项目服务间的调用使用的是openfeign,技术leader要求记录服务调用日志便于问题排查。开启feign日志打印后,feign方式调用日志无法保存入库,只能输出到控制台。为了解决上述问题,通过对Feign的日志功能进行重写的方式,实现了日志的格式化输出和存储。
feign默认提供了获取请求和响应报文并打印到控制台的功能,但需要进行开启,且只能打印到控制台中。
在application.yml配置文件中进行如下配置:
feign:
client:
config:
default:
loggerLevel: FULL
局部生效的配置
feign:
client:
config:
userservice: # 当请求userservice服务的时候才有日志
loggerLevel: FULL
java代码方式,需要先声明一个Bean:
import feign.Logger;
import org.springframework.context.annotation.Bean;
/**
*注意,这个配置了@Configuration之后就是全局的bean了,就变成全局配置,意味着所有的@FeignClient都会
*应用该配置
*/
//@Configuration
public class FeignClientConfiguration {
/**
* 日志级别
* 通过源码可以看到日志等级有 4 种,分别是:
* NONE:不输出日志。
* BASIC:只输出请求方法的 URL 和响应的状态码以及接口执行的时间。
* HEADERS:将 BASIC 信息和请求头信息输出。
* FULL:输出完整的请求信息。
*/
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.FULL;
}
}
局部配置生效,把它放到@FeignClient这个注解中
@FeignClient(value = "userservice",configuration = FeignClientConfiguration.class)
全局配置的另一种形式,则把它放到@EnableFeignClients这个注解中
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
参考:
feign开启日志输出
Feign开启日志
介绍注解 @FeignClient 日志打印功能
Feign之日志输出
通过自定义CustomizationFeignLoggerFactory来重新实现FeignLoggerFactory接口,可以将打印log的对象设置为我们新定义的对象,从而实现调用进行其他操作。这个步骤貌似不是必须的,其他的一些博客也有未实现该步骤,不过没实验过
重写create方法,使用自定义logger对象
通过设置Logger.Level为FULL,开启日志信息的获取和打印
代码实现
import feign.Logger;
import feign.slf4j.Slf4jLogger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignLoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class CustomizationFeignLoggerFactory implements FeignLoggerFactory {
//日志开关
@Value("${feign.log.enable:false}")
private Boolean logEnable;
@Resource
private CustomizationFeignLogger customizationFeignLogger;
@Override
public Logger create(Class<?> type) {
return logEnable?customizationFeignLogger:new Slf4jLogger(type);
}
@Bean
Logger.Level feignLoggerLevel(){
return logEnable?Logger.Level.FULL:Logger.Level.NONE;
}
}
通过自定义CustomizationFeignLogger来重写feign.Logger对象,来重写实现日志相关操作。
通过重写logRequest()方法,来获取请求信息并将请求信息进行保存。
通过重写logAndRebufferResponse()方法,来获取响应信息并将响应信息进行对应的更新。
代码实现
import cn.hutool.json.JSONUtil;
import com.zeekr.mp.apply.aspect.SkipFeignLog;
import com.zeekr.mp.apply.po.CallOtherLog;
import com.zeekr.mp.apply.service.CallOtherLogService;
import feign.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
/**
* @Description 自定义feign日志记录
*/
@Slf4j
@Component
public class CustomizationFeignLogger extends Logger {
/**
* 用来存放日志记录的id
*/
private static final ThreadLocal<Long> logIdCache = new ThreadLocal<>();
@Resource
private LogService logService;
/**
* 这个方法才应该是记录日志的方法,换句话说规范的做法是logRequest方法和logAndRebufferResponse
* 方法调用log方法,不过当时为了赶时间就没有去研究这个方法的作用,可查看下面《FeignClient日志打印》中有使用
*
* @param s *
* @param s1 *
* @param objects *
*/
@Override
protected void log(String s, String s1, Object... objects) {
}
/**
* 请求日志
* @param configKey *
* @param logLevel *
* @param request *
*/
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
boolean skiplog = request.requestTemplate().feignTarget().type().isAnnotationPresent(SkipFeignLog.class);
if (skiplog){
return;
}
log.info("fegin请求url:{}",request.url());
log.info("fegin请求头:{}",request.headers());
log.info("fegin请求类型:{}",request.httpMethod().name());
log.info("fegin请求方法:{}",configKey);
log.info("fegin请求body:{}",(request.body() == null || request.body().length == 0) ? "" : new String(request.body()));
log.info("fegin请求协议:{}",request.protocolVersion());
saveLog(configKey, request);
}
/**
* 响应日志
* @param configKey *
* @param logLevel *
* @param response *
* @param elapsedTime *
* @return *
* @throws IOException *
*/
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
boolean skiplog = response.request().requestTemplate().feignTarget().type().isAnnotationPresent(SkipFeignLog.class);
String result = "";
try {
if(skiplog){
return response;
}
log.info("feign响应信息为码status:{}", response.status());
log.info("feign响应信息body:{}", result);
log.info("feign响应信息耗时ElapsedTime:[{}ms]",elapsedTime);
if (response.body() != null) {
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
result = new String(bodyData,"utf-8");
//必须使用如下方式返回,重新构建一个response的body流,否则异常stream is closed reading POST https:xxx,
// 因为获取response返回的数据(流),因为Response里的body流对象比较特殊,只能读取一次,所以一旦读取过一次后想再次读取那么就会报流相关的异常
return response.toBuilder().body(bodyData).build();
}
return response;
} finally {
updateLog(response, elapsedTime, result,skiplog);
//日志记录完成后,清除ThreadLocal,防止内存泄露
logIdCache.remove();
}
}
/**
* 更新feign响应日志---可优化为在logAndRebufferResponse方法中一次性保存,因为response对象中可获取请求时request对象
* @param response 响应对象
* @param elapsedTime 响应时间
* @param result 响应数据
*/
private void updateLog(Response response, long elapsedTime, String result,boolean skip) {
//是否跳过日志记录
if(skip){
return;
}
//防止保存日志影响正常业务执行使用try catch
try {
Long id = logIdCache.get();
CallLog otherLog = logService.getById(id);
callLog.setResponseBody(result);
callLog.setResponseStatus(String.valueOf(response.status()));
callLog.setCostTime(elapsedTime);
logService.saveOrUpdate(otherLog);
}catch (Exception e){
log.error("fegin响应日志更新记录异常:{}",e);
}
}
/**
* 保存feign请求日志
* @param configKey feign调用方法
* @param request 请求对象
*/
private void saveLog(String configKey, Request request) {
//防止保存日志影响正常业务执行使用try catch
try {
String param = (request.body() == null || request.body().length == 0) ? "" : new String(request.body());
CallLog callLog = new CallLog();
//请求方法作为业务类型
callLog.setBizType(configKey);
//请求url
callLog.setRequestUrl(request.url());
//请求类型
callLog.setRequestMethod(request.httpMethod().name());
//请求头
callLog.setRequestHeaders(JSONUtil.toJsonStr(request.headers()));
//请求入参
callLog.setRequestParams(param);
logService.save(otherLog);
//设置日志id,用于响应时更新日志记录
logIdCache.set(otherLog.getId());
}catch (Exception e){
log.error("fegin请求日志保存记录异常:{}",e);
}
}
}
使用feign调用记录日志篇
Feign默认日志级别为NONE,不记录任何日志信息,示例将日志级别改为info,服务间调用错误处理
FeignClient日志打印
Feign 打印请求日志