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的請求和響應日志》 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM