Skip to content

Commit 2438a85

Browse files
author
adriancole
committed
fix issue OpenFeign#16: Wire is now Logger, with configurable Logger.Level
1 parent 5bf4569 commit 2438a85

7 files changed

Lines changed: 385 additions & 169 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
### Version 3.0
2+
* Wire is now Logger, with configurable Logger.Level.
23
* decoupled ErrorDecoder from fallback handling
34
* Decoders can throw checked exceptions, but needn't declare Throwable
45
* Decoders no longer read methodKey

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,17 @@ Almost all configuration of Feign is represented as Map bindings, where the key
9595
return ImmutableMap.of("GitHub", gsonDecoder);
9696
}
9797
```
98-
#### Wire Logging
99-
You can log the http messages going to and from the target by setting up a `Wire`. Here's the easiest way to do that:
98+
#### Logging
99+
You can log the http messages going to and from the target by setting up a `Logger`. Here's the easiest way to do that:
100100
```java
101101
@Module(overrides = true)
102102
class Overrides {
103-
@Provides @Singleton Wire provideWire() {
104-
return new Wire.LoggingWire().appendToFile("logs/http-wire.log");
103+
@Provides @Singleton Logger.Level provideLoggerLevel() {
104+
return Logger.Level.FULL;
105+
}
106+
107+
@Provides @Singleton Logger provideLogger() {
108+
return new Logger.JavaLogger().appendToFile("logs/http.log");
105109
}
106110
}
107111
GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonGitHubModule(), new Overrides());

