SpringBoot(二十):SpringBoot+Feign:实现Feign日志写入一行几种方案

默认情况下Feign日志

1)配置FeignConfig

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({HttpClientFeignConfiguration.class})
@AutoConfigureBefore({FeignClientsConfiguration.class})
public class FeignConfig {
    /**
     * +使用Apache Httpclient实现FeignClient客户端
     * @param httpClient #HttpClientFeignConfiguration.customHttpClient()
     * @return ApacheHttpClient实例
     */
    @Bean
    @Primary
    public Client feignClient(HttpClient httpClient) {
        return new ApacheHttpClient(httpClient);
    }

    /**
     * +用Spring定义的日志系统代理feign日志系统
     * @return 代理日志系统
     */
    @Bean
    @Primary
    public Logger logger() {
        return new FeignLog(this.getClass());
    }
  
    /**
     * Feign的代理log
     *
     */
    final class FeignLog extends Logger {
        private Log log;

        public FeignLog(Class<?> clazz) {
            log = LogFactory.getLog(clazz);
        }

        @Override
        protected void log(String configKey, String format, Object... args) {
            if (log.isInfoEnabled()) {
                log.info(String.format(methodTag(configKey) + format, args));
            }
        }
    }
}

2)yml配置

# hystrix相关
feign:
  okhttp: 
    enabled: false
  httpclient:
    #Apache的HTTP Client替换Feign原始的http client
    enabled: true
    max-connections: 20480
    max-connections-per-route: 512
    time-to-live: 60
    time-to-live-unit: SECONDS
    connection-timeout: 60000
    user-agent: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0'
  client:
    # 配置文件优先级高
    default-to-properties: true
    config:
      default:
        logger-level: FULL #BASIC
        connect-timeout: 60000
        read-timeout: 60000
        decode404: true
  hystrix: 
    enabled: true
  compression: 
    request: 
      #请求和响应GZIP压缩支持
      enabled: true
      mime-types: application/json,application/xml,text/xml
      min-request-size: 2048
    response: 
      enabled: true
      useGzipDecoder: true

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 15000
        # 熔断关闭
        timeout:
          enabled: false
  threadpool:
    default:
      coreSize: 40
      maximumSize: 100
      maxQueueSize: 100

3)打印日志格式

INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] ---> POST http://api.xxx.com/api/v1/point/remove HTTP/1.1]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] Accept: application/json]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] Accept-Encoding: gzip]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] Accept-Encoding: deflate]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] authorization: hmac id="xxx", algorithm="hm-a1", headers="date source", signature="xxtR8="]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] Content-Length: 20]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] Content-Type: application/json]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] date: Mon, 15 Mar 2021 13:57:38 GMT]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] tenantid: xxxx]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] x-date: Mon, 15 Mar 2021 13:57:38 GMT]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] ]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] {"point_id":"5646"}]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] ---> END HTTP (20-byte body)]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] <--- HTTP/1.1 500 Internal Server Error (83ms)]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] connection: keep-alive]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] content-type: text/html; charset=utf-8]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] date: Mon, 15 Mar 2021 13:57:38 GMT]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] server: nginx]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] transfer-encoding: chunked]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] x-powered-by: PHP/7.0.33]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] ]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removePoint] <!DOCTYPE html>
<html>
<head>
...
</html>
]
INFO  c.d.d.f.cfg.FeignConfig - [[MyFeignClient#removeTarget] <--- END HTTP (36033-byte body)]

4)缺点

这么多行,比如一些日志采集系统采集到后不好聚合或者索引查找。

日志写入一行#重写feign.Client.Default

1)定义MyClient类

自定义MyClient类

public class MyClient extends Client.Default {
    protected static Log logger = LogFactory.getLog("FEIGN.LOG");

