简体   繁体   中英

Feign Client request and response and URL Logging

How I can log the payload of Feign client request, response and URL. do I have to Implement an Interceptor? Because my requirement is logging the request and response on a special table on the database.

Feign has out of box logging mechanism and it can be achieved through simple steps.

If you are using spring-cloud-starter-feign

Feign using Slf4jLogger for logging. Feign logging documentation

As per doc, the below logging levels are available to configure,

  • NONE - No logging (DEFAULT).
  • BASIC - Log only the request method and URL and the response status code and execution time.
  • HEADERS - Log the basic information along with request and response headers.
  • FULL - Log the headers, body, and metadata for both requests and responses.

Injecting the Logger.Level bean is enough.

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

OR

If you prefer using configuration properties to configured all @FeignClient , you can create configuration properties with default feign name.

feign:
  client:
    config:
      default:
        loggerLevel: basic

If you are using 'io.github.openfeign:feign-core'

If you are constructing the Feign builder then you can mention logLevel(Level.BASIC) as

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

We have the flexibility to customize the logging message

The default feign request and response logging

Request logging

Resopnse logging

we can customize the feign request, response logging pattern by overriding Logger#logRequest and Logger#logAndRebufferResponse methods. In the following example, we have customized request logging pattern

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

and response logging pattern

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

The Full example is


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);
    }
}

NOTE: Request payload can be easily logged through

String bodyText =
              request.charset() != null ? new String(request.body(), request.charset()) : null;

but be careful writing the response payload after you are reading the input stream Util.toByteArray(response.body().asInputStream()) then you have to construct the response again like response.toBuilder().body(bodyData).build() . Otherwise, you will end up with the expection. The reason is response streams are read and always closed before returning, thats why the method is named as logAndRebufferResponse

How to use the custom CustomFeignRequestLogging ?

If you are building feign client using just 'io.github.openfeign:feign-core'

Feign.builder()
     .logger(new CustomFeignRequestLogging())
     .logLevel(feign.Logger.Level.BASIC);

If you are using '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;
    }
}

The accepted answer did not work for me until I added the following settings to my application.yml file:

logging:
  level:
    com:
      mypackage1:
        mysubackage1:
          mysubpackage2: DEBUG

There is no interceptor for Feign client response. The request interceptor the only available for Feign client.

The best solution will be by using RestTemplate rather than 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;
    }

}

And the @Autowire the restTemplate where you want to use as the following:

@Autowire
RestTemplate restTemplate;

Feign provides a Logger interface that can log the full Request and Response. You will need to set the Logger.Level in the Feign Builder or Configuration.

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

in your RestConfiguration you need to up default level of logging feignClient and override by @Bean feignLogger like:

@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()
    }
}

and implement your logger as you want. For example logging in logbook format:

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}"""
}

I am using Feign client builder as follows

@Bean
public VpsFeignClient vpsFeignClient() {
  return Feign.builder()
      .encoder(new FormEncoder(new GsonEncoder()))
      .decoder(new GsonDecoder())
      .logger(new Slf4jLogger(VpsFeignClient.class))
      .logLevel(feignLoggerLevel())
      .retryer(new Default())
      .errorDecoder(new CustomServerErrorDecoder())
      .requestInterceptor(template -> {
        //Set some header if necessary
        template.header("Content-Type", "application/json");

      })
      .contract(new SpringMvcContract())
      .target(VpsFeignClient.class, dataVpsEndpoint);
}

And my feign is

public interface VpsFeignClient {

  @RequestMapping(path = "/test", method = RequestMethod.GET)
  TimeRespDto getTestValue();

}

I worked for me. Hope to help you!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM