Skip to content

Commit 2c5d359

Browse files
committed
slf4j: add slf4j integration module (OpenFeign#94)
Adds a new "slf4j" module. A few methods in Logger are now protected rather than package protected to allow access by Logger subclasses that aren't inner classes of Logger.
1 parent 8f894e0 commit 2c5d359

10 files changed

Lines changed: 311 additions & 12 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### Version 6.1.0
2+
* Add [SLF4J](http://www.slf4j.org/) integration
3+
14
### Version 6.0.1
25
* Fix for BasicAuthRequestInterceptor when username and/or password are long.
36

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ Integration requires you to pass your ribbon client name as the host part of the
141141
MyService api = Feign.create(MyService.class, "https://myAppProd", new RibbonModule());
142142
```
143143

144+
### SLF4J
145+
[SLF4JModule](https://github.com/Netflix/feign/tree/master/slf4j) allows directing Feign's logging to [SLF4J](http://www.slf4j.org/), allowing you to easily use a logging backend of your choice (Logback, Log4J, etc.)
146+
147+
To use SLF4J with Feign, add both the SLF4J module and an SLF4J binding of your choice to your classpath. Then, configure Feign to use the Slf4jLogger:
148+
149+
```java
150+
GitHub github = Feign.builder()
151+
.logger(new Slf4jLogger())
152+
.target(GitHub.class, "https://api.github.com");
153+
```
154+
144155
### Decoders
145156
`Feign.builder()` allows you to specify additional configuration such as how to decode a response.
146157

@@ -198,3 +209,5 @@ GitHub github = Feign.builder()
198209
.logLevel(Logger.Level.FULL)
199210
.target(GitHub.class, "https://api.github.com");
200211
```
212+
213+
The SLF4JModule (see above) may also be of interest.

build.gradle

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,18 @@ project(':feign-ribbon') {
118118
testCompile 'com.google.mockwebserver:mockwebserver:20130706'
119119
}
120120
}
121+
122+
project(':feign-slf4j') {
123+
apply plugin: 'java'
124+
125+
test {
126+
useTestNG()
127+
}
128+
129+
dependencies {
130+
compile project(':feign-core')
131+
compile 'org.slf4j:slf4j-api:1.7.5'
132+
testCompile 'org.testng:testng:6.8.5'
133+
testCompile 'org.slf4j:slf4j-simple:1.7.5'
134+
}
135+
}

core/src/main/java/feign/Logger.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,13 @@ public static class ErrorLogger extends Logger {
7070
public static class JavaLogger extends Logger {
7171
final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName());
7272

73-
@Override void logRequest(String configKey, Level logLevel, Request request) {
73+
@Override protected void logRequest(String configKey, Level logLevel, Request request) {
7474
if (logger.isLoggable(java.util.logging.Level.FINE)) {
7575
super.logRequest(configKey, logLevel, request);
7676
}
7777
}
7878

79-
@Override
80-
Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
79+
@Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
8180
if (logger.isLoggable(java.util.logging.Level.FINE)) {
8281
return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
8382
}
@@ -110,16 +109,14 @@ public String format(LogRecord record) {
110109
}
111110

112111
public static class NoOpLogger extends Logger {
113-
@Override void logRequest(String configKey, Level logLevel, Request request) {
112+
@Override protected void logRequest(String configKey, Level logLevel, Request request) {
114113
}
115114

116-
@Override
117-
Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
115+
@Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
118116
return response;
119117
}
120118

121-
@Override
122-
protected void log(String configKey, String format, Object... args) {
119+
@Override protected void log(String configKey, String format, Object... args) {
123120
}
124121
}
125122

@@ -133,7 +130,7 @@ protected void log(String configKey, String format, Object... args) {
133130
*/
134131
protected abstract void log(String configKey, String format, Object... args);
135132

136-
void logRequest(String configKey, Level logLevel, Request request) {
133+
protected void logRequest(String configKey, Level logLevel, Request request) {
137134
log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url());
138135
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
139136

@@ -160,7 +157,7 @@ void logRetry(String configKey, Level logLevel) {
160157
log(configKey, "---> RETRYING");
161158
}
162159

163-
Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
160+
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
164161
log(configKey, "<--- HTTP/1.1 %s %s (%sms)", response.status(), response.reason(), elapsedTime);
165162
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
166163

@@ -200,7 +197,7 @@ IOException logIOException(String configKey, Level logLevel, IOException ioe, lo
200197
return ioe;
201198
}
202199

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

settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
rootProject.name='feign'
2-
include 'core', 'sax', 'gson', 'jackson', 'jaxrs', 'ribbon', 'example-github', 'example-wikipedia'
2+
include 'core', 'sax', 'gson', 'jackson', 'jaxrs', 'ribbon', 'slf4j', 'example-github', 'example-wikipedia'
33

44
rootProject.children.each { childProject ->
55
childProject.name = 'feign-' + childProject.name

slf4j/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SLF4J
2+
===================
3+
4+
This module allows directing Feign's logging to [SLF4J](http://www.slf4j.org/), allowing you to easily use a logging backend of your choice (Logback, Log4J, etc.)
5+
6+
To use SLF4J with Feign, add both the SLF4J module and an SLF4J binding of your choice to your classpath. Then, configure Feign to use the Slf4jLogger:
7+
8+
```java
9+
GitHub github = Feign.builder()
10+
.logger(new Slf4jLogger())
11+
.target(GitHub.class, "https://api.github.com");
12+
```
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.slf4j;
17+
18+
import feign.Request;
19+
import feign.Response;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
23+
import java.io.IOException;
24+
25+
/**
26+
* Logs to SLF4J at the debug level, if the underlying logger has debug logging enabled. The underlying logger can
27+
* be specified at construction-time, defaulting to the logger for {@link feign.Logger}.
28+
*/
29+
public class Slf4jLogger extends feign.Logger {
30+
private final Logger logger;
31+
32+
public Slf4jLogger() {
33+
this(feign.Logger.class);
34+
}
35+
36+
public Slf4jLogger(Class<?> clazz) {
37+
this(LoggerFactory.getLogger(clazz));
38+
}
39+
40+
public Slf4jLogger(String name) {
41+
this(LoggerFactory.getLogger(name));
42+
}
43+
44+
Slf4jLogger(Logger logger) {
45+
this.logger = logger;
46+
}
47+
48+
@Override protected void logRequest(String configKey, Level logLevel, Request request) {
49+
if (logger.isDebugEnabled()) {
50+
super.logRequest(configKey, logLevel, request);
51+
}
52+
}
53+
54+
@Override protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException {
55+
if (logger.isDebugEnabled()) {
56+
return super.logAndRebufferResponse(configKey, logLevel, response, elapsedTime);
57+
}
58+
return response;
59+
}
60+
61+
@Override protected void log(String configKey, String format, Object... args) {
62+
// Not using SLF4J's support for parameterized messages (even though it would be more efficient) because it would
63+
// require the incoming message formats to be SLF4J-specific.
64+
logger.debug(String.format(methodTag(configKey) + format, args));
65+
}
66+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.slf4j;
17+
18+
import java.lang.reflect.Field;
19+
import java.lang.reflect.Method;
20+
21+
/**
22+
* Lightweight approach to using reflection to bypass access restrictions for testing. If this class grows, it may be
23+
* better to use a testing library instead, such as Powermock.
24+
*/
25+
class ReflectionUtil {
26+
static void setStaticField(Class<?> declaringClass, String fieldName, Object fieldValue) throws Exception {
27+
Field field = declaringClass.getDeclaredField(fieldName);
28+
field.setAccessible(true);
29+
field.set(null, fieldValue);
30+
}
31+
32+
static void invokeVoidNoArgMethod(Class<?> declaringClass, String methodName, Object instance) throws Exception {
33+
Method method = declaringClass.getDeclaredMethod(methodName);
34+
method.setAccessible(true);
35+
method.invoke(instance);
36+
}
37+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.slf4j;
17+
18+
import org.slf4j.LoggerFactory;
19+
import org.slf4j.impl.SimpleLogger;
20+
import org.slf4j.impl.SimpleLoggerFactory;
21+
22+
import java.io.File;
23+
24+
/**
25+
* A testing utility to allow control over {@link SimpleLogger}. In some cases, reflection is used to bypass access
26+
* restrictions.
27+
*/
28+
class SimpleLoggerUtil {
29+
static void initialize(File file, String logLevel) throws Exception {
30+
System.setProperty(SimpleLogger.SHOW_THREAD_NAME_KEY, "false");
31+
System.setProperty(SimpleLogger.LOG_FILE_KEY, file.getAbsolutePath());
32+
System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, logLevel);
33+
resetSlf4j();
34+
}
35+
36+
static void resetToDefaults() throws Exception {
37+
System.clearProperty(SimpleLogger.SHOW_THREAD_NAME_KEY);
38+
System.clearProperty(SimpleLogger.LOG_FILE_KEY);
39+
System.clearProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY);
40+
resetSlf4j();
41+
}
42+
43+
private static void resetSlf4j() throws Exception {
44+
ReflectionUtil.setStaticField(SimpleLogger.class, "INITIALIZED", false);
45+
ReflectionUtil.invokeVoidNoArgMethod(SimpleLoggerFactory.class, "reset", LoggerFactory.getILoggerFactory());
46+
}
47+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2013 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package feign.slf4j;
17+
18+
import feign.Feign;
19+
import feign.Logger;
20+
import feign.Request;
21+
import feign.RequestTemplate;
22+
import feign.Response;
23+
import feign.Util;
24+
import org.slf4j.LoggerFactory;
25+
import org.testng.annotations.AfterMethod;
26+
import org.testng.annotations.Test;
27+
28+
import java.io.File;
29+
import java.io.FileReader;
30+
import java.util.Collection;
31+
import java.util.Collections;
32+
33+
import static org.testng.Assert.assertEquals;
34+
35+
public class Slf4jLoggerTest {
36+
private static final String CONFIG_KEY = "someMethod()";
37+
private static final Request REQUEST =
38+
new RequestTemplate().method("GET").append("http://api.example.com").request();
39+
private static final Response RESPONSE =
40+
Response.create(200, "OK", Collections.<String, Collection<String>>emptyMap(), new byte[0]);
41+
42+
private File logFile;
43+
private Slf4jLogger logger;
44+
45+
@AfterMethod
46+
void tearDown() throws Exception {
47+
SimpleLoggerUtil.resetToDefaults();
48+
logFile.delete();
49+
}
50+
51+
@Test public void useFeignLoggerByDefault() throws Exception {
52+
initializeSimpleLogger("debug");
53+
logger = new Slf4jLogger();
54+
logger.log(CONFIG_KEY, "This is my message");
55+
assertLoggedMessages("DEBUG feign.Logger - [someMethod] This is my message\n");
56+
}
57+
58+
@Test public void useLoggerByNameIfRequested() throws Exception {
59+
initializeSimpleLogger("debug");
60+
logger = new Slf4jLogger("named.logger");
61+
logger.log(CONFIG_KEY, "This is my message");
62+
assertLoggedMessages("DEBUG named.logger - [someMethod] This is my message\n");
63+
}
64+
65+
@Test public void useLoggerByClassIfRequested() throws Exception {
66+
initializeSimpleLogger("debug");
67+
logger = new Slf4jLogger(Feign.class);
68+
logger.log(CONFIG_KEY, "This is my message");
69+
assertLoggedMessages("DEBUG feign.Feign - [someMethod] This is my message\n");
70+
}
71+
72+
@Test public void useSpecifiedLoggerIfRequested() throws Exception {
73+
initializeSimpleLogger("debug");
74+
logger = new Slf4jLogger(LoggerFactory.getLogger("specified.logger"));
75+
logger.log(CONFIG_KEY, "This is my message");
76+
assertLoggedMessages("DEBUG specified.logger - [someMethod] This is my message\n");
77+
}
78+
79+
@Test public void logOnlyIfDebugEnabled() throws Exception {
80+
initializeSimpleLogger("info");
81+
logger = new Slf4jLogger();
82+
logger.log(CONFIG_KEY, "A message with %d formatting %s.", 2, "tokens");
83+
logger.logRequest(CONFIG_KEY, Logger.Level.BASIC, REQUEST);
84+
logger.logAndRebufferResponse(CONFIG_KEY, Logger.Level.BASIC, RESPONSE, 273);
85+
assertLoggedMessages("");
86+
}
87+
88+
@Test public void logRequestsAndResponses() throws Exception {
89+
initializeSimpleLogger("debug");
90+
logger = new Slf4jLogger();
91+
logger.log(CONFIG_KEY, "A message with %d formatting %s.", 2, "tokens");
92+
logger.logRequest(CONFIG_KEY, Logger.Level.BASIC, REQUEST);
93+
logger.logAndRebufferResponse(CONFIG_KEY, Logger.Level.BASIC, RESPONSE, 273);
94+
assertLoggedMessages(
95+
"DEBUG feign.Logger - [someMethod] A message with 2 formatting tokens.\n" +
96+
"DEBUG feign.Logger - [someMethod] ---> GET http://api.example.com HTTP/1.1\n" +
97+
"DEBUG feign.Logger - [someMethod] <--- HTTP/1.1 200 OK (273ms)\n"
98+
);
99+
}
100+
101+
private void initializeSimpleLogger(String logLevel) throws Exception {
102+
logFile = File.createTempFile(getClass().getName(), ".log");
103+
SimpleLoggerUtil.initialize(logFile, logLevel);
104+
}
105+
106+
private void assertLoggedMessages(String expectedMessages) throws Exception {
107+
assertEquals(Util.toString(new FileReader(logFile)), expectedMessages);
108+
}
109+
}

0 commit comments

Comments
 (0)