    public MyClient(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
        super(sslContextFactory, hostnameVerifier);
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        FeignLog feignLog = new FeignLog();
        feignLog.setRequestUrl(request.url());
        feignLog.setRequestMethod(request.httpMethod().name());
        feignLog.setRequestHeader(new HashMap<>());
        if (request.headers() != null && !request.headers().isEmpty()) {
            for (Map.Entry<String, Collection<String>> ob : request.headers().entrySet()) {
                for (String val : ob.getValue()) {
                    feignLog.getRequestHeader().put(ob.getKey(), val);
                }
            }
        }
        if (request.body() != null && request.body().length > 0) {
            feignLog.setRequestBody(new String(request.body()));
        }

        long costTime = -1;
        Exception exception = null;
        BufferingFeignClientResponse response = null;
        long begin = System.currentTimeMillis();
        try {
            response = new BufferingFeignClientResponse(super.execute(request, options));
            costTime = (System.currentTimeMillis() - begin);
        } catch (Exception exp) {
            costTime = (System.currentTimeMillis() - begin);
            exception = exp;
            throw exp;
        } finally {
            feignLog.setCosTime(costTime + "ms");

            if (response != null) {
                feignLog.setStatus(response.status());
            }
            if (response != null) {
                feignLog.setResponseHeader(new HashMap<>());
                if (response.headers() != null && !response.headers().isEmpty()) {
                    for (Map.Entry<String, Collection<String>> ob : response.headers().entrySet()) {
                        for (String val : ob.getValue()) {
                            feignLog.getResponseHeader().put(ob.getKey(), val);
                        }
                    }
                }
                if (request.body() != null && request.body().length > 0) {
                    feignLog.setResponseBody(new String(response.body()));
                }
            }
            if (exception != null) {
                feignLog.setException(exception.getMessage());
            }
            //logger.info(feignLog);
        }

        Response ret = response.getResponse().toBuilder().body(response.getBody(), response.getResponse().body().length()).build();
        List<String> list = new ArrayList<>();
        list.add(feignLog.toString());
        ret.headers().getOrDefault("my-client-log", list);
        response.close();

        return ret;
    }

    @Data
    static final class FeignLog {
        private String requestUrl;// 请求地址
        private String requestMethod; // 请求发送方式:GET/POST/DELETE/PUT/HEADER/...
        private Map<String, String> requestHeader;// 请求header信息
        private String requestBody;// 请求body
        private Integer status;// 请求返回状态
        private String exception;// 请求返回异常信息
        private Map<String, String> responseHeader;// 响应header信息
        private String responseBody;// 响应body
        private String cosTime;// 请求到接收到响应数据耗费时长单位ms

        @Override
        public String toString() {
            return "request url='" + requestUrl + "\'\r\n" +
                    "request method='" + requestMethod + "\'\r\n" +
                    "request header=" + requestHeader + "\'\r\n" +
                    "request body='" + requestBody + "\'\r\n" +
                    "status=" + status + "\'\r\n" +
                    "response header='" + responseHeader + "\'\r\n" +
                    "response body='" + responseBody + "\'\r\n" +
                    "cost Time='" + cosTime + "\'\r\n" +
                    "exception='" + exception + "\'\r\n";
        }

    }

    static final class BufferingFeignClientResponse implements Closeable {
        private Response response;
        private byte[] body;

        private BufferingFeignClientResponse(Response response) {
            this.response = response;
        }

        private Response getResponse() {
            return this.response;
        }

        private int status() {
            return this.response.status();
        }

        private Map<String, Collection<String>> headers() {
            return this.response.headers();
        }

        private String body() throws IOException {
            StringBuilder sb = new StringBuilder();
            try (InputStreamReader reader = new InputStreamReader(getBody())) {
                char[] tmp = new char[1024];
                int len;
                while ((len = reader.read(tmp, 0, tmp.length)) != -1) {
                    sb.append(new String(tmp, 0, len));
                }
            }
            return sb.toString();
        }

        private InputStream getBody() throws IOException {
            if (this.body == null) {
                this.body = StreamUtils.copyToByteArray(this.response.body().asInputStream());
            }
            return new ByteArrayInputStream(this.body);
        }

        @Override
        public void close() {
            this.response.close();
        }
    }
}

在覆盖feign.Client.Deafult#exceute(...),记录日志包含:

        private String requestUrl;// 请求地址
        private String requestMethod; // 请求发送方式:GET/POST/DELETE/PUT/HEADER/...
        private Map<String, String> requestHeader;// 请求header信息
        private String requestBody;// 请求body
        private Integer status;// 请求返回状态
        private String exception;// 请求返回异常信息
        private Map<String, String> responseHeader;// 响应header信息
        private String responseBody;// 响应body
        private String cosTime;// 请求到接收到响应数据耗费时长单位ms

