Feign Slf4j Logger you wish you had
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:
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:
-
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.
-
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:
-
You can change amount of log data you want, by changing SLF4j log level of a specific Feign client class.
-
Sl4j levels correspond to logged data:
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. |
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;
}