全局记录Feign的请求和响应日志
目录
项目里使用了Feign进行远程调用,有时为了问题排查,需要开启请求和响应日志,下面简介一下如何开启Feign日志:
注:本文基于
- spring-boot-starter-parent 2.3.4.RELEASE
- spring-cloud-starter-openfeign 2.2.3.RELEASE
2024-07-23 补充内容:
今天在JDK21
+ spring-cloud-starter-openfeign4.1.2
+ spring-cloud-config-server
中,发现配置中心的yml里写:
feign.client.config.default.logger-level: full
无效,改用代码里定义才生效,暂时不确定原因:
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
2024-8-30补充内容:
调试跟踪了一下代码,feign.SynchronousMethodHandler类的executeAndDecode方法,会判断当前类的logLevel 并输出日志:
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
这个logLevel 属性,是由FeignClientFactoryBean类的configureUsingProperties方法,去读取yml配置,然后把defaultConfig写进去,这个defaultConfig定义在ConfigurationProperties类里,openfeign2.2.6的定义如下,读取yml里 feign.client
配置:
@ConfigurationProperties("feign.client")
public class FeignClientProperties {
private String defaultConfig = "default";
public String getDefaultConfig() {
return this.defaultConfig;
}
而openfeign4.1.2的定义如下,读取yml里 spring.cloud.openfeign.client
配置:
@ConfigurationProperties("spring.cloud.openfeign.client")
public class FeignClientProperties {
private String defaultConfig = "default";
public String getDefaultConfig() {
return defaultConfig;
}
所以,如果使用spring-cloud-starter-openfeign4.1.2或更高版本时,yml配置如下:
# 下面的配置,也可以写代码代替
# @Bean
# public Logger.Level level() { return Logger.Level.FULL; }
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
# 参考: https://docs.spring.io/spring-cloud-openfeign/reference/spring-cloud-openfeign.html
以下是日志记录的正文
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:
logger-level: 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] <!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
2020-10-13 11:46:24.257 DEBUG 20824 --- [nio-8080-exec-4] com.example.demo.feign.FeignDemo : [FeignDemo#test] <--- END HTTP (2443-byte body)
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
2.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<String, Collection<String>> headers) {
if (headers != null && !headers.isEmpty()) {
sb.append("Headers:\r\n");
for (Map.Entry<String, Collection<String>> 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<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();
}
}
}
输出日志示例:
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我是ddd
Response 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:
<!DOCTYPE html>百度的html</html>
[log ended]
5、重写feign.Logger输出日志(推荐)
上面是重写Client,需要关注http的执行过程,并且还要处理ssl逻辑,麻烦一些,也可以直接重写feign.Logger
日志操作类,只关注写日志的过程。
5.1、修改application.yml配置(也可以用Bean修改)
logging:
level:
beinet.cn.demologfeign.feign.MyFeignLogger: debug # 注意,这个要用你的代码里的包名替换
feign:
client:
config:
default:
logger-level: full
5.2、Bean定义:
@Configuration
public class FeignConfiguration {
@Bean
@ConditionalOnProperty("logging.level.beinet.cn.demologfeign.feign.MyFeignLogger")
MyFeignLogger createMyLogger() {
return new MyFeignLogger();
}
5.3、MyFeignLogger日志重写实现:
package beinet.cn.demologfeign.feign;
import feign.Logger;
import feign.Request;
import feign.Response;
import feign.Util;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import static feign.Util.decodeOrDefault;
import static feign.form.util.CharsetUtil.UTF_8;
/**
* 新类
*
* @author youbl
* @date 2023/3/6 9:45
*/
@Slf4j
public class MyFeignLogger extends Logger {
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
byte[] arrBody = request.body();
String body = arrBody == null ? "" : new String(arrBody);
log.info("[log request started]\n{} {}\nbody: {}\n{}",
request.httpMethod(),
request.url(),
body,
CombineHeaders(request.headers()));
}
@Override
protected Response logAndRebufferResponse(String configKey,
Level logLevel,
Response response,
long elapsedTime) {
int status = response.status();
String content = "";
if (response.body() != null && !(status == 204 || status == 205)) {
byte[] bodyData;
try {
bodyData = Util.toByteArray(response.body().asInputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
if (bodyData.length > 0) {
content = decodeOrDefault(bodyData, UTF_8, "Binary data");
}
response = response.toBuilder().body(bodyData).build();
}
log.info("[log request ended]\ncost time(ms): {} status:{} from {} {}\n{}\nBody:\n{}",
elapsedTime,
status,
response.request().httpMethod(),
response.request().url(),
CombineHeaders(response.headers()),
content);
return response;
}
@Override
protected void log(String configKey, String format, Object... args) {
//log.info(String.format(methodTag(configKey) + format, args));
}
private static String CombineHeaders(Map<String, Collection<String>> headers) {
StringBuilder sb = new StringBuilder();
if (headers != null && !headers.isEmpty()) {
sb.append("Headers:\r\n");
for (Map.Entry<String, Collection<String>> ob : headers.entrySet()) {
for (String val : ob.getValue()) {
sb.append(" ").append(ob.getKey()).append(": ").append(val).append("\r\n");
}
}
}
return sb.toString();
}
}
6、使用Aspect切面输出日志(不推荐)
这个不推荐,因为它无法打印出具体的url、header等数据,大家可以参考:
不需要yml配置,直接在项目里添加如下代码即可:
package com.example.demo.feign;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class FeignAspect {
// 这个也行 @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) {
log.warn(dto.toString());
} else {
log.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;
}
}
OK,输出的日志如下:
2020-10-13 14:24:48.321 INFO 21304 --- [nio-8080-exec-3] com.example.demo.feign.FeignAspect : FeignAspect.Dto(clas=com.example.demo.feign.FeignDemo|com.sun.proxy.$Proxy72, method=test, para=null, result="<!DOCTYPE html>\r\n<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class=\"bg s_ipt_wr\"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class=\"bg s_btn_wr\"><input type=submit id=su value=百度一下 class=\"bg s_btn\" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href=\"http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === \"\" ? \"?\" : \"&\")+ \"bdorz_come=1\")+ '\" name=\"tj_login\" class=\"lb\">登录</a>');\r\n </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style=\"display: block;\">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>\r\n", costTime=22, remark=null, exp=null)
注:Feign官方文档:
https://cloud.spring.io/spring-cloud-openfeign/reference/html/
更多推荐
所有评论(0)