2)注入自定义MyClient类到配置类

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({HttpClientFeignConfiguration.class})
@AutoConfigureBefore({FeignClientsConfiguration.class})
public class FeignConfig {
    /**
     * +使用Apache Httpclient实现FeignClient客户端
     * @param httpClient #HttpClientFeignConfiguration.customHttpClient()
     * @return ApacheHttpClient实例
     */
//    @Bean
//    @Primary
//    public Client feignClient(HttpClient httpClient) {
//        return new ApacheHttpClient(httpClient);
//    }

    // 默认不注入,如果yml配置里有 com.beyonds.phoenix.integration.domain.feign.client.MyClient 才注入
    @Bean
    @ConditionalOnProperty("com.dx.domain.feign.client.MyClient")
    MyClient getClient() throws NoSuchAlgorithmException, KeyManagementException {
        // 忽略SSL校验
        SSLContext ctx = SSLContext.getInstance("SSL");
        X509TrustManager tm = new X509TrustManager() {
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) {
            }
    
            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) {
            }
    
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
        };
        ctx.init(null, new TrustManager[]{tm}, null);
        return new MyClient(ctx.getSocketFactory(), (hostname, sslSession) -> true);
    }
}

注意:

1)只能注入一个自定义Client类,上边feignClient也是自定义Client,因此这里注释掉了该注解;

2)注入自定义MyClient加上了@ConditionalOnProperty("com.dx.domain.feign.client.MyClient")修饰,因此需要在yml中配置了com.dx.domain.feign.client.MyClient:FULL 或者BASIC/DEBUG等才能生效,否定义MyClient bean不生效。

3)配置yml

Bootstrap.yml

# hystrix相关
feign:
  okhttp: 
    enabled: false
  httpclient:
    #Apache的HTTP Client替换Feign原始的http client
    enabled: true
    max-connections: 20480
    max-connections-per-route: 512
    time-to-live: 60
    time-to-live-unit: SECONDS
    connection-timeout: 60000
    user-agent: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0'
  client:
    # 配置文件优先级高
    default-to-properties: true
    config:
      default:
        logger-level: BASIC
        connect-timeout: 60000
        read-timeout: 60000
        decode404: true
  hystrix: 
    enabled: true
  compression: 
    request: 
      #请求和响应GZIP压缩支持
      enabled: true
      mime-types: application/json,application/xml,text/xml
      min-request-size: 2048
    response: 
      enabled: true
      useGzipDecoder: true

com:
  dx:
    domain:
      feign:
        client:
          MyClient: FULL

4)测试输出日志格式

2021-03-15 14:33:37,107 [hystrix-myFeignClient-1-14175][TraceId:] INFO  FEIGN.LOG - [
request url='http://api.xxx.com/api/v1/point/remove'
request method='POST'
request header={authorization=hmac id="xx", algorithm="hx-a1", headers="date source", signature="xxAWzmpSM=", date=Mon, 15 Mar 2021 06:33:36 GMT, Accept=application/json, tenantid=xxxx, User-Agent=Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0, Accept-Encoding=deflate, Content-Length=20, x-date=Mon, 15 Mar 2021 06:33:36 GMT, Content-Type=application/json}'
request body='{"point_id":"5646"}'
status=500'
response header='{date=Mon, 15 Mar 2021 06:33:37 GMT, server=nginx, transfer-encoding=chunked, x-powered-by=PHP/7.0.33, connection=keep-alive, content-type=text/html; charset=utf-8}'
response body='<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>System Error</title>
    <meta name="robots" content="noindex,nofollow" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    ...
    </body>
</html>
'
cost Time='61ms'
exception='null'
]

5)缺点

日志中缺少了:

1)MyFeingClient#removePoint方法;

2)缺少了MyFeingClient#removePoint方法参数信息。

日志写入一行#使用Aspect切入FeignClient

1)定义FeignAspect类

@Aspect
@Component
public class FeignAspect {
    protected static Log logger = LogFactory.getLog("FEIGN.LOG");
    // 这个也行 @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    // 参考 https://github.com/spring-cloud/spring-cloud-openfeign/issues/322
    @Pointcut("@within(org.springframework.cloud.openfeign.FeignClient)")
    public void feignClientPointcut() {
    }

    @Around("feignClientPointcut()")
    public Object feignAround(ProceedingJoinPoint joinPoint) throws Throwable {
        return logAround(joinPoint);
    }

    private static ObjectMapper mapper = new ObjectMapper();

