${project.groupId}
okhttp-testing-support
diff --git a/okcurl/src/main/java/okhttp3/curl/Main.java b/okcurl/src/main/java/okhttp3/curl/Main.java
index cee260061974..4b1fd210871e 100644
--- a/okcurl/src/main/java/okhttp3/curl/Main.java
+++ b/okcurl/src/main/java/okhttp3/curl/Main.java
@@ -48,6 +48,8 @@
import okhttp3.internal.http.StatusLine;
import okhttp3.internal.http2.Http2;
import okhttp3.internal.platform.Platform;
+import okhttp3.logging.HttpLoggingInterceptor;
+import okhttp3.logging.LoggingEventListener;
import okio.BufferedSource;
import okio.Okio;
import okio.Sink;
@@ -121,6 +123,11 @@ private static String protocols() {
@Option(name = {"-V", "--version"}, description = "Show version number and quit")
public boolean version;
+ @Option(
+ name = {"-v", "--verbose"},
+ description = "Makes " + NAME + " verbose during the operation")
+ public boolean verbose;
+
@Arguments(title = "url", description = "Remote resource URL")
public String url;
@@ -184,6 +191,16 @@ private OkHttpClient createClient() {
builder.sslSocketFactory(sslSocketFactory, trustManager);
builder.hostnameVerifier(createInsecureHostnameVerifier());
}
+ if (verbose) {
+ HttpLoggingInterceptor.Logger logger =
+ new HttpLoggingInterceptor.Logger() {
+ @Override
+ public void log(String message) {
+ System.out.println(message);
+ }
+ };
+ builder.eventListenerFactory(new LoggingEventListener.Factory(logger));
+ }
return builder.build();
}
diff --git a/okhttp-logging-interceptor/src/main/java/okhttp3/logging/LoggingEventListener.java b/okhttp-logging-interceptor/src/main/java/okhttp3/logging/LoggingEventListener.java
new file mode 100644
index 000000000000..cc5adde48e9a
--- /dev/null
+++ b/okhttp-logging-interceptor/src/main/java/okhttp3/logging/LoggingEventListener.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okhttp3.logging;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import okhttp3.Call;
+import okhttp3.Connection;
+import okhttp3.EventListener;
+import okhttp3.Handshake;
+import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+
+/**
+ * An OkHttp EventListener, which logs call events. Can be applied as an
+ * {@linkplain OkHttpClient#eventListenerFactory() event listener factory}.
+ *
+ * The format of the logs created by this class should not be considered stable and may change
+ * slightly between releases. If you need a stable logging format, use your own event listener.
+ */
+public final class LoggingEventListener extends EventListener {
+ private final HttpLoggingInterceptor.Logger logger;
+ private long startNs;
+
+ private LoggingEventListener(HttpLoggingInterceptor.Logger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public void callStart(Call call) {
+ startNs = System.nanoTime();
+
+ logWithTime("callStart: " + call.request());
+ }
+
+ @Override
+ public void dnsStart(Call call, String domainName) {
+ logWithTime("dnsStart: " + domainName);
+ }
+
+ @Override
+ public void dnsEnd(Call call, String domainName, List inetAddressList) {
+ logWithTime("dnsEnd: " + inetAddressList);
+ }
+
+ @Override
+ public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
+ logWithTime("connectStart: " + inetSocketAddress + " " + proxy);
+ }
+
+ @Override
+ public void secureConnectStart(Call call) {
+ logWithTime("secureConnectStart");
+ }
+
+ @Override
+ public void secureConnectEnd(Call call, @Nullable Handshake handshake) {
+ logWithTime("secureConnectEnd");
+ }
+
+ @Override
+ public void connectEnd(
+ Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) {
+ logWithTime("connectEnd: " + protocol);
+ }
+
+ @Override
+ public void connectFailed(
+ Call call,
+ InetSocketAddress inetSocketAddress,
+ Proxy proxy,
+ @Nullable Protocol protocol,
+ IOException ioe) {
+ logWithTime("connectFailed: " + protocol + " " + ioe);
+ }
+
+ @Override
+ public void connectionAcquired(Call call, Connection connection) {
+ logWithTime("connectionAcquired: " + connection);
+ }
+
+ @Override
+ public void connectionReleased(Call call, Connection connection) {
+ logWithTime("connectionReleased");
+ }
+
+ @Override
+ public void requestHeadersStart(Call call) {
+ logWithTime("requestHeadersStart");
+ }
+
+ @Override
+ public void requestHeadersEnd(Call call, Request request) {
+ logWithTime("requestHeadersEnd");
+ }
+
+ @Override
+ public void requestBodyStart(Call call) {
+ logWithTime("requestBodyStart");
+ }
+
+ @Override
+ public void requestBodyEnd(Call call, long byteCount) {
+ logWithTime("requestBodyEnd: byteCount=" + byteCount);
+ }
+
+ @Override
+ public void responseHeadersStart(Call call) {
+ logWithTime("responseHeadersStart");
+ }
+
+ @Override
+ public void responseHeadersEnd(Call call, Response response) {
+ logWithTime("responseHeadersEnd: " + response);
+ }
+
+ @Override
+ public void responseBodyStart(Call call) {
+ logWithTime("responseBodyStart");
+ }
+
+ @Override
+ public void responseBodyEnd(Call call, long byteCount) {
+ logWithTime("responseBodyEnd: byteCount=" + byteCount);
+ }
+
+ @Override
+ public void callEnd(Call call) {
+ logWithTime("callEnd");
+ }
+
+ @Override
+ public void callFailed(Call call, IOException ioe) {
+ logWithTime("callFailed: " + ioe);
+ }
+
+ private void logWithTime(String message) {
+ long timeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
+ logger.log("[" + timeMs + " ms] " + message);
+ }
+
+ public static class Factory implements EventListener.Factory {
+ private final HttpLoggingInterceptor.Logger logger;
+
+ public Factory() {
+ this(HttpLoggingInterceptor.Logger.DEFAULT);
+ }
+
+ public Factory(HttpLoggingInterceptor.Logger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public EventListener create(Call call) {
+ return new LoggingEventListener(logger);
+ }
+ }
+}
diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java
index b2acd9051ac0..48e86ab9cda3 100644
--- a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java
+++ b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.java
@@ -807,7 +807,7 @@ private Request.Builder request() {
return new Request.Builder().url(url);
}
- private static class LogRecorder implements HttpLoggingInterceptor.Logger {
+ static class LogRecorder implements HttpLoggingInterceptor.Logger {
private final List logs = new ArrayList<>();
private int index;
diff --git a/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java
new file mode 100644
index 000000000000..0f0ea2bd715e
--- /dev/null
+++ b/okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okhttp3.logging;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+import okhttp3.Dns;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.SocketPolicy;
+import okhttp3.tls.HandshakeCertificates;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static okhttp3.tls.internal.TlsUtil.localhost;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public final class LoggingEventListenerTest {
+ private static final MediaType PLAIN = MediaType.get("text/plain");
+
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ private final HandshakeCertificates handshakeCertificates = localhost();
+ private final LogRecorder logRecorder = new LogRecorder();
+ private final LoggingEventListener.Factory loggingEventListenerFactory =
+ new LoggingEventListener.Factory(logRecorder);
+ private OkHttpClient client;
+ private HttpUrl url;
+
+ @Before
+ public void setUp() {
+ client =
+ new OkHttpClient.Builder()
+ .eventListenerFactory(loggingEventListenerFactory)
+ .sslSocketFactory(
+ handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager())
+ .retryOnConnectionFailure(false)
+ .build();
+
+ url = server.url("/");
+ }
+
+ @Test
+ public void get() throws Exception {
+ server.enqueue(new MockResponse().setBody("Hello!").setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ assertNotNull(response.body());
+ response.body().bytes();
+
+ logRecorder
+ .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}")
+ .assertLogMatch("dnsStart: " + url.host())
+ .assertLogMatch("dnsEnd: \\[.+\\]")
+ .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
+ .assertLogMatch("connectEnd: http/1.1")
+ .assertLogMatch(
+ "connectionAcquired: Connection\\{"
+ + url.host()
+ + ":\\d+, proxy=DIRECT hostAddress="
+ + url.host()
+ + "/.+ cipherSuite=none protocol=http/1\\.1\\}")
+ .assertLogMatch("requestHeadersStart")
+ .assertLogMatch("requestHeadersEnd")
+ .assertLogMatch("responseHeadersStart")
+ .assertLogMatch(
+ "responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url="
+ + url
+ + "}")
+ .assertLogMatch("responseBodyStart")
+ .assertLogMatch("responseBodyEnd: byteCount=6")
+ .assertLogMatch("connectionReleased")
+ .assertLogMatch("callEnd")
+ .assertNoMoreLogs();
+ }
+
+ @Test
+ public void post() throws IOException {
+ server.enqueue(new MockResponse());
+ client.newCall(request().post(RequestBody.create(PLAIN, "Hello!")).build()).execute();
+
+ logRecorder
+ .assertLogMatch("callStart: Request\\{method=POST, url=" + url + ", tags=\\{\\}\\}")
+ .assertLogMatch("dnsStart: " + url.host())
+ .assertLogMatch("dnsEnd: \\[.+\\]")
+ .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
+ .assertLogMatch("connectEnd: http/1.1")
+ .assertLogMatch(
+ "connectionAcquired: Connection\\{"
+ + url.host()
+ + ":\\d+, proxy=DIRECT hostAddress="
+ + url.host()
+ + "/.+ cipherSuite=none protocol=http/1\\.1\\}")
+ .assertLogMatch("requestHeadersStart")
+ .assertLogMatch("requestHeadersEnd")
+ .assertLogMatch("requestBodyStart")
+ .assertLogMatch("requestBodyEnd: byteCount=6")
+ .assertLogMatch("responseHeadersStart")
+ .assertLogMatch(
+ "responseHeadersEnd: Response\\{protocol=http/1\\.1, code=200, message=OK, url="
+ + url
+ + "}")
+ .assertLogMatch("responseBodyStart")
+ .assertLogMatch("responseBodyEnd: byteCount=0")
+ .assertLogMatch("connectionReleased")
+ .assertLogMatch("callEnd")
+ .assertNoMoreLogs();
+ }
+
+ @Test
+ public void secureGet() throws Exception {
+ server.useHttps(handshakeCertificates.sslSocketFactory(), false);
+ url = server.url("/");
+
+ server.enqueue(new MockResponse());
+ Response response = client.newCall(request().build()).execute();
+ assertNotNull(response.body());
+ response.body().bytes();
+
+ logRecorder
+ .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}")
+ .assertLogMatch("dnsStart: " + url.host())
+ .assertLogMatch("dnsEnd: \\[.+\\]")
+ .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
+ .assertLogMatch("secureConnectStart")
+ .assertLogMatch("secureConnectEnd")
+ .assertLogMatch("connectEnd: h2")
+ .assertLogMatch(
+ "connectionAcquired: Connection\\{"
+ + url.host()
+ + ":\\d+, proxy=DIRECT hostAddress="
+ + url.host()
+ + "/.+ cipherSuite=.+ protocol=h2}")
+ .assertLogMatch("requestHeadersStart")
+ .assertLogMatch("requestHeadersEnd")
+ .assertLogMatch("responseHeadersStart")
+ .assertLogMatch(
+ "responseHeadersEnd: Response\\{protocol=h2, code=200, message=, url=" + url + "}")
+ .assertLogMatch("responseBodyStart")
+ .assertLogMatch("responseBodyEnd: byteCount=0")
+ .assertLogMatch("connectionReleased")
+ .assertLogMatch("callEnd")
+ .assertNoMoreLogs();
+ }
+
+ @Test
+ public void dnsFail() throws IOException {
+ client =
+ new OkHttpClient.Builder()
+ .dns(
+ new Dns() {
+ @Override
+ public List lookup(String hostname) throws UnknownHostException {
+ throw new UnknownHostException("reason");
+ }
+ })
+ .eventListenerFactory(loggingEventListenerFactory)
+ .build();
+
+ try {
+ client.newCall(request().build()).execute();
+ fail();
+ } catch (UnknownHostException expected) {
+ }
+
+ logRecorder
+ .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}")
+ .assertLogMatch("dnsStart: " + url.host())
+ .assertLogMatch("callFailed: java.net.UnknownHostException: reason")
+ .assertNoMoreLogs();
+ }
+
+ @Test
+ public void connectFail() {
+ server.useHttps(handshakeCertificates.sslSocketFactory(), false);
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+ url = server.url("/");
+
+ try {
+ client.newCall(request().build()).execute();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ logRecorder
+ .assertLogMatch("callStart: Request\\{method=GET, url=" + url + ", tags=\\{\\}\\}")
+ .assertLogMatch("dnsStart: " + url.host())
+ .assertLogMatch("dnsEnd: \\[.+\\]")
+ .assertLogMatch("connectStart: " + url.host() + "/.+ DIRECT")
+ .assertLogMatch("secureConnectStart")
+ .assertLogMatch(
+ "connectFailed: null javax\\.net\\.ssl\\.SSLProtocolException: Handshake message sequence violation, 1")
+ .assertLogMatch(
+ "callFailed: javax.net.ssl.SSLProtocolException: Handshake message sequence violation, 1")
+ .assertNoMoreLogs();
+ }
+
+ private Request.Builder request() {
+ return new Request.Builder().url(url);
+ }
+
+ private static class LogRecorder extends HttpLoggingInterceptorTest.LogRecorder {
+ LogRecorder assertLogMatch(String pattern) {
+ return (LogRecorder) super.assertLogMatch("\\[\\d+ ms] " + pattern);
+ }
+ }
+}