feign-core/src/main/java/feign/Feign.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import dagger.Provides;
2828
import feign.Request.Options;
2929
import feign.Target.HardCodedTarget;
30-
import feign.Wire.NoOpWire;
30+
import feign.Logger.NoOpLogger;
3131
import feign.codec.BodyEncoder;
3232
import feign.codec.Decoder;
3333
import feign.codec.ErrorDecoder;
@@ -80,6 +80,12 @@ public static ObjectGraph createObjectGraph(Object... modules) {
8080

8181
@dagger.Module(complete = false, injects = Feign.class, library = true)
8282
public static class Defaults {
83+
84+
@Provides
85+
Logger.Level logLevel() {
86+
return Logger.Level.NONE;
87+
}
88+
8389
@Provides Contract contract() {
8490
return new Contract.DefaultContract();
8591
}
@@ -96,8 +102,8 @@ public static class Defaults {
96102
return new Retryer.Default();
97103
}
98104

99-
@Provides Wire noOp() {
100-
return new NoOpWire();
105+
@Provides Logger noOp() {
106+
return new NoOpLogger();
101107
}
102108

103109
@Provides Map<String, Options> noOptions() {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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;
17+
18+
import java.io.BufferedReader;
19+
import java.io.IOException;
20+
import java.io.Reader;
21+
import java.text.SimpleDateFormat;
22+
import java.util.logging.FileHandler;
23+
import java.util.logging.LogRecord;
24+
import java.util.logging.SimpleFormatter;
25+
26+
import static feign.Util.UTF_8;
27+
import static feign.Util.ensureClosed;
28+
import static feign.Util.valuesOrEmpty;
29+
30+
/**
31+
* Simple logging abstraction for debug messages. Adapted from {@code retrofit.RestAdapter.Log}.
32+
*/
33+
public abstract class Logger {
34+
35+
/**
36+
* Controls the level of logging.
37+
*/
38+
public enum Level {
39+
/**
40+
* No logging.
41+
*/
42+
NONE,
43+
/**
44+
* Log only the request method and URL and the response status code and execution time.
45+
*/
46+
BASIC,
47+
/**
48+
* Log the basic information along with request and response headers.
49+
*/
50+
HEADERS,
51+
/**
52+
* Log the headers, body, and metadata for both requests and responses.
53+
*/
54+
FULL
55+
}
56+
57+
/**
58+
* logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}.
59+
*/
60+
public static class ErrorLogger extends Logger {
61+
final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName());
62+
63+
@Override protected void log(Target<?> target, String format, Object... args) {
64+
System.err.printf(format + "%n", args);
65+
}
66+
}
67+
68+
/**
69+
* logs to the category {@link Logger} at {@link java.util.logging.Level#FINE}, if loggable.
70+
*/
71+
public static class JavaLogger extends Logger {
72+
final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(Logger.class.getName());
73+
74+
@Override void logRequest(Target<?> target, Level logLevel, Request request) {
75+
if (logger.isLoggable(java.util.logging.Level.FINE)) {
76+
super.logRequest(target, logLevel, request);
77+
}
78+
}
79+
80+
@Override
81+
Response logAndRebufferResponse(Target<?> target, Level logLevel, Response response, long elapsedTime) throws IOException {
82+
if (logger.isLoggable(java.util.logging.Level.FINE)) {
83+
return super.logAndRebufferResponse(target, logLevel, response, elapsedTime);
84+
}
85+
return response;
86+
}
87+
88+
@Override protected void log(Target<?> target, String format, Object... args) {
89+
logger.fine(String.format(format, args));
90+
}
91+
92+
/**
93+
* helper that configures jul to sanely log messages.
94+
*/
95+
public JavaLogger appendToFile(String logfile) {
96+
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
97+
logger.setLevel(java.util.logging.Level.FINE);
98+
try {
99+
FileHandler handler = new FileHandler(logfile, true);
100+
handler.setFormatter(new SimpleFormatter() {
101+
@Override
102+
public String format(LogRecord record) {
103+
String timestamp = sdf.format(new java.util.Date(record.getMillis())); // NOPMD
104+
return String.format("%s %s%n", timestamp, record.getMessage()); // NOPMD
105+
}
106+
});
107+
logger.addHandler(handler);
108+
} catch (IOException e) {
109+
throw new IllegalStateException("Could not add file handler.", e);
110+
}
111+
return this;
112+
}
113+
}
114+
115+
public static class NoOpLogger extends Logger {
116+
@Override void logRequest(Target<?> target, Level logLevel, Request request) {
117+
}
118+
119+
@Override
120+
Response logAndRebufferResponse(Target<?> target, Level logLevel, Response response, long elapsedTime) throws IOException {
121+
return response;
122+
}
123+
124+
@Override
125+
protected void log(Target<?> target, String format, Object... args) {
126+
}
127+
}
128+
129+
/**
130+
* Override to log requests and responses using your own implementation.
131+
* Messages will be http request and response text.
132+
*
133+
* @param target useful if using MDC (Mapped Diagnostic Context) loggers
134+
* @param format {@link java.util.Formatter format string}
135+
* @param args arguments applied to {@code format}
136+
*/
137+
protected abstract void log(Target<?> target, String format, Object... args);
138+
139+
void logRequest(Target<?> target, Level logLevel, Request request) {
140+
log(target, "---> %s %s HTTP/1.1", request.method(), request.url());
141+
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
142+
143+
for (String field : request.headers().keySet()) {
144+
for (String value : valuesOrEmpty(request.headers(), field)) {
145+
log(target, "%s: %s", field, value);
146+
}
147+
}
148+
149+
int bytes = 0;
150+
if (request.body() != null) {
151+
bytes = request.body().getBytes(UTF_8).length;
152+
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
153+
log(target, ""); // CRLF
154+
log(target, "%s", request.body());
155+
}
156+
}
157+
log(target, "---> END HTTP (%s-byte body)", bytes);
158+
}
159+
}
160+
161+
Response logAndRebufferResponse(Target<?> target, Level logLevel, Response response, long elapsedTime) throws IOException {
162+
log(target, "<--- HTTP/1.1 %s %s (%sms)", response.status(), response.reason(), elapsedTime);
163+
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
164+
165+
for (String field : response.headers().keySet()) {
166+
for (String value : valuesOrEmpty(response.headers(), field)) {
167+
log(target, "%s: %s", field, value);
168+
}
169+
}
170+
171+
if (response.body() != null) {
172+
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
173+
log(target, ""); // CRLF
174+
}
175+
176+
Reader body = response.body().asReader();
177+
try {
178+
StringBuilder buffered = new StringBuilder();
179+
BufferedReader reader = new BufferedReader(body);
180+
String line;
181+
while ((line = reader.readLine()) != null) {
182+
buffered.append(line);
183+
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
184+
log(target, "%s", line);
185+
}
186+
}
187+
String bodyAsString = buffered.toString();
188+
log(target, "<--- END HTTP (%s-byte body)", bodyAsString.getBytes(UTF_8).length);
189+
return Response.create(response.status(), response.reason(), response.headers(), bodyAsString);
190+
} finally {
191+
ensureClosed(response.body());
192+
}
193+
}
194+
}
195+
return response;
196+
}
197+
}

feign-core/src/main/java/feign/MethodHandler.java

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package feign;
1717

1818
import java.io.IOException;
19+
import java.util.concurrent.TimeUnit;
1920

2021
import javax.inject.Inject;
2122
import javax.inject.Provider;
@@ -42,26 +43,31 @@ static class Factory {
4243

4344
private final Client client;
4445
private final Provider<Retryer> retryer;
45-
private final Wire wire;
46+
private final Logger logger;
47+
private final Logger.Level logLevel;
4648

47-
@Inject Factory(Client client, Provider<Retryer> retryer, Wire wire) {
49+
@Inject Factory(Client client, Provider<Retryer> retryer, Logger logger, Logger.Level logLevel) {
4850
this.client = checkNotNull(client, "client");
4951
this.retryer = checkNotNull(retryer, "retryer");
50-
this.wire = checkNotNull(wire, "wire");
52+
this.logger = checkNotNull(logger, "logger");
53+
this.logLevel = checkNotNull(logLevel, "logLevel");
5154
}
5255

53-
public MethodHandler create(Target<?> target, MethodMetadata md,
54-
BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) {
55-
return new SynchronousMethodHandler(target, client, retryer, wire, md, buildTemplateFromArgs, options, decoder, errorDecoder);
56+
public MethodHandler create(Target<?> target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs,
57+
Options options, Decoder decoder, ErrorDecoder errorDecoder) {
58+
return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs,
59+
options, decoder, errorDecoder);
5660
}
5761
}
5862

5963
static final class SynchronousMethodHandler extends MethodHandler {
6064
private final Decoder decoder;
6165

62-
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Wire wire, MethodMetadata metadata,
63-
BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) {
64-
super(target, client, retryer, wire, metadata, buildTemplateFromArgs, options, errorDecoder);
66+
private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
67+
Logger.Level logLevel, MethodMetadata metadata,
68+
BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder decoder,
69+
ErrorDecoder errorDecoder) {
70+
super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder);
6571
this.decoder = checkNotNull(decoder, "decoder for %s", target);
6672
}
6773

@@ -79,18 +85,21 @@ private SynchronousMethodHandler(Target<?> target, Client client, Provider<Retry
7985
protected final Target<?> target;
8086
protected final Client client;
8187
protected final Provider<Retryer> retryer;
82-
protected final Wire wire;
88+
protected final Logger logger;
89+
protected final Logger.Level logLevel;
8390

8491
protected final BuildTemplateFromArgs buildTemplateFromArgs;
8592
protected final Options options;
8693
protected final ErrorDecoder errorDecoder;
8794

88-
private MethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Wire wire, MethodMetadata metadata,
89-
BuildTemplateFromArgs buildTemplateFromArgs, Options options, ErrorDecoder errorDecoder) {
95+
private MethodHandler(Target<?> target, Client client, Provider<Retryer> retryer, Logger logger,
96+
Logger.Level logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs,
97+
Options options, ErrorDecoder errorDecoder) {
9098
this.target = checkNotNull(target, "target");
9199
this.client = checkNotNull(client, "client for %s", target);
92100
this.retryer = checkNotNull(retryer, "retryer for %s", target);
93-
this.wire = checkNotNull(wire, "wire for %s", target);
101+
this.logger = checkNotNull(logger, "logger for %s", target);
102+
this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
94103
this.metadata = checkNotNull(metadata, "metadata for %s", target);
95104
this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);
96105
this.options = checkNotNull(options, "options for %s", target);
@@ -114,15 +123,24 @@ public Object executeAndDecode(Object[] argv, RequestTemplate template)
114123
throws Throwable {
115124
// create the request from a mutable copy of the input template.
116125
Request request = target.apply(new RequestTemplate(template));
117-
wire.wireRequest(target, request);
126+
127+
if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) {
128+
logger.logRequest(target, logLevel, request);
129+
}
130+
118131
Response response;
132+
long start = System.nanoTime();
119133
try {
120134
response = client.execute(request, options);
121135
} catch (IOException e) {
122136
throw errorExecuting(request, e);
123137
}
138+
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
139+
124140
try {
125-
response = wire.wireAndRebufferResponse(target, response);
141+
if (logLevel.ordinal() > Logger.Level.NONE.ordinal()) {
142+
response = logger.logAndRebufferResponse(target, logLevel, response, elapsedTime);
143+
}
126144
if (response.status() >= 200 && response.status() < 300) {
127145
return decode(argv, response);
128146
} else {

0 commit comments

Comments
 (0)