    private Object logAround(ProceedingJoinPoint point) throws Throwable {
        long beginTime = System.currentTimeMillis();
        Object result = null;
        Exception exception = null;
        try {
            result = point.proceed();
        } catch (Exception exp) {
            exception = exp;
        }
        long time = System.currentTimeMillis() - beginTime;
        saveLog(point, result, exception, time);

        if (exception != null) {
            throw exception;
        }
        return result;
    }

    private static void saveLog(ProceedingJoinPoint joinPoint, Object result, Exception exception, long time) {
        Dto dto = new Dto();
        dto.setCostTime(time);

        try {
            if (exception != null) {
                dto.setExp(exception.toString());
            }
            if (result != null) {
                dto.setResult(serial(result));
            }
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //请求的 类名、方法名
            String className = joinPoint.getTarget().getClass().getName();
            String signName = signature.getDeclaringTypeName();
            if (!signName.equalsIgnoreCase(className))
                signName += "|" + className;
            dto.setClas(signName);

            String methodName = signature.getName();
            dto.setMethod(methodName);

            //请求的参数
            Object[] args = joinPoint.getArgs();
            if (args != null && args.length > 0) {
                dto.setPara(serial(args));
            }
        } catch (Exception e) {
            dto.setExp(e.toString());
        }
        if (exception != null) {
            logger.warn(dto.toString());
        } else {
            logger.info(dto.toString());
        }
    }
    private static String serial(Object obj) {
        try {
            return mapper.writeValueAsString(obj);
        } catch (Exception ex) {
            return obj.toString();
        }
    }
    @Data
    private static class Dto {
        /**
         * 调用类名
         */
        private String clas;
        /**
         * 调用方法名
         */
        private String method;
        /**
         * 调用的参数
         */
        private String para;
        /**
         * 方法返回结果
         */
        private String result;
        /**
         * 执行时长,毫秒
         */
        private long costTime;
        /**
         * 备注
         */
        private String remark;

        /**
         * 出现的异常
         */
        private String exp;
    }
}

2)配置FeignConfig和yml

都和“默认情况下Feign日志”章节配置一致

3)日志格式:

[FeignAspect.Dto(
    clas=com.dx.domain.feign.MyFeignClient|com.sun.proxy.$Proxy179, 
    method=removeTarget, 
    para=["http://api.xxx.com/","xx","Mon, 15 Mar 2021 06:40:06 GMT","Mon, 15 Mar 2021 06:40:06 GMT","hmac id=\"AKIxxxw8\", algorithm=\"h-a1\", headers=\"date source\", signature=\"xxxY=\"",{"point_id":"5646"}], 
    result={"status":5000,"message":"Api调用失败","data":false,"meta":null}, 
    costTime=348, 
    remark=null, 
    exp=null
    )]

4)缺点

缺少完整的请求地址;

缺少feign原始日志中的请求响应信息。

日志写入一行#重写feign.Logger

