Feign Client allows us to abstract the mechanics of calling a REST service. We need to configure and annotate the Feign Client interface to call a RESTful web service by making a simple Java method call.
@FeignClient(name = "UserService", url = "${userService.base_url}")
public interface UserServiceClient {
@GetMapping("/users")
ResponseEntity<UserServiceResponse> getUsers();
}
Feign provides an out-of-the-box solution to log the outgoing requests and the following responses. Read this article to know how to do the configuration for it.
Feign uses SL4j Logger under the hood, and the default logging configuration prints each piece of information on a separate line, which I found disturbing. So, I created a custom logger for the Feign clients to customize the log statements.
Creating our own Custom Logger #
We create a configuration class in Spring Boot that instantiates a bean of our custom logger class. The custom logger extends the original Logger
implementation and overrides the necessary methods.
@Slf4j
@Configuration
public class FeignConfig {
@Bean
public CustomFeignClientLogger customFeignClientLogger() {
return new CustomFeignClientLogger();
}
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
static class CustomFeignClientLogger extends Logger {
@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
/*
* This method is intentionally left blank to prevent it
* from logging the request. We will log both the request and the response
* in the logAndRebufferResponse method.
*/
}
@Override
protected Response logAndRebufferResponse(
String configKey, Level logLevel, Response response, long elapsedTime) {
int status = response.status();
Request request = response.request();
String responseString = "";
Response clonedResponse = response.toBuilder().build();
if (Objects.nonNull(response.body())) {
try {
byte[] bodyData = Util.toByteArray(response.body().asInputStream());
Response.Body responseBodyCopy = response.toBuilder().body(bodyData).build().body();
clonedResponse = response.toBuilder().body(responseBodyCopy).build();
responseString = StringUtil.toString(responseBodyCopy.asReader(StandardCharsets.UTF_8));
} catch (IOException exception) {
log.error(
"Error while reading the Feign client response || Error Details: {}",
exception.getMessage());
}
}
if (Objects.nonNull(request.body())) {
log(
configKey,
"%n%n---> %s %s %nRequest: %s %n %n<--- %s %s %s (%sms) %nResponse: %s",
request.httpMethod().name(),
request.url(),
new String(request.body()),
request.httpMethod().name(),
request.url(),
status,
elapsedTime,
responseString);
}
return clonedResponse;
}
@Override
protected void log(String configKey, String format, Object... args) {
log.info(format(configKey, format, args));
}
protected String format(String configKey, String format, Object... args) {
String methodTag = methodTag(configKey);
String formattedString = String.format(format, args);
return methodTag + formattedString;
}
}
}
We read the Response.body
as an InputStream
. Response streams are read and always closed before it gets returned. So, we have to clone the whole Response
object and return the cloned response from the logAndRebufferResponse
method so that we don’t encounter a ‘stream is closed’ IOException later.
If we annotate the class with the @Configuration
annotation then this custom logger configuration will be picked up by all the Feign Clients in our service.
Here is how the feign log statements look now.