The Problem

When it comes to Feign and logging its requests and responses, it provides Logger - an abstraction layer with dedicated set of log Levels:

Table 1. Feign Log Levels
Log Level Description

NONE

No logging at all.

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.

Most popular Feign Logger implementation is the SLF4j that outputs Feign information to DEBUG level of the SLF4j, which has couple of downsides:

  1. How much is logged is still configured via Feign, which makes it pretty difficult to change the level at runtime. This struck me, when one of the external dependencies my service was using started to misbehave - I was not able to quickly gather debug information.

  2. Having everything logged at DEBUG level contradicts the general SLF4j concept of using log levels to determine how much detailed information we want to see.

The Remedy

The solution I created is an enhanced version of the original Slf4jLogger implementation. It embraces Slf4j concepts, providing better logging flexibility:

  1. You can change amount of log data you want, by changing SLF4j log level of a specific Feign client class.

  2. Sl4j levels correspond to logged data:

Table 2. Feign Log Levels
Log Level Description

INFO

Log the request method, request URL, response status code and the execution time.

DEBUG

In addition to INFO level, log payload body of both request and response.

TRACE

In addition to DEBUG, log request and response headers and metadata.

Slf4jLoggerLevelBasedFeignLogger.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * FeignLogger based on Slf4j logger level:
 * INFO -> Log only the request method and URL and the response status code and execution time.
 * DEBUG -> Additionally, log body of both request and response.
 * TRACE -> Additionally, log headers and metadata of both request and response.
 *
 * Feign Logger level has to be set to FULL.
 * Otherwise, things won't get logged.
 */
public class Slf4jLoggerLevelBasedFeignLogger extends feign.Logger {

    private static final Map<String, BiConsumer<Logger, String>> LOGGER_BY_FORMAT = Map.ofEntries( (1)
            entry("<--- ERROR %s: %s (%sms)", Logger::error),
            entry("<--- END ERROR", Logger::debug),
            entry("<--- HTTP/1.1 %s%s (%sms)", Logger::info),
            entry("---> RETRYING", Logger::info),
            entry("---> %s %s HTTP/1.1", Logger::info),
            entry("---> END HTTP (%s-byte body)", Logger::info),
            entry("<--- END HTTP (%s-byte body)", Logger::info),
            entry("", Logger::debug),
            entry("%s", Logger::debug),
            entry("%s: %s", Logger::trace)
    );

    private final Logger logger;

    public Slf4jLoggerLevelBasedFeignLogger(Class<?> clazz) {
        this(LoggerFactory.getLogger(clazz));
    }

    Slf4jLoggerLevelBasedFeignLogger(Logger logger) {
        this.logger = logger;
    }

    @Override
    protected void log(String configKey, String format, Object... args) {
        LOGGER_BY_FORMAT.get(format).accept(logger, String.format(methodTag(configKey) + format, args));
    }

}
1 Here, we are mapping format values (as implemented in feign.Logger) to specific SLF4j levels. They are most of the time kept in 1:1 relation to a specific logging case.

Usage

You use and configure Feign logging level as you would normally define SLF4j Logger level:

1
2
3
4
GitHub github = Feign.builder()
                 .logger(new Slf4jLoggerLevelBasedFeignLogger(GitHub.class)) (1)
                 .logLevel(Level.FULL) (2)
                 .target(GitHub.class, "https://api.github.com");
1 This is the only difference as you should always want to be able to set Slf4j log level per Feign client.
2 You need to make sure Feign Client has FULL level set up. This is required, due to the way how Feign abstract Logger is implemented.

Sample output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
20-09-2021 [main] INFO  c.e.f.Client.log - [Client#getUser] ---> GET https://reqres.in/api/users/2 HTTP/1.1
20-09-2021 [main] INFO  c.e.f.Client.log - [Client#getUser] ---> END HTTP (0-byte body)
20-09-2021 [main] INFO  c.e.f.Client.log - [Client#getUser] <--- HTTP/1.1 200 OK (287ms)
20-09-2021 [main] TRACE c.e.f.Client.log - [Client#getUser] accept-ranges: bytes
20-09-2021 [main] TRACE c.e.f.Client.log - [Client#getUser] access-control-allow-origin: *
20-09-2021 [main] TRACE c.e.f.Client.log - [Client#getUser] cache-control: max-age=14400
20-09-2021 [main] TRACE c.e.f.Client.log - [Client#getUser] cf-cache-status: HIT
20-09-2021 [main] TRACE c.e.f.Client.log - [Client#getUser] connection: keep-alive
20-09-2021 [main] TRACE c.e.f.Client.log - [Client#getUser] content-length: 280
20-09-2021 [main] TRACE c.e.f.Client.log - [Client#getUser] content-type: application/json; charset=utf-8
20-09-2021 [main] DEBUG c.e.f.Client.log - [Client#getUser]
20-09-2021 [main] DEBUG c.e.f.Client.log - [Client#getUser] {"data":{"id":2,"email":"janet.weaver@reqres.in","first_name":"Janet","last_name":"Weaver","avatar":"https://reqres.in/img/faces/2-image.jpg"},"support":{"url":"https://reqres.in/#support-heading","text":"To keep ReqRes free, contributions towards server costs are appreciated!"}}
20-09-2021 [main] INFO  c.e.f.Client.log - [Client#getUser] <--- END HTTP (280-byte body)

Spring Cloud OpenFeign usage

If you use Feign through Spring Cloud dependency, usage boils down to adding FeignLoggerFactory bean in a @Configuration class:

1
2
3
4
@Bean
FeignLoggerFactory slf4jLoggerLevelBasedFeignLoggerFactory() {
    return Slf4jLoggerLevelBasedFeignLogger::new;
}

also make sure that you have appropriate Logger.Level set:

1
2
3
4
@Bean
Logger.Level level() {
    return Logger.Level.FULL;
}