【feign】重写OpenFeign的Client记录日志

概述

项目里使用了Feign进行远程调用,有时为了便于排查问题,需要记录请求和响应日志,下面简介一下如何保存Feign日志到数据库(Redis/MongoDB):

  • 重写FeignClient记录日志
  • 使用Aspect切面记录日志

本文依赖:

  • spring-boot-starter-parent:2.4.2
  • spring-cloud-starter-openfeign:3.0.0

重写FeignClient记录日志

那么怎么才能让OpenFeign记录请求和响应日志呢?

默认情况下,OpenFeign使用feign.Client.Default发起http请求。我们可以重写Client,并注入Bean来替换掉feign.Client.Default,从而实现日志记录,当然也可以做其它事情,比如添加Header。

通过对源码feign.SynchronousMethodHandler#executeAndDecode response = client.execute(request, options);分析不难发现:执行request请求以及接收response响应的是feign.Client(默认feign.Client.Default1)。重写这个Client,spring 容器启动的时候创建我们重写的Client便可以实现。由于feign提供的Response.class是final类型,导致我们没有办法进行流copy,所以我们需要创建一个类似BufferingClientHttpRequestFactory东西进行流copy。

在FeignClient中配置

@FeignClient(url = "${weather.api.url}", name = "logFeignClient", configuration = FeignConfiguration.class)

编写FeignConfiguration

public class FeignConfiguration {
    @Bean
    public Client feignClient() {
        return new LogClient(null, null);
    }
}

重写Client

@Slf4j
public class LogClient extends Client.Default {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public LogClient(SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier) {
        super(socketFactory, hostnameVerifier);
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Exception exception = null;
        BufferingFeignClientResponse bufferingFeignClientResponse = null;
        try {
            bufferingFeignClientResponse = new BufferingFeignClientResponse(super.execute(request, options));
        } catch (Exception exp) {
            log.error(exp.getMessage(), exp);
            exception = exp;
            throw exp;
        } finally {
            stopWatch.stop();
            this.logAndSave(request, bufferingFeignClientResponse, stopWatch, exception);
        }

        Response response = bufferingFeignClientResponse.getResponse().toBuilder()
                .body(bufferingFeignClientResponse.getBody(), bufferingFeignClientResponse.getResponse().body().length())
                .build();
        bufferingFeignClientResponse.close();

        return response;
    }

    private void logAndSave(Request request, BufferingFeignClientResponse bufferingFeignClientResponse, StopWatch stopWatch, Exception exception) {
        // 组装request及response信息
        StringBuilder sb = new StringBuilder("[log started]\r\n");
        sb.append(request.httpMethod()).append(" ").append(request.url()).append("\r\n");
        // 请求Header
        combineHeaders(sb, request.headers());
        combineRequestBody(sb, request.body(), request.requestTemplate().queries());
        sb.append("\r\nResponse cost time(ms): ").append(stopWatch.getLastTaskTimeMillis());
        if (bufferingFeignClientResponse != null) {
            sb.append("  status: ").append(bufferingFeignClientResponse.status());
        }
        sb.append("\r\n");
        if (bufferingFeignClientResponse != null) {
            // 响应Header
            combineHeaders(sb, bufferingFeignClientResponse.headers());
            combineResponseBody(sb, bufferingFeignClientResponse.toBodyString(), bufferingFeignClientResponse.headers().get(HttpHeaders.CONTENT_TYPE));
        }
        if (exception != null) {
            sb.append("Exception:\r\n  ").append(exception.getMessage()).append("\r\n");
        }
        sb.append("\r\n[log ended]");
        log.debug(sb.toString());
        // 保存日志信息至缓存,可替换成MySQL或者MongoDB存储
        redisTemplate.opsForValue().set("sbLog" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")), sb.toString());
    }

    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 combineRequestBody(StringBuilder sb, byte[] body, Map<String, Collection<String>> params) {
        if (params != null) {
            sb.append("Request Params:\r\n").append("  ").append(params).append("\r\n");
        }
        if (body != null && body.length > 0) {
            sb.append("Request Body:\r\n").append("  ").append(new String(body)).append("\r\n");
        }
    }

    private static void combineResponseBody(StringBuilder sb, String respStr, Collection<String> collection) {
        if (respStr == null) {
            return;
        }
        if (collection.contains(MediaType.APPLICATION_JSON_VALUE)) {
            try {
                respStr = JSON.parseObject(respStr).toString();
                //no care this exception
            } catch (JSONException ignored) {
            }
        }
        sb.append("Body:\r\n").append(respStr).append("\r\n");
    }

    static final class BufferingFeignClientResponse implements Closeable {
        private final 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 toBodyString() {
            try {
                return new String(toByteArray(getBody()), UTF_8);
            } catch (Exception e) {
                return super.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() {
            ensureClosed(response);
        }
    }
}

使用Aspect切面记录日志

这个不推荐,因为它无法打印出具体的url、header等数据,有兴趣的可以看看全局记录Feign的请求和响应日志手动AOP这两篇文章

参考


  1. 官方文档提到: Client feignClient: If Spring Cloud LoadBalancer is on the classpath, FeignBlockingLoadBalancerClient is used. If none of them is on the classpath, the default feign client is used. ↩︎

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值