feign自定义调用记录并入库

项目场景:

最近接手了一个项目服务间的调用使用的是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.client.config.xxx.loggerLevel
    • 如果xxx是default则代表全局
    • 如果xxx是服务名称,例如userservice则代表某服务
  • 方式二是java代码配置Logger.Level这个Bean
    • 如果在@EnableFeignClients注解声明则代表全局
    • 如果在@FeignClient注解中声明则代表某服务

参考:
feign开启日志输出
Feign开启日志
介绍注解 @FeignClient 日志打印功能
Feign之日志输出


实现方案

步骤一:
1.1.重新实现FeignLoggerFactory接口

通过自定义CustomizationFeignLoggerFactory来重新实现FeignLoggerFactory接口,可以将打印log的对象设置为我们新定义的对象,从而实现调用进行其他操作。这个步骤貌似不是必须的,其他的一些博客也有未实现该步骤,不过没实验过

1.2.重写定义log对象

重写create方法,使用自定义logger对象

1.3.开启feign日志打印功能

通过设置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;
    }
}
步骤二:
2.1.重写feign.Logger抽象类

通过自定义CustomizationFeignLogger来重写feign.Logger对象,来重写实现日志相关操作。

2.2.重写logRequest()方法

通过重写logRequest()方法,来获取请求信息并将请求信息进行保存。

2.3.重写logAndRebufferResponse()方法

通过重写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 打印请求日志

你可能感兴趣的:(spring,cloud,java,springcloud,spring)