项目里使用了Feign进行远程调用,有时为了问题排查,需要开启请求和响应日志
下面简介一下如何开启Feign日志:
注:本文基于
- spring-boot-starter-parent 2.3.4.RELEASE
- spring-cloud-starter-openfeign 2.2.3.RELEASE
1、项目里定义FeignClient接口
package com.example.demo.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @FeignClient(name = "deom", url = "https://www.baidu.com") public interface FeignDemo { @GetMapping("/") String test(); }
2、单个FeignClient接口开启日志
在 application.yml 里指定Feign接口日志级别为DEBUG,类型为FULL:
注:com.example.demo.feign.FeignDemo就是上面定义的FeignClient接口
logging: level: com.example.demo.feign.FeignDemo: debug # 下面的配置,也可以写代码代替 # @Bean # public Logger.Level level() { return Logger.Level.FULL; } feign: client: config: default: loggerLevel: full
OK了,重启项目,调用 FeignDemo.test() 方法后,会输出如下日志:
2020-10-13 11:46:24.161 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] ---> GET https://www.baidu.com HTTP/1.1
2020-10-13 11:46:24.162 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] ---> END HTTP (0-byte body)
2020-10-13 11:46:24.255 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] <--- HTTP/1.1 200 OK (93ms)
2020-10-13 11:46:24.255 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] content-length: 2443
2020-10-13 11:46:24.255 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] content-type: text/html
2020-10-13 11:46:24.256 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] date: Tue, 13 Oct 2020 03:46:24 GMT
2020-10-13 11:46:24.256 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] server: bfe
2020-10-13 11:46:24.256 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test]
2020-10-13 11:46:24.257 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test]
百度一下,你就知道
3、所有FeignClient接口 开启日志
上面的方法,只能开启单个FeignClient接口,如果项目里有10个接口,那么要在yml里配置10项,而且以后添加新的FeignClient,还要记得去修改yml配置,太麻烦。
所以,下面是开启所有FeignClient接口日志的配置:
3.1、修改FeignConfiguration
自定义feign.Logger,如下:
package com.example.demo.cacheDemo; import feign.slf4j.Slf4jLogger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignConfiguration { @Bean public feign.Logger logger() { return new Slf4jLogger(); } }
3.2、还是修改 application.yml 配置
logging: level: # 删除具体的FeignClient接口配置,只保留这一个就好了 feign.Logger: debug # 也可以写代码代替 # @Bean # public Logger.Level level() { return Logger.Level.FULL; } feign: client: config: default: loggerLevel: full
3.3、OK了,此时项目里
不管新增多少个 FeignClient,都会输出日志。
4、重写FeignClient输出日志
根据上面输出的日志,可以看到是多条INFO日志,在并发时,很有可能会互相干扰,而且格式也无法调整。
我们知道,Feign默认情况下,是使用 feign.Client.Default 发起http请求;
我们可以重写Client,并注入Bean来替换掉 feign.Client.Default,从而实现日志记录,当然也可以做其它任意事情了,比如添加Header。下面是注入Bean的代码:
// 默认不注入,如果yml配置里有 logging.level.beinet.cn.demostudy.MyClient 才注入 @Bean @ConditionalOnProperty("logging.level.beinet.cn.demostudy.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); }
下面是重写的Client完整代码:
package beinet.cn.demostudy; import feign.Client; import feign.Request; import feign.Response; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StreamUtils; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; import java.io.*; import java.util.Collection; import java.util.Map; @Slf4j public class MyClient extends Client.Default { public MyClient(SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier) { super(socketFactory, hostnameVerifier); } @Override public Response execute(Request request, Request.Options options) throws IOException { StringBuilder sb = new StringBuilder("[log started]\r\n"); sb.append(request.httpMethod()).append(" ").append(request.url()).append("\r\n"); CombineHeaders(sb, request.headers()); // 请求Header CombineBody(sb, 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 { sb.append("\r\nResponse cost time(ms): ").append(String.valueOf(costTime)); if (response != null) sb.append(" status: ").append(response.status()); sb.append("\r\n"); if (response != null) { CombineHeaders(sb, response.headers()); // 响应Header sb.append("Body:\r\n").append(response.body()).append("\r\n"); } if (exception != null) { sb.append("Exception:\r\n ").append(exception.getMessage()).append("\r\n"); } sb.append("\r\n[log ended]"); log.debug(sb.toString()); } Response ret = response.getResponse().toBuilder() .body(response.getBody(), response.getResponse().body().length()).build(); response.close(); return ret; } private static void CombineHeaders(StringBuilder sb, Map> headers) { if (headers != null && !headers.isEmpty()) { sb.append("Headers:\r\n"); for (Map.Entry > ob : headers.entrySet()) { for (String val : ob.getValue()) { sb.append(" ").append(ob.getKey()).append(": ").append(val).append("\r\n"); } } } } private static void CombineBody(StringBuilder sb, byte[] body) { if (body == null || body.length <= 0) return; sb.append("Body:\r\n").append(new String(body)).append("\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 > 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(); } } }
输出日志示例:
2020-10-15 16:48:26.081 DEBUG 15664 --- [ main] beinet.cn.demostudy.MyClient : [log started]
POST https://www.baidu.com?flg=3
Headers:
Content-Length: 14
Content-Type: text/plain;charset=UTF-8
Body:
abcde我是dddResponse cost time(ms): 207 status: 200
Headers:
content-length: 2443
content-type: text/html
date: Thu, 15 Oct 2020 08:48:27 GMT
server: bfe
Body:
百度的html