Feign 客户端请求和响应以及 URL 日志记录

Feign Client request and response and URL Logging

如何记录 Feign 客户端请求、响应和 URL 的负载。我必须实施拦截器吗?因为我的要求是在数据库的特殊 table 上记录请求和响应。

Feign 客户端响应没有拦截器。 Feign客户端唯一可用的请求拦截器。

最好的解决方案是使用 RestTemplate 而不是 Feign:

@Configuration
public class RestConfiguration {
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate
                = new RestTemplate(
                new BufferingClientHttpRequestFactory(
                        new SimpleClientHttpRequestFactory()
                )
        );

        List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
        if (CollectionUtils.isEmpty(interceptors)) {
            interceptors = new ArrayList<>();
        }
        interceptors.add(new UserRestTemplateClientInterceptor());
        restTemplate.setInterceptors(interceptors);
        return restTemplate;
    }

}

和@Autowire restTemplate 你想使用的地方如下:

@Autowire
RestTemplate restTemplate;

Feign 提供了一个 Logger 接口,可以记录完整的请求和响应。您需要在 Feign Builder 或配置中设置 Logger.Level

Feign.builder()
   .logLevel(Logger.Level.FULL) // this will log the request and response
   .target(MyApi, "my host");

Feign 具有开箱即用的日志记录机制,可以通过简单的步骤实现。

如果您使用的是spring-cloud-starter-feign

假装使用 Slf4jLogger 进行日志记录。Feign logging documentation

根据文档,可以配置以下日志记录级别,

  • NONE - 无日志记录(默认)。
  • BASIC - 仅记录请求方法和URL以及响应状态代码和执行时间。
  • HEADERS - 记录基本信息以及请求和响应 headers。
  • FULL - 记录请求和响应的 headers、body 和元数据。

注入 Logger.Level bean 就足够了。

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.BASIC;
    }

如果您更喜欢使用配置属性而不是全部配置 @FeignClient,您可以使用默认的伪装名称创建配置属性。

feign:
  client:
    config:
      default:
        loggerLevel: basic

如果您使用的是'io.github.openfeign:feign-core'

如果您正在构建 Feign 生成器,那么您可以提及 logLevel(Level.BASIC)

Feign.builder()
    .logger(new Slf4jLogger())
    .logLevel(Level.BASIC)
    .target(SomeFeignClient.class, url);

我们可以灵活地自定义日志消息

默认的feign请求和响应日志记录

Request logging

Resopnse logging

我们可以通过覆盖 Logger#logRequestLogger#logAndRebufferResponse 方法来自定义假请求、响应日志记录模式。在下面的示例中,我们自定义了请求日志记录模式

log(configKey, "---> %s %s HTTP/1.1 (%s-byte body) ", request.httpMethod().name(), request.url(), bodyLength);

和响应记录模式

log(configKey, "<--- %s %s HTTP/1.1 %s (%sms) ", request.httpMethod().name(), request.url(), status, elapsedTime);

完整示例是


import feign.Logger;
import feign.Request;
import feign.Response;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

import static feign.Logger.Level.HEADERS;

@Slf4j
public class CustomFeignRequestLogging extends Logger {

    @Override
    protected void logRequest(String configKey, Level logLevel, Request request) {

        if (logLevel.ordinal() >= HEADERS.ordinal()) {
            super.logRequest(configKey, logLevel, request);
        } else {
            int bodyLength = 0;
            if (request.requestBody().asBytes() != null) {
                bodyLength = request.requestBody().asBytes().length;
            }
            log(configKey, "---> %s %s HTTP/1.1 (%s-byte body) ", request.httpMethod().name(), request.url(), bodyLength);
        }
    }

    @Override
    protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime)
            throws IOException {
        if (logLevel.ordinal() >= HEADERS.ordinal()) {
            super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
        } else {
            int status = response.status();
            Request request = response.request();
            log(configKey, "<--- %s %s HTTP/1.1 %s (%sms) ", request.httpMethod().name(), request.url(), status, elapsedTime);
        }
        return response;
    }


    @Override
    protected void log(String configKey, String format, Object... args) {
        log.debug(format(configKey, format, args));
    }

    protected String format(String configKey, String format, Object... args) {
        return String.format(methodTag(configKey) + format, args);
    }
}

注意: 可以通过

轻松记录请求负载
String bodyText =
              request.charset() != null ? new String(request.body(), request.charset()) : null;

