0

I'm developing a custom logging framework for springboot to log rest-template requests and response and is working fine. Am trying to implement the same for 'Feign-Client' and am faced with couple of issues.

  1. For request logging, am leveraging FeignRequestInterceptor and it is working fine, only problem here is I cannot retrieve the full request URL. Below method is giving me only relative URL.

    requestTemplate.url()
    
  2. To log the response, only way i could find was the ResponseDecoder. There I'm able to retrieve everything other than the payload. When accessing the payload from

    InputStream is = response.body().asInputStream();
    String payload = new String(IOUtils.toByteArray(is));
    

This method works, but the original stream is closed because of which logging happens fine, but client is throwing exception when returning response.

'trying to open closed stream'

I would like suggestions if there are better ways of logging request response in Feign similar to spring rest-template. Or if the method I have adopted is fine, help me resolve the problems above.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Muthu
  • 11
  • 1
  • 4

2 Answers2

2

You can configure a custom feign.Logger instance to handle this. There are two built in, JavaLogger which uses java.util.logging and Slf4JLogger that uses slf4j. You can create your own logger implementation by extending feign.Logger and registering it as a @Bean.

That logger should be picked up by Spring and registered with your FeignClient. Here is the Logger base class to get you started:

protected abstract void log(String configKey, String format, Object... args);

Create your own instance, implement this method and it will be called before the request and after the response is returned. No need to update the interceptor or create a response decoder.

Kevin Davis
  • 1,193
  • 8
  • 14
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 (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}"""
}