Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to exclude headers from logs #1530

Merged
merged 7 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ with the following alterations:
---
### Customization

Feign has several aspects that can be customized.
Feign has several aspects that can be customized.
For simple cases, you can use `Feign.builder()` to construct an API interface with your custom components.<br>
For request setting, you can use `options(Request.Options options)` on `target()` to set connectTimeout, connectTimeoutUnit, readTimeout, readTimeoutUnit, followRedirects.<br>
For example:
Expand Down Expand Up @@ -765,6 +765,14 @@ public class Example {

The SLF4JLogger (see above) may also be of interest.

There is way to filter sensitive information in headers like authorization data, tokens etc.
```java
Feign.builder()
.logger(new Logger.JavaLogger(header -> !header.getKey().equals("X-Token"),
header -> !header.getKey().equals("X-Sign")))
...
```


#### Request Interceptors
When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`.
Expand Down
86 changes: 72 additions & 14 deletions core/src/main/java/feign/Logger.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
Expand All @@ -16,19 +16,51 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Map;
import java.util.function.Predicate;
import java.util.logging.FileHandler;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import static feign.Util.*;
import static java.util.Objects.nonNull;
import static java.util.Objects.requireNonNull;

/**
* Simple logging abstraction for debug messages. Adapted from {@code retrofit.RestAdapter.Log}.
*/
public abstract class Logger {

protected static String methodTag(String configKey) {
return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('(')))
.append("] ").toString();
return '[' + configKey.substring(0, configKey.indexOf('(')) + "] ";
}

protected Predicate<Map.Entry<String, Collection<String>>> requestHeaderFilter;
vitalijr2 marked this conversation as resolved.
Show resolved Hide resolved
protected Predicate<Map.Entry<String, Collection<String>>> responseHeaderFilter;

/**
* Default logger constructor
*/
protected Logger() {
requestHeaderFilter = header -> !(header.getValue() == null || header.getValue().isEmpty());
vitalijr2 marked this conversation as resolved.
Show resolved Hide resolved
responseHeaderFilter = header -> !(header.getValue() == null || header.getValue().isEmpty());
}

/**
* Logger constructor with additional header filters
*
* @param additionalRequestHeaderFilter additional request header filter, can be null
* @param additionalResponseHeaderFilter additional response header filter, can be null
*/
protected Logger(Predicate<Map.Entry<String, Collection<String>>> additionalRequestHeaderFilter,
Predicate<Map.Entry<String, Collection<String>>> additionalResponseHeaderFilter) {
this();
if (nonNull(additionalRequestHeaderFilter)) {
requestHeaderFilter = requestHeaderFilter.and(additionalRequestHeaderFilter);
}
if (nonNull(additionalResponseHeaderFilter)) {
responseHeaderFilter = responseHeaderFilter.and(additionalResponseHeaderFilter);
}
}

/**
Expand All @@ -45,11 +77,10 @@ protected void logRequest(String configKey, Level logLevel, Request request) {
log(configKey, "---> %s %s HTTP/1.1", request.httpMethod().name(), request.url());
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {

for (String field : request.headers().keySet()) {
for (String value : valuesOrEmpty(request.headers(), field)) {
log(configKey, "%s: %s", field, value);
}
}
request.headers().entrySet().stream()
.filter(requestHeaderFilter)
.forEach(header -> header.getValue()
.forEach(value -> log(configKey, "%s: %s", header.getKey(), value)));

int bodyLength = 0;
if (request.body() != null) {
Expand Down Expand Up @@ -83,11 +114,10 @@ protected Response logAndRebufferResponse(String configKey,
log(configKey, "<--- HTTP/1.1 %s%s (%sms)", status, reason, elapsedTime);
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {

for (String field : response.headers().keySet()) {
for (String value : valuesOrEmpty(response.headers(), field)) {
log(configKey, "%s: %s", field, value);
}
}
response.headers().entrySet().stream()
.filter(responseHeaderFilter)
.forEach(header -> header.getValue()
.forEach(value -> log(configKey, "%s: %s", header.getKey(), value)));

int bodyLength = 0;
if (response.body() != null && !(status == 204 || status == 205)) {
Expand Down Expand Up @@ -183,7 +213,7 @@ public JavaLogger() {

/**
* Constructor for JavaLogger class
*
*
* @param loggerName a name for the logger. This should be a dot-separated name and should
* normally be based on the package name or class name of the subsystem, such as java.net
* or javax.swing
Expand All @@ -201,6 +231,34 @@ public JavaLogger(Class<?> clazz) {
logger = java.util.logging.Logger.getLogger(clazz.getName());
}

/**
* Constructor for JavaLogger class
*
* @param loggerName loggerName a name for the logger
* @param requestHeaderFilter additional request header filter, can be null
* @param responseHeaderFilter additional response header filter, can be null
*/
public JavaLogger(String loggerName,
Predicate<Map.Entry<String, Collection<String>>> requestHeaderFilter,
Predicate<Map.Entry<String, Collection<String>>> responseHeaderFilter) {
super(requestHeaderFilter, responseHeaderFilter);
logger = java.util.logging.Logger.getLogger(loggerName);
}

/**
* Constructor for JavaLogger class
*
* @param clazz the returned logger will be named after clazz
* @param requestHeaderFilter additional request header filter, can be null
* @param responseHeaderFilter additional response header filter, can be null
*/
public JavaLogger(Class<?> clazz,
Predicate<Map.Entry<String, Collection<String>>> requestHeaderFilter,
Predicate<Map.Entry<String, Collection<String>>> responseHeaderFilter) {
super(requestHeaderFilter, responseHeaderFilter);
logger = java.util.logging.Logger.getLogger(clazz.getName());
}

@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
if (logger.isLoggable(java.util.logging.Level.FINE)) {
Expand Down
14 changes: 11 additions & 3 deletions core/src/test/java/feign/LoggerTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
Expand Down Expand Up @@ -49,7 +49,7 @@ public class LoggerTest {
interface SendsStuff {

@RequestLine("POST /")
@Headers("Content-Type: application/json")
@Headers({"Content-Type: application/json", "X-Token: qwerty"})
@Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
String login(
@Param("customer_name") String customer,
Expand Down Expand Up @@ -99,7 +99,7 @@ public static Iterable<Object[]> data() {

@Test
public void levelEmits() {
server.enqueue(new MockResponse().setBody("foo"));
server.enqueue(new MockResponse().setHeader("Y-Powered-By", "Mock").setBody("foo"));

SendsStuff api = Feign.builder()
.logger(logger)
Expand Down Expand Up @@ -383,9 +383,17 @@ public Retryer clone() {

private static final class RecordingLogger extends Logger implements TestRule {

private static final String PREFIX_X = "x-";
private static final String PREFIX_Y = "y-";

private final List<String> messages = new ArrayList<>();
private final List<String> expectedMessages = new ArrayList<>();

RecordingLogger() {
super(header -> !header.getKey().toLowerCase().startsWith(PREFIX_X),
header -> !header.getKey().toLowerCase().startsWith(PREFIX_Y));
}

void expectMessages(List<String> expectedMessages) {
this.expectedMessages.addAll(expectedMessages);
}
Expand Down
4 changes: 2 additions & 2 deletions core/src/test/java/feign/MultipleLoggerTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
Expand Down Expand Up @@ -41,7 +41,7 @@ public void testAppendSeveralFilesToOneJavaLogger() throws Exception {
}

@Test
public void testJavaLoggerInstantationWithLoggerName() throws Exception {
public void testJavaLoggerInstantiationWithLoggerName() throws Exception {
Logger.JavaLogger l1 = new Logger.JavaLogger("First client")
.appendToFile(tmp.newFile("1.log").getAbsolutePath());
Logger.JavaLogger l2 = new Logger.JavaLogger("Second client")
Expand Down
29 changes: 28 additions & 1 deletion slf4j/src/main/java/feign/slf4j/Slf4jLogger.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2012-2020 The Feign Authors
* Copyright 2012-2021 The Feign Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
Expand All @@ -16,6 +16,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.function.Predicate;
import feign.Request;
import feign.Response;

Expand All @@ -40,10 +43,34 @@ public Slf4jLogger(String name) {
this(LoggerFactory.getLogger(name));
}

public Slf4jLogger(Predicate<Map.Entry<String, Collection<String>>> requestHeaderFilter,
Predicate<Map.Entry<String, Collection<String>>> responseHeaderFilter) {
this(feign.Logger.class, requestHeaderFilter, responseHeaderFilter);
}

public Slf4jLogger(Class<?> clazz,
Predicate<Map.Entry<String, Collection<String>>> requestHeaderFilter,
Predicate<Map.Entry<String, Collection<String>>> responseHeaderFilter) {
this(LoggerFactory.getLogger(clazz), requestHeaderFilter, responseHeaderFilter);
}

public Slf4jLogger(String name,
Predicate<Map.Entry<String, Collection<String>>> requestHeaderFilter,
Predicate<Map.Entry<String, Collection<String>>> responseHeaderFilter) {
this(LoggerFactory.getLogger(name), requestHeaderFilter, responseHeaderFilter);
}

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

Slf4jLogger(Logger logger,
Predicate<Map.Entry<String, Collection<String>>> requestHeaderFilter,
Predicate<Map.Entry<String, Collection<String>>> responseHeaderFilter) {
super(requestHeaderFilter, responseHeaderFilter);
this.logger = logger;
}

@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
if (logger.isDebugEnabled()) {
Expand Down