但是在读取输入流后要小心写入响应负载 Util.toByteArray(response.body().asInputStream()) 然后你必须像 response.toBuilder().body(bodyData).build() 一样再次构造响应。否则,您将以期望结束。原因是响应流在返回之前被读取并总是关闭,这就是为什么该方法被命名为 logAndRebufferResponse

如何使用自定义CustomFeignRequestLogging

如果您只使用 'io.github.openfeign:feign-core'

构建假客户端
Feign.builder()
     .logger(new CustomFeignRequestLogging())
     .logLevel(feign.Logger.Level.BASIC);

如果您正在使用 'org.springframework.cloud:spring-cloud-starter-openfeign'

@Configuration
public class FeignLoggingConfiguration {

    @Bean
    public CustomFeignRequestLogging customFeignRequestLogging() {
        return new CustomFeignRequestLogging();
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.BASIC;
    }
}

在您的 RestConfiguration 中,您需要设置默认的日志记录级别 feignClient 并由 @Bean feignLogger 覆盖,例如:

@Configuration(proxyBeanMethods = false)
@EnableCircuitBreaker
@EnableFeignClients(basePackageClasses = [Application::class])
class RestConfiguration: WebMvcConfigurer {

    @Bean
    fun feignLoggerLevel(): Logger.Level {
        return Logger.Level.FULL
    }

    @Bean
    fun feignLogger(): Logger {
        return FeignClientLogger()
    }
}

并根据需要实现您的记录器。例如以日志格式登录:

import feign.Logger
import feign.Request
import feign.Response
import feign.Util.*
import org.slf4j.LoggerFactory

class FeignClientLogger : Logger() {
    private val log = LoggerFactory.getLogger(this::class.java)

    override fun logRequest(configKey: String?, logLevel: Level?, request: Request?) {
        if (request == null)
            return

        val feignRequest = FeignRequest()
        feignRequest.method = request.httpMethod().name
        feignRequest.url = request.url()
        for (field in request.headers().keys) {
            for (value in valuesOrEmpty(request.headers(), field)) {
                feignRequest.addHeader(field, value)
            }
        }

        if (request.requestBody() != null) {
            feignRequest.body = request.requestBody().asString()
        }

        log.trace(feignRequest.toString())
    }

    override fun logAndRebufferResponse(
        configKey: String?,
        logLevel: Level?,
        response: Response?,
        elapsedTime: Long
    ): Response? {
        if (response == null)
            return response

        val feignResponse = FeignResponse()
        val status = response.status()
        feignResponse.status = response.status()
        feignResponse.reason =
            (if (response.reason() != null && logLevel!! > Level.NONE) " " + response.reason() else "")
        feignResponse.duration = elapsedTime

        if (logLevel!!.ordinal >= Level.HEADERS.ordinal) {
            for (field in response.headers().keys) {
                for (value in valuesOrEmpty(response.headers(), field)) {
                    feignResponse.addHeader(field, value)
                }
            }

            if (response.body() != null && !(status == 204 || status == 205)) {
                val bodyData: ByteArray = toByteArray(response.body().asInputStream())
                if (logLevel.ordinal >= Level.FULL.ordinal && bodyData.isNotEmpty()) {
                    feignResponse.body = decodeOrDefault(bodyData, UTF_8, "Binary data")
                }
                log.trace(feignResponse.toString())

                return response.toBuilder().body(bodyData).build()
            } else {
                log.trace(feignResponse.toString())
            }
        }
        return response
    }

    override fun log(p0: String?, p1: String?, vararg p2: Any?) {}
}

class FeignResponse {
    var status = 0
    var reason: String? = null
    var duration: Long = 0
    private val headers: MutableList<String> = mutableListOf()
    var body: String? = null

    fun addHeader(key: String?, value: String?) {
        headers.add("$key: $value")
    }

    override fun toString() =
        """{"type":"response","status":"$status","duration":"$duration","headers":$headers,"body":$body,"reason":"$reason"}"""
}

class FeignRequest {
    var method: String? = null
    var url: String? = null
    private val headers: MutableList<String> = mutableListOf()
    var body: String? = null

    fun addHeader(key: String?, value: String?) {
        headers.add("$key: $value")
    }

    override fun toString() =
        """{"type":"request","method":"$method","url":"$url","headers":$headers,"body":$body}"""
}