17

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.

Rami Nassar
  • 303
  • 1
  • 4
  • 13

6 Answers6

60

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()) {
            return 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;
    }
}

Community
  • 1
  • 1
Prasanth Rajendran
  • 4,570
  • 2
  • 42
  • 59
  • 1
    Thank you for detailed information, it is helpful for me. – Haven Lin Mar 03 '21 at 01:59
  • How do you plug the CustomFeignRequestLogging to feign config? – Manoj Kumar S Aug 05 '21 at 12:28
  • 1
    @ManojKumarS, good question, I have updated the answer stating How to use the custom CustomFeignRequestLogging? – Prasanth Rajendran Aug 05 '21 at 13:26
  • 2
    This is a very useful answer. I just wanted to add clarity on the part that requires being careful when there is a need of writing the response. Reconstructing the response would look like this: `ByteArray bodyData = Util.toByteArray(response.body().asInputStream())` `Response responseCopy = response.toBuilder().body(bodyData).build()` With this, you can keep using `responseCopy` as desired. – Sam S Dec 02 '22 at 10:18
3

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
mnagdev
  • 384
  • 3
  • 11
1

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

}
cafce25
  • 15,907
  • 4
  • 25
  • 31
phancuongviet
  • 311
  • 2
  • 11
  • When using `@Slf4j` the line `.logger(new Slf4jLogger(VpsFeignClient.class))` did the trick for me. Now I'm able to see the Feign Client logs. – dsicari Jun 04 '23 at 23:35
0

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");
Kevin Davis
  • 1,193
  • 8
  • 14
  • Thank you Kevin. Yes, you right but I want to log the request and response on special table on the database. – Rami Nassar May 26 '19 at 09:11
  • @RamiNassar, That was not in your original question. I recommend either updating your original question with that information or asking another question being more specific. – Kevin Davis May 28 '19 at 16:12
0

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

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;
Rami Nassar
  • 303
  • 1
  • 4
  • 13