1)自定义FeignLogger extends feign.Logger

 /**
     * Springboot的代理log
     * author:
     * date:   2021年3月15日
     * time:   下午10:07:57
     *
     */
    final static class FeignLogger extends feign.Logger {
        private final Log logger;
        private final static ThreadLocal<MyLoggerModel> myLoggerModelThreadLocal = new ThreadLocal();
        private final Boolean loggerable;

        public FeignLogger(Class<?> clazz) {
            this.logger = LogFactory.getLog(clazz);
            this.loggerable = logger.isInfoEnabled();
        }

        @Override
        protected void logRequest(String configKey, Level logLevel, Request request) {
            final MyLoggerModel myLoggerModel = new MyLoggerModel();
            if (request.requestTemplate() != null && request.requestTemplate().feignTarget() != null && request.requestTemplate().feignTarget().type() != null) {
                myLoggerModel.setFeignClass(request.requestTemplate().feignTarget().type().getName());
            } else {
                myLoggerModel.setFeignClass(null);
            }
            myLoggerModel.setFeignMethod(configKey);
            if (request.requestTemplate() != null && request.requestTemplate().methodMetadata() != null && request.requestTemplate().methodMetadata().returnType() != null) {
                myLoggerModel.setFeignMethodReturnType(request.requestTemplate().methodMetadata().returnType().getTypeName());
            } else {
                myLoggerModel.setFeignMethodReturnType(null);
            }
            myLoggerModel.setRequestHttpMethod(request.httpMethod() == null ? null : request.httpMethod().name());
            myLoggerModel.setRequestUrl(request.url());
            myLoggerModel.setRequestCharset(request.charset() == null ? null : request.charset().name());
            myLoggerModel.setRequestHeader(request.headers());
            myLoggerModel.setRequestBody(request.body() == null ? null : new String(request.body()));

            final LinkedHashMap<String, String> feignMethodParameters = new LinkedHashMap<>();
            final HashMap<String, String> urlRequestKeyValueMap = new HashMap<>();
            if (myLoggerModel.getRequestUrl() != null && myLoggerModel.getRequestUrl().split("\\?").length > 1) {
                final String[] urlSplitArr = myLoggerModel.getRequestUrl().split("\\?");
                final String[] paramKeyValueArr = urlSplitArr[1].split("&");
                for (String keyValue : paramKeyValueArr) {
                    final String[] keyValueArr = keyValue.split("=");
                    urlRequestKeyValueMap.put(keyValueArr[0], keyValueArr.length > 1 ? keyValueArr[1] : null);
                }
            }
            if (request.requestTemplate() != null && request.requestTemplate().methodMetadata() != null && request.requestTemplate().methodMetadata().indexToName() != null && request.requestTemplate().methodMetadata().indexToName().size() > 0) {
                for (Map.Entry<Integer, Collection<String>> entry : request.requestTemplate().methodMetadata().indexToName().entrySet()) {
                    final String paramName = entry.getValue().toArray()[0].toString();
                    final Collection<String> paramValues = myLoggerModel.getRequestHeader().getOrDefault(paramName, null);
                    String paramValue = null;
                    if (paramValues != null && paramValues.size() > 0) {
                        paramValue = paramValues.toArray()[0].toString();
                    }
                    if (paramValue == null && urlRequestKeyValueMap.containsKey(paramName)) {
                        paramValue = urlRequestKeyValueMap.get(paramName);
                    }
                    feignMethodParameters.put(paramName, paramValue);
                }
            }
            myLoggerModel.setFeignMethodParameters(feignMethodParameters);

            myLoggerModelThreadLocal.set(myLoggerModel);
        }

        @Override
        protected Response logAndRebufferResponse(
                String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
            final MyLoggerModel myLoggerModel = myLoggerModelThreadLocal.get();
            myLoggerModelThreadLocal.remove();

            myLoggerModel.setResponseStatus(response.status());
            myLoggerModel.setResponseReason(response.reason());
            myLoggerModel.setResponseHeader(response.headers());
            myLoggerModel.setElapsedTime(elapsedTime);

            if (response.body() != null && !(response.status() == 204 || response.status() == 205)) {
                byte[] bodyData = Util.toByteArray(response.body().asInputStream());
                if (bodyData.length > 0) {
                    String responseBody = feign.Util.decodeOrDefault(bodyData, feign.Util.UTF_8, "Binary data");
                    myLoggerModel.setResponseBody(responseBody.replaceAll("\\s*|\t|\r|\n", ""));
                }
                if (this.loggerable) {
                    this.logger.info(myLoggerModel);
                }
                return response.toBuilder().body(bodyData).build();
            }
            if (this.loggerable) {
                this.logger.info(myLoggerModel);
            }
            return response;
        }

        protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
            final MyLoggerModel myLoggerModel = myLoggerModelThreadLocal.get();
            myLoggerModelThreadLocal.remove();

            myLoggerModel.setElapsedTime(elapsedTime);
            myLoggerModel.setIoException(ioe);

            if (this.loggerable) {
                this.logger.warn(myLoggerModel);
            }
            return ioe;
        }

        @Override
        protected void log(String configKey, String format, Object... args) {
            if (this.loggerable) {
                this.logger.debug(String.format(methodTag(configKey) + format, args));
            }
        }
    }

    @Data
    final static class MyLoggerModel {
        private String feignClass;// requestTemplate#feignTarget#type#name=com.dx.domain.feign.MyFeignClient
        private String feignMethod; // MyFeignClient#removeTarget(URI,String,String,String,String,RemoveTargetRequest)
        private Map<String, String> feignMethodParameters;
        private String feignMethodReturnType;//request#requestTemplate#methodMetadata#returnType=com...ReturnResult<Boolean>

        private String requestHttpMethod;// post
        private String requestUrl;// http://api.xx.com/api/v1/point/remove
        private String requestCharset;
        private Map<String, Collection<String>> requestHeader;
        private String requestBody;

        private Integer responseStatus; // 500
        private String responseReason; // Internal Server Error
        private Map<String, Collection<String>> responseHeader;
        private String responseBody; //

        private Long elapsedTime;

        private IOException ioException;

        @Override
        public String toString() {
            return "feign Class=" + feignClass + "\r\n" +
                    "feign Method=" + feignMethod + "\r\n" +
                    "feign Method Parameters=" + feignMethodParameters + "\r\n" +
                    "feign Method ReturnType=" + feignMethodReturnType + "\r\n" +
                    "request HttpMethod='" + requestHttpMethod + "\r\n" +
                    "request Url=" + requestUrl + "\r\n" +
                    "request Charset=" + requestCharset + "\r\n" +
                    "request Header=" + requestHeader + "\r\n" +
                    "request Body=" + requestBody + "\r\n" +
                    "response Status=" + responseStatus + "\r\n" +
                    "response Reason='" + responseReason + "\r\n" +
                    "response Header=" + responseHeader + "\r\n" +
                    "response Body=" + responseBody + "\r\n" +
                    "elapsed Time=" + elapsedTime + "\r\n" +
                    "io Exception=" + ioException + "\r\n";
        }
    }

2)配置FeignConfig

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({HttpClientFeignConfiguration.class})
@AutoConfigureBefore({FeignClientsConfiguration.class})
public class FeignConfig {
    /**
     * +使用Apache Httpclient实现FeignClient客户端
     * @param httpClient #HttpClientFeignConfiguration.customHttpClient()
     * @return ApacheHttpClient实例
     */
    @Bean
    @Primary
    public Client feignClient(HttpClient httpClient) {
        return new ApacheHttpClient(httpClient);
    }

    /**
     * +用Spring定义的日志系统代理feign日志系统
     * @return 代理日志系统
     */
    @Bean
    @Primary
    public Logger logger() {
        return new FeignLogger(this.getClass());
    }
  ...
}    

3)配置yml

都和“默认情况下Feign日志”章节配置一致

4)日志格式

2021-03-16 22:00:34,744 [hystrix-machineIntegrationFeignClient-1-28356][TraceId:] INFO  com.dx.cfg.FeignConfig - [
feign Class=com.dx.feign.MyClient
feign Method=MyFeignClient#createOrUpdateTarget(URI,String,String,String,String,CreateOrUpdateTargetRequest)
feign Method Parameters={tenantid=xx, date=Tue, 16 Mar 2021 14:00:34 GMT, x-date=Tue, 16 Mar 2021 14:00:34 GMT, authorization=hmac id="xx8", algorithm="h-a1", headers="x-date tenantId", signature="fgxxKSec="}
feign Method ReturnType=com.dx...ReturnResult<java.lang.String>
request HttpMethod='POST
request Url=http://api.xxx.com/api/v1/point/deploy
request Charset=UTF-8
request Header={Accept=[application/json], Accept-Encoding=[gzip, deflate], authorization=[hmac id="xx8", algorithm="-a1", headers="x-date tenantId", signature="fgxxSec="], Content-Length=[107], Content-Type=[application/json], date=[Tue, 16 Mar 2021 14:00:34 GMT], tenantid=[2020051800043], User-Agent=[Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0], x-date=[Tue, 16 Mar 2021 14:00:34 GMT]}
request Body={"bname":null,"cy":4,"floor":3,"name":"2","serial_no":"de","site_id":"1"}
response Status=200
response Reason='OK
response Header={access-control-allow-credentials=[true], access-control-allow-headers=[x-token,x-uid,x-token-check,x-requested-with,content-type,Host], access-control-allow-methods=[*], access-control-allow-origin=[*], connection=[keep-alive], content-type=[application/json; charset=utf-8], date=[Tue, 16 Mar 2021 14:00:34 GMT], server=[nginx], transfer-encoding=[chunked], x-powered-by=[PHP/7.0.33]}
response Body={"data":"17","status":200,"message":"success"}
elapsed Time=392
io Exception=null
]

5)缺点

缺少调用feign后代码接口返回值,这块目前也不在需求范围内,暂时不需要实现。

 

参考:

Feign Client全局日志打印

AOP进阶实战——双切面实现集中打印Feign日志

全局记录Feign的请求和响应日志》 

 

posted @ 2021-03-16 22:55  cctext  阅读(2072)  评论(0编辑  